Skip to content

How the engine reaches a finding

Two kinds of work happen in this pipeline: deterministic analysis that returns the same answer for the same input, and a model pass that argues about reachability. This page documents both, including where the second is allowed to overrule the first — and where nothing overrules anything.

From a signed webhook to a patch pull request

The same figure used on the overview, in the place where its stages are the subject rather than a summary. Every one of the six is a real pass: two are deterministic analysis, one is a model that argues, and one is a human who has to reproduce the finding before it is reported.

Signed webhook intakeHMAC verified before any fetch
webhook.log
POST /api/webhooks/github          HTTP/1.1
X-Hub-Signature-256: sha256=9f2c…  ✓ verified
X-GitHub-Event: pull_request

{
  "action": "opened",
  "repository": { "full_name": "acme/vault-core" },
  "pull_request": {
    "number": 482,
    "head": { "sha": "a41d9c2" },
    "changed_files": 7,
    "additions": 214
  }
}

A push that fails signature verification is dropped without a response body. There is no path in the pipeline that clones a repository before its origin has been proven.

  • Every pull request

    incremental

    Analysis runs on the diff, not the whole repository, so a review lands in minutes rather than hours.

  • Every dependency bump

    reachability

    A version change re-checks the call sites that touch it, and flags newly reachable vulnerable paths.

  • Every deploy gate

    blocking

    Critical findings block the merge. Nothing is silently downgraded to keep a pipeline green.

  • Every regression

    permanent

    A fixed bug becomes a permanent failing-then-passing test, so it cannot return unnoticed.

Rule packsEVM · SolidityEVM · VyperSVM · Rust/AnchorTypeScriptSolidity IR
  1. Repository webhook

    A push or pull request fires a signed GitHub or GitLab webhook. The diff is hashed and queued; nothing is cloned until the signature verifies.

    HMAC-signed · <200ms
  2. AST & call-graph build

    Sources are parsed into an AST, storage layouts are reconstructed, and an inter-procedural call graph is built so taint can be traced across function boundaries.

    Solidity · Rust · TS
  3. Symbolic & fuzz execution

    Property tests and symbolic execution explore reachable states, then a fuzzing campaign tries to violate the invariants declared in the audit scope.

    Halmos · Echidna · Trident
  4. Reasoning ensemble

    Claude receives the candidate set and the source, and its first job is to argue each candidate is unreachable. Only what survives is passed to a second pass that writes the fix.

    Mythos / Fable → Opus
  5. Human verification

    A security researcher reproduces every surviving finding. Anything that cannot be demonstrated against a running target is downgraded or dropped.

    No unreproduced findings
  6. Patch PR & regression gate

    A minimal fix lands as a pull request with a failing-then-passing regression test. The gate stays armed so the bug cannot silently return.

    Auto-PR · CI enforced
Inter-procedural taint traceoutput of stage 02
handle()untrusted input_burn()no validation_settle()delegatecallbalances[]state writedirect call · skips a frame

A rule that only reads one function body sees _settle() and nothing that taints it. The path above crosses two frames before it reaches the state write, which is why the graph is reconstructed before any rule runs.

The rule pack runs on the server; the reasoning pass runs beside itRuns against GitHub, GitLab and Bitbucket

What the deterministic pack checks

Thirty rule classes across three languages, split 15 Solidity, 8 Vyper and 7 Rust/Anchor. Each one is a source-level check with no network call and no hidden state, so the same input always produces the same findings. The full inventory is below rather than summarised, because a count a reader cannot check is a claim rather than a fact.

Solidity

15 rules

EVM

The EVM pack, over the classes that account for most real-world loss: reentrancy, access control, unchecked returns, oracle manipulation and unsafe upgradeability.

  • SC-REENTRANCY-01Reentrancy: external call precedes state update
    Critical
  • SC-ACCESS-01Missing access control on a state-mutating function
    Critical
  • SC-TXORIGIN-01Authorization via tx.origin
    High
  • SC-UNCHECKED-CALL-01Unchecked return value from a low-level call
    Medium
  • SC-UNCHECKED-ERC20-01ERC-20 transfer return value not checked
    High
  • SC-RANDOMNESS-01Weak on-chain randomness
    High
  • SC-DELEGATECALL-01delegatecall to a caller-controlled address
    Critical
  • SC-SELFDESTRUCT-01Unprotected selfdestruct
    Critical
  • SC-ORACLE-01Spot price read from own reserves
    Critical
  • SC-INITIALIZER-01Unprotected initializer
    Critical
  • SC-ARITHMETIC-01Arithmetic on an unchecked compiler version
    High
  • SC-LOOP-01Unbounded loop over a growable array
    Medium
  • SC-APPROVE-01Unlimited token approval
    Low
  • SC-TIMESTAMP-01block.timestamp used in critical logic
    Low
  • SC-PRAGMA-01Floating pragma
    Info

Vyper

8 rules

EVM

A separate pack, not Solidity rules relabelled. Every pattern below is written against Vyper syntax, because a Solidity pattern matches no Vyper source at all.

  • VY-REENTRANCY-01Reentrancy: external call precedes state update
    Critical
  • VY-ACCESS-01Missing access control on a privileged state-mutating function
    Critical
  • VY-DELEGATECALL-01Delegatecall into a target held in mutable storage
    Critical
  • VY-UNCHECKED-CALL-01raw_call with revert_on_failure disabled
    High
  • VY-TXORIGIN-01Authorization via tx.origin
    High
  • VY-BALANCE-01Accounting derived from self.balance
    High
  • VY-SELFDESTRUCT-01Contract can be emptied in a single call
    High
  • VY-VERSION-01Floating or missing compiler version
    Info

Rust / Anchor

7 rules

SVM

The Solana pack, targeting the failure modes of that runtime — account confusion, arbitrary cross-program invocation, and unvalidated signer authority — rather than reused EVM heuristics.

  • RS-SIGNER-01Mutable account without a signer check
    Critical
  • RS-LAMPORTS-01Manual lamport mutation via try_borrow_mut_lamports
    Critical
  • RS-CPI-01CPI to an unvalidated program
    Critical
  • RS-ARITHMETIC-01Unchecked arithmetic on token amounts
    High
  • RS-UNWRAP-01Panicking unwrap on instruction-path data
    Medium
  • RS-RENT-01Account may fall below rent exemption
    Medium
  • RS-TRANSMUTE-01Unsafe memory reinterpretation
    High

Three of the thirty carry a runnable Foundry proof of concept. The other twenty-seven return the finding, its line, its impact and a remediation, but no test harness — and nothing on this site implies they do.

Ships a proof of conceptSC-REENTRANCY-01SC-ACCESS-01SC-ORACLE-01

Why Vyper has its own pack

Vyper is a different language, not a dialect of Solidity. It uses def rather than function, assert rather than require, self.x rather than a bare storage variable, raw_call rather than call, and it has no braces at all. A Solidity pattern applied to Vyper source matches nothing, which means claiming Vyper is covered from a Solidity pack is not a description of the work — it is a description of its absence.

Solidity
function setOwner(address newOwner) external {
    require(msg.sender == owner, "not owner");
    owner = newOwner;
}
Vyper
@external
def set_owner(new_owner: address):
    assert msg.sender == self.owner, "not owner"
    self.owner = new_owner
Function declaration
function transfer(address to) external {
def transfer(to: address):
Authorisation guard
require(msg.sender == owner, "not owner");
assert msg.sender == self.owner, "not owner"
Storage write
balances[msg.sender] -= amount;
self.balances[msg.sender] -= amount
External call
(bool ok, ) = target.call(data);
raw_call(target, data)
Block delimiters
{ … }
indentation only — there are no braces

Two of the eight have no Solidity counterpart, because the bug class belongs to the language.

VY-UNCHECKED-CALL-01

revert_on_failure=False swallows a failed call

High

By default a raw_call reverts when the callee reverts. Setting revert_on_failure=False turns that into a returned success flag instead — and when the statement binds no variable, a failed call and a successful one become indistinguishable at the call site. The transaction continues down a path that assumes value moved while nothing did. Solidity reaches the same place through a discarded boolean, but the spelling is different enough that it needs its own rule.

VY-UNCHECKED-CALL-01
@external
def settle(user: address, amount: uint256):
    # a revert inside the callee never reaches this contract
    raw_call(self.token, method_id("transfer(address,uint256)"),
             args=[user, amount], revert_on_failure=False)
    self.owed[user] -= amount
VY-DELEGATECALL-01

raw_call(..., is_delegate_call=True) into mutable storage

Critical

A delegate call executes the callee in the caller contract own context, with full control of its storage and balance. When the target is read from a self. slot that is neither immutable nor constant, whoever can write that slot — through a setter, an initializer, or an incomplete access check — can execute arbitrary code as this contract. The rule only fires when the target is mutable storage; an immutable or constant declaration is accepted as pinned.

VY-DELEGATECALL-01
implementation: public(address)   # mutable — reassignable

@external
def execute(data: Bytes[1024]):
    raw_call(self.implementation, data, is_delegate_call=True)

Two rules that reason about position

A rule that matches a pattern is easy to write and easy to be wrong with. These two are the clearest cases of a rule that has to reason about the relationship between lines — one about ordering, one about a conjunction — and both are documented here with the case a naive matcher would get wrong.

Reentrancy is a question about order

SC-REENTRANCY-01

Does control leave the contract before the state that records the effect is written?

The rule locates the first external call in the function body, then asks whether a balance or status write appears after it. It raises a finding only when the call comes first — the ordering that lets a re-entrant callee observe stale state. A function that writes first and calls second is checks-effects-interactions already, and the rule is silent on it, which is why it does not flag every function that happens to contain a call.

A pure line-matcher that looked for an external call and a state write in the same function would flag both of the functions below. The rule distinguishes them by which comes first.

Fires — the call precedes the writefires · critical
SC-REENTRANCY-01
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok);
    balances[msg.sender] -= amount;   // written after control returns
}
Silent — the write precedes the callno finding
SC-REENTRANCY-01
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;   // written before control leaves
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok);
}

Access control is a conjunction, not a keyword

SC-ACCESS-01

Is a privileged function mutating a privileged variable without a sender check?

The rule requires two things to be true at once: the function name implies privileged authority, and the body writes a privileged variable such as the owner, the implementation pointer or the fee recipient. Only then does a missing authorisation modifier or sender check become a finding. Either half alone is noise, and noise is the fastest way to lose a triager.

The privileged-name list is deliberately narrow. A generic verb such as withdraw or update belongs to plenty of legitimate user-facing functions, so it is not on the list, and a withdraw that writes only caller-scoped state never reaches the privileged-variable test. The rule does not flag it, with or without a guard.

Fires — privileged name and privileged writefires · critical
SC-ACCESS-01
// setOracle matches the privileged-name set, and oracle is a
// privileged variable, but nothing checks the caller.
function setOracle(address next) external {
    oracle = next;
}
Silent — user-facing name, caller-scoped writeno finding
SC-ACCESS-01
// withdraw is not a privileged name and balances is not a
// privileged variable, so the missing guard is not a finding.
function withdraw(uint256 amount) external {
    balances[msg.sender] -= amount;
    payable(msg.sender).transfer(amount);
}

Scoring, and telling a new report from an old one

Two pieces of arithmetic sit behind a triage result: a CVSS v3.1 base score, and a similarity check against the submissions the programme has already received. Both are computed on the server with no model involved, so both can be checked.

CVSS v3.1, scored on integers

Triage derives a real CVSS v3.1 base score from the specification, including its own Roundup function — the step that made the 3.0 formula notorious. Roundup rounds a value up to one decimal place, but computing it in floating point turns values such as 4.02 into 4.0 instead of 4.1. The implementation scales the input to an integer first, so the rounding decision is made on whole numbers and there is no drift to argue about.

  • An explicit vector in the report always wins: if the reporter supplies CVSS:3.1/AV:N/..., the engine parses and scores that rather than re-deriving one.
  • Otherwise the vector starts from the matched vulnerability class and is adjusted by what the report text actually asserts — pre-auth, user interaction, scope change, confidentiality and availability impact.
  • The base score, the severity band and the full vector are all returned, so the arithmetic can be checked rather than trusted.
roundUp.ts
function roundUp(input: number): number {
  const intInput = Math.round(input * 100_000);
  if (intInput % 10_000 === 0) return intInput / 100_000;
  return (Math.floor(intInput / 10_000) + 1) / 10;
}

The contract posture score is a separate number

A contract scan returns a posture score from 0 to 100, and it is not a CVSS score. Each finding contributes a weight by severity and a confidence factor, the weights are summed into a risk mass, and the score is an exponential decay of that mass. A clean contract scores 100. The formula is deliberately shallow: it is a summary of the findings above it, and it is never the only thing a reader sees.

Telling a new report from a variant of an old one

Duplicate detection is a cosine similarity over term-frequency vectors, computed on the server with no model involved. The report title, asset and body are tokenised, stopwords and one- and two-character tokens are dropped, and each report becomes a sparse vector of term counts. The cosine of the angle between two of those vectors is 1 when they use the same terms in the same proportions and 0 when they share none.

  • A matching asset host adds 0.12 to the score — the same endpoint is strong evidence on its own, even when the prose reads differently.
  • A matching vulnerability class adds 0.1.
  • The result is capped below 1.0, because two reports are never identical artefacts, and the four closest matches are returned rather than one.
High
top match at 0.55 or above — confirm novelty before a payout is considered
Moderate
top match at 0.35 or above — check the affected asset differs before accepting
Low
no close match — treated as novel pending asset verification

One model argues, another has to agree

A single model asked whether a piece of code is exploitable will usually find a reason to say yes. So the first pass is asked to prove each candidate wrong, and only what survives it is passed to the pass that writes the fix. Each pass runs on a different model tier, at a different route, on a different question.

Anthropic

Claude Mythos 5.1

Adversarial refutation

The frontier tier. Receives every candidate finding from the rule pack with one instruction: prove it is wrong. It reads the source looking for the guard, modifier or unreachable branch that makes the reported path a false positive, and returns a verdict only — this pass is not allowed to write code.

Where the deployment is admitted to the Cyber Verification Program · /api/review

Anthropic

Claude Fable 5.1

Adversarial refutation, general release

The same core model as Mythos 5.1, served with the general-release safeguards rather than the restricted ones. It runs the identical refutation pass, and it is what every deployment without Cyber Verification Program admission actually gets. Nothing about the verdict changes — only which release of the model produces it.

The default for this pass · /api/review

Anthropic

Claude Opus 5.5

Patch synthesis

Runs only over what survived refutation. For each survivor it writes the smallest change that closes the finding, plus a test that fails against the current source and passes against the patch.

Survivors only · emits the proposed diff

Anthropic

Claude Sonnet 5.5

Triage judgement

Reads an inbound bug report against the CVSS vector the deterministic engine derived, and judges the inputs rather than the arithmetic: impact the reproduction steps cannot support, evidence asserted but not attached, a class that does not match the mechanism.

Every triaged report · /api/triage/notes

Refutation

Claude Mythos 5.1claude-mythos-5-1/api/review
Receives

The source, and one candidate per rule-pack finding with its title, severity, line, snippet and description.

Returns

A verdict of confirmed, refuted or uncertain, with a short rationale naming the guard or path that decides it. This pass is not allowed to write code.

Twelve candidates per request, highest severity first. Served by the frontier tier: Mythos 5.1 where the deployment is admitted to Anthropic’s Cyber Verification Program, Fable 5.1 — the same core model under general-release safeguards — everywhere else. /api/health reports which one this deployment resolves to.

Patch synthesis

Claude Opus 5.5claude-opus-5-5/api/review
Receives

The source, and only the findings the refutation pass confirmed, with the reviewer reasoning attached.

Returns

A unified diff that makes the smallest change closing the finding, and a regression test that fails against the current source and passes against the patch.

Six patches per request. When nothing survives refutation, this pass does not run at all and is reported as not run.

Triage judgement

Claude Sonnet 5.5claude-sonnet-5-5/api/triage/notes
Receives

The submitted report and the deterministic engine output — the extracted fields, the CVSS vector and the base score.

Returns

A read of what the report establishes as distinct from what it claims, a keep, raise or lower call on the severity band, the evidence it asserts but does not supply, and questions worth putting back to the reporter.

One pass per report, on the fast tier.

What the refutation pass is not shown

The prompt for the refutation pass deliberately leaves out two fields the rule pack produces: its long-form recommendation and its proof-of-concept string. Both are the rule pack argument for its own finding, and handing a reviewer the argument it is meant to test anchors it toward agreement. The reviewer gets the finding and the source, and has to reach its own conclusion. The same discipline runs through the output: a verdict about a finding that was never sent is dropped rather than displayed, because it would be advice about code the reviewer was not shown.

Deterministic, simulated, or a model pass

Every result on this site is one of three things, and the interface says which. Two of the three are deterministic; one is a labelled simulation; the model passes are advisory and are reported as not run when no key is configured rather than quietly skipped.

Contract rule packDeterministic
Identical source yields identical findings, on every run and every deployment. No network call and no hidden state sits between the input and the result.
Contract posture scoreDeterministic
Weighted severity and confidence decayed into a 0–100 summary, computed locally from the findings above it.
Triage parse, CVSS and similarityDeterministic
The report is parsed into sections, a real CVSS v3.1 base score is derived, and duplicate matching runs against the submission corpus — all arithmetic, no model.
Web posture reportLabelled simulation
Derived from an FNV-1a hash of the hostname through a seeded generator, across 18 weighted checks. The same URL always produces the same report, and no traffic is sent to the target.
Refutation and patch synthesisModel pass
Advisory, and labelled as such in the interface. It sits beside the deterministic result, never on top of it, and is reported as not run when no key is configured.
Triage judgementModel pass
Reads the report against the vector the deterministic engine derived and judges the inputs to the score, not the arithmetic.

The API surface

Six endpoints behind the sandbox. The deterministic engines and the model passes are separate routes on purpose: a slow, rate-limited or unreachable model cannot hold up or invalidate the deterministic result a reader is looking at. The model routes take only the raw input and re-run the engine themselves, so they can never be driven as a general-purpose proxy for the model key.

GET/api/healthDeterministic
body none

Reports the engine version, the detector count in each pack, and whether a model key is configured on this deployment — never the key, and never a claim that the models are reachable.

POST/api/auditDeterministic
body { source }

Runs the rule pack over one source file and returns the findings, the posture score and a plain-language trace. The source is parsed, scored and discarded with the response.

POST/api/scan-webLabelled simulation
body { target }

Derives a representative posture report from the hostname. It opens no socket, and the response carries a notice field the interface surfaces to the reader.

POST/api/triageDeterministic
body { report }

Parses an inbound bug report, derives the CVSS v3.1 base score, scores the write-up and runs duplicate matching. No model is involved.

POST/api/triage/notesModel pass
body { report }

The triage judgement pass. It re-runs the deterministic engine itself and never accepts a caller-supplied finding list, which is what stops it working as a general-purpose proxy for the model key.

POST/api/reviewModel pass
body { source }

The refutation and patch pass, deliberately separate from the audit route so a slow or rate-limited model cannot hold up or invalidate the deterministic result a reader is looking at.

Point the engine at your own code

The sandbox runs the same rule pack documented here, on the server, and discards your source with the response. Start with a single function.