新功能: 智能体审批、红队测试与模型扫描。 了解详情
Docs / 安全 / Model Scanning

Model Scanning

A model artifact is the one thing in your AI system that is executable, opaque and shipped by someone else. A PyTorch checkpoint is a pickle stream, and a pickle stream is a program: loading one runs whatever it names. A chat template is rendered into every conversation. A repository with auto_map in its config asks the loader to import Python that came with the weights.

None of that appears in a dependency manifest, and none of it is what a software composition scanner looks at. Model scanning reads the artifacts themselves and reports what is in them, with the byte offsets to prove it.

Declare a repository at Models and MITRITY scans it on a schedule, or on demand. The models your agents call through the LLM gateway appear in the same inventory on their own — see Hosted models observed at the gateway.

What is scanned

InOut
Pickle streams (protocols 0–5), bare and inside PyTorch zip containersExecuting, importing or resolving anything a stream names
safetensors and GGUF header integrityVerifying tensor values or model behavior
Hidden instructions in model cards, configuration, metadata and chat templatesJudging whether a model is good, biased or accurate
The trust_remote_code surface: auto_map, custom_pipelines, shipped .pyFull static analysis of Python
Repository hygiene: pinning, licensing, model card, format choiceLegal advice on a license

The scanner never resolves, imports or invokes anything a file names. A pickle disassembler walks the opcode stream and records every global it references and which of them the stream calls; it never executes a REDUCE.

Nothing is downloaded

Every format is read from the part of the file that carries its structure, through ranged requests. A thirty-gigabyte repository is scanned with roughly a megabyte of transfer.

FileWhat is fetchedWhy
*.safetensorsThe first 64 KiB; if the declared header is longer, one more request for exactly the header, up to 16 MiBThe length prefix and JSON header sit at the start.
*.gguf, *.ggmlThe first 64 KiBThe magic bytes, counts and metadata block sit at the start. Which reader runs is decided by the bytes, not the extension.
*.bin, *.pt, *.pth, *.ckpt, *.pkl, *.pickleThe last 64 KiB, then the members named by the archive directoryThe same read answers two questions: whether the file is a zip container, and how large it is. Only the pickle members are then read.
the same, when there is no archive directoryThe whole file, capped at 64 MiBAn older torch.save wrote a bare pickle. Over the cap the file is reported as unknown.
*.md, *.txt, *.rst, *.json, *.yaml, *.py, *.jinjaThe first 1 MiBText and configuration. Over the cap, the rest is unknown.
anything elsenothingNo scanner reads it, and it costs no request.

Two consequences are worth stating. config.json is read before any .py, so the scanner knows which Python the config imports on load before it reports on it. And every file is fetched at the commit the source resolved, not at the branch name, so one scan covers one revision even if the branch moves while it runs.

Finding categories

Every finding declares one category, so you can tell what kind of result you are looking at without reading the detail text.

CategoryMeaning
securityThe artifact can harm the system that loads it.
supply_chainThe artifact's provenance or integrity is in question, rather than its behavior.
complianceUsing the artifact carries licensing or policy obligations.
hygieneA practice worth improving. Never a vulnerability.

Severities are info, low, medium, high and critical, in that order. A repository's severity is the highest among its findings. A repository with no findings is info — never "clean" as a separate state.

Security versus hygiene

The dashboard gives the two a different treatment because they are not the same kind of result. The distinction the product holds to:

A security finding saysA hygiene finding says
Loading this artifact can run code, exfiltrate data or change the model's instructions.This artifact is harder to govern than it needs to be.
It names the file, and where in the file.It names a practice, or a limit of the scan.
It ranks up to critical.It never ranks above medium.
Remediation is an action against the artifact.Remediation is a change in how the artifact is published or consumed.

weights_pickle_only is the case that shows why the split matters. Shipping pickle weights is not itself a vulnerability — the finding is not evidence of anything in the file, and a repository with clean pickle weights is not unsafe. It is hygiene: safetensors removes a class of risk that pickle cannot. Calling it a vulnerability would train you to ignore the category.

What the findings cover

Pickle streams. Every global the stream references is recorded, and every one it calls through REDUCE, INST, OBJ, NEWOBJ or NEWOBJ_EX. A known remote-code-execution or remote-load target — os.system, subprocess.*, builtins.eval, torch.load, numpy.load and their neighbors — is critical whether the stream calls it or merely imports it. A global the stream calls but that is not on the allowlist is high; one it only imports is medium. An EXT opcode or a STACK_GLOBAL whose operands are assembled at load time is high, because the target is not in the file to review.

PyTorch archives. Executable and script members in a weights archive, files under extra/ that are restored to disk on load, member paths that escape the extraction directory, and nested archives this scan does not reach into.

safetensors and GGUF. Header integrity: a declared header length the file cannot hold, tensors claiming overlapping bytes or bytes past the data block, implausible tensor counts, and a magic that disagrees with the file's extension.

Hidden instructions. Text that addresses a model rather than a reader, in model cards, documentation, JSON configuration values, format metadata and chat templates: instruction overrides, role hijacking, exfiltration, invisible characters, lookalike substitutions and base64 blocks that decode to instructions. A chat template is judged differently from a README — emitting <|im_start|>system is what a chat template is for — but text that instructs the model still counts, because the template is prepended to every conversation.

Remote code. A config declaring auto_map or custom_pipelines means loading the repository requires trust_remote_code=True, which runs its Python on your machine. Shipped Python that evaluates code, starts processes or decodes and runs a payload is critical.

Repository level. An unpinned revision, weights shipped only in the pickle format, a missing or use-restricting license, a missing model card, and the budgets a scan ran into.

Every finding is verifiable

If you disagree with a result, you can check it against the file.

  • Every pickle finding carries the byte offset, the opcode mnemonic, and the module and name the stream reaches. Open the file at that offset and the opcode the finding is about is there.
  • Every text finding carries an excerpt bounded at 240 characters — evidence, not a copy of the document.
  • A finding whose severity comes from a curated table carries that table's version, so a re-scan after a curation change clears stale results instead of leaving you to triage them twice.

Incomplete is never reported as clean

A scanner that quietly gives up on a file it could not read is worse than no scanner, because it turns an unknown into a pass. Every path that cannot finish emits a finding and marks the scan incomplete: a pickle stream that ended before its STOP opcode, an archive directory that could not be read, a header larger than the scan budget, a file over its size cap, a repository with more scannable files than the per-repository budget.

The dashboard says so on the inventory row and again at the top of the repository, and an empty findings list under an incomplete scan is labelled as incomplete rather than empty. Re-scan before relying on the result.

Pin the revision

A scan of main tells you about main on the day it ran. The scan records the commit the source resolved, and reports unpinned_revision so you can pin to it. Once the repository is pinned to a commit, a later push to the branch cannot change what you load, and the scan result stays true for as long as the pin holds.

Gated and private repositories

A repository that needs credentials takes an access token, stored write-only. The token is used only to read that repository, it is dropped if a file request redirects to another host, and it is never returned by the API or displayed in the dashboard — Models reports only that a token is held, and offers to replace or clear it.

Access being gated or private is itself recorded, as repo_gated and repo_private, so your inventory reflects how each repository was reached.

Hosted models observed at the gateway

A model your agents reach through a provider API is not a file you can scan, but it is very much part of your inventory. Every call governed by the LLM gateway names a provider and a model, and MITRITY turns those names into inventory rows: one per provider and model, of kind hosted, with the identity model:<provider>:<model> — model:azure-openai:gpt-4o, model:vertex:gemini-2.5-pro. An Azure deployment is resolved to the model it serves, so the row is the model and the deployment names are listed beside it.

A model first called this morning is in the inventory this morning: the gateway's usage records are swept every few minutes, and a nightly pass reads everything so nothing that arrived late is missed. A hosted row shows what the gateway has seen — calls, calls a policy blocked, the agents behind them, tokens in and out, the deployments the calls came through — and links to the LLM gateway dashboard for the full usage picture. The LLM Bill of Materials there links back to the inventory row.

Nothing about a hosted model is fetched, so it is never scanned. It is judged on every sweep against two things you control:

CodeCategorySeverityMeaning
model_not_in_any_allowlistcompliancemediumNo policy rule names the model. No allow rule admits llm:send:<provider>:<model> and no deny rule refuses it, so whether an agent may call it is decided by the policy default alone. Allow and deny rules both count — a model you denied is a model you decided about. The finding names the action type to write a rule for and resolves on the next sweep once a rule names the model.
model_known_badsupply_chainthe entry's ownThe model is on the curated list of known-unsafe models, by name or as part of a provider or family entry. The finding names the entry it matched, and resolves on the next sweep once the entry is retired.

The rule check uses the same matching the gateway applies — llm:send:azure-openai:* names every Azure OpenAI model, llm:send:anthropic:claude-* a family, and a regex: pattern is matched unanchored — so a rule that admits or refuses a call in the gateway is the rule that names it here. Rules bound to a catalog tool rather than an action pattern are not consulted.

A model_known_bad finding on a hosted model arrives through the same notification channels as one on a repository, under the model.risk_advisory type; the policy cross-check stays in the dashboard.

Acting on a finding

Open a repository from Models to see its summary, the reach of its last scan, and every finding with its evidence and its remediation. Each open finding can be closed two ways:

  • Resolve — the finding has been dealt with.
  • Accept risk — the finding still stands and you are choosing to live with it.

Both take an optional note, and both are recorded. A later scan that sees the same thing opens a new finding, so accepting a risk today does not silence it forever.

New advisories for a scanned repository, and known-unsafe matches on a hosted model, arrive through your configured notification channels — email, Slack, webhook and the in-portal bell — under the model.risk_advisory type. Configure them in Notification Preferences.

  • AI Bill of Materials — the inventory this fits into: the MCP servers your agents reach, alongside the models they load.
  • Injection Detection — the runtime counterpart. A model with hidden instructions in its card or its chat template is one source of the prompt injection the edge detects at request time.