SkillTotal Scoring

June 28, 2026 · View on GitHub

Scoring is deterministic and derived only from the component's findings.

Severity weights

SeverityWeight
critical+30
high+20
medium+10
low+3

Score

risk_score = min(100, Σ weight(finding.severity) for findings that are risk, not capability)

The sum is over distinct findings (one finding per rule), so repeated matches of the same rule do not inflate the score — only a new kind of risk does. The score is capped at 100.

Capability is not risk. Only findings whose threat_class is malicious_indicator or risky_construct contribute to the score. Neutral capability findings (shell execution, filesystem read/write, network egress, MCP tool surface) are still reported — as findings and as capability chips — but contribute 0. A legitimate-but-powerful component is therefore not pushed into the red by what it can do; the score and verdict reflect actual risk (deliberate malice and dangerous constructs), not raw capability.

Risk levels

Score rangeLevel
0–24low
25–49medium
50–74high
75–100critical

Default severities by category

CategorySeverity
Shell executionhigh
Sensitive path accesshigh
Filesystem access (read/write)medium
Network egressmedium
Install-time executionhigh
Dynamic code executionhigh
Obfuscation (decode-and-execute)high
Prompt injection indicatorsmedium
Broad MCP tools (shell/fs/network/credential)high
MCP server launches a commandhigh
MCP surface detectedlow

Combination rule (sensitive data + network ⇒ critical)

If a component both accesses sensitive data — a credential-location reference (ST-SENS-PATH) or an embedded secret (ST-SECRET-EMBEDDED) — and has network_egress, the engine adds one synthesized critical risky_construct finding, ST-COMBO-EXFIL ("Sensitive-data access combined with network egress"). Its evidence is drawn (de-duplicated) from the contributing finding/capabilities, so the evidence invariant still holds. This makes the credential-exfiltration path explicit and scored.

Note this is sensitivity-gated: plain filesystem access plus network is a neutral capability combination (legitimate tools read files and use the network) and is not flagged — only access to secret data combined with an egress channel is.

Taint: untrusted input → dangerous sink (Python)

Beyond raw capability, the Python AST scanner reports a proven data flow from an untrusted source (environment, sys.argv, input(), a network response body, or an MCP tool-handler argument) to a dangerous sink, as risky_construct (high):

  • ST-TAINT-EXEC-PY — reaches eval / exec / compile
  • ST-TAINT-SHELL-PY — reaches a shell (os.system / os.popen / subprocess(..., shell=True))
  • ST-TAINT-DESERIAL-PY — reaches unsafe deserialization

These upgrade an already-reported capability (ST-DYN-PY / ST-SHELL-PY / ST-DESERIALIZE-PY, weight 0) into a scored risk; the capability finding still appears. This is stronger than ST-CMDI-PY (which flags any non-constant shell command): when taint proves an untrusted source reaches the shell, ST-TAINT-SHELL-PY supersedes ST-CMDI-PY on that node so the injection is scored once. The analysis is conservative (default-deny propagation, sanitizers clear taint, no inter-procedural tracking) to keep benign false positives at zero.

Agent Skill: declared vs actual (least privilege)

For an agent_skill component (a folder with a SKILL.md), the engine compares the skill's declared allowed-tools against what its bundled code actually does. If the skill declares a non-empty, non-wildcard allow-list but the code exercises a dangerous capability those tools do not grant — shell, network, filesystem-write, dynamic code execution, or install-time execution — it synthesizes ST-SKILL-CAP-MISMATCH (risky_construct, medium). filesystem_read is not checked (benign). This is the deterministic, evidence-anchored counterpart to "the skill does more than it claims": no LLM, fires only when there is an explicit declaration to contradict.

Evidence-context demotion (what does NOT score)

Matches that are not executed, agent-facing behavior are moved to needs_review before scoring, so the engine does not flag descriptions of attacks or its own detection patterns:

  • Test codetests/, *.test.*, conftest.py, compound test trees (cli-e2e-tests/, integration-tests/), … (not run by consumers).
  • Documentation / proseREADME, CHANGELOG, LICENSE, docs/, *.egg-info/PKG-INFO, ignore-files. AI-instruction surfaces (SKILL.md, AGENTS.md, MCP manifests, .cursorrules) are kept in scope — a real injection lives there.
  • Data / eval / benchmark corpora — inert reference data files under eval_datasets/, fixtures/, benchmarks/, datasets/, snapshots/, … (is_data_corpus_path). A prompt-injection string in an eval dataset is a detector test vector, not behavior. Restricted to non-code suffixes, so a real payload shipped as code in such a directory is still scanned/scored.
  • Python string literals / comments, shell # comments, and C-family // /* */ comments (.ts/.js/.go/.rs/…) — a behavior/text detector matching its own regex literal or a docstring example is not behavior; a # Usage: curl … | bash line is a doc comment, not a runnable pipe-to-shell. Governed per-rule by code_context (strings_and_comments for decode-exec / tool-poisoning / prompt-injection / sensitive-path; comments for the network-exposure rules and shell pipe-to-shell, whose real positives are value-strings / real commands).
  • Denylist / guardrail credential paths and public DocSearch keys — demoted at the scanner level (sensitive_paths.py, secrets.py): a policy that protects ~/.ssh/id_rsa is not access to it, and an Algolia DocSearch search key sitting next to an app id / index name is public/read-only.

needs_review and the score

Items in needs_review (e.g. lone base64 blobs, ambiguous prompt phrasing, minified files, unparseable manifests) are never scored. They are surfaced for a human to decide, preserving the rule that the score reflects only evidence-confirmed risk.

Worked example (malicious npm fixture)

FindingSeverityThreat classWeight
ST-SENS-PATH (reads ~/.ssh, .aws/credentials)highrisky_construct20
ST-COMBO-EXFIL (sensitive data + network)criticalrisky_construct30
ST-INSTALL-NPMhighcapability0
ST-SHELL-NODEhighcapability0
ST-FS-NODE-READmediumcapability0
ST-NET-NODEmediumcapability0
Sum50

Result: risk_score = 50, risk_level = high. The credential-exfiltration pattern (read secrets + reach the network) drives the score; the raw capabilities are reported but do not. A benign tool with the same capabilities but no sensitive-data access scores 0 / low.