Skip to main content

static-analysis-integration

SARIF-first pre-pass stage for /code-review that runs available static analysis tools and normalizes their output to the unified finding envelope defined in security-primitives-contract v1.0.0. Deduplicates findings across tools and passes confirmed issues to AI agents so they can focus on semantic concerns.

Source facts

Repository
bdfinst/agentic-dev-team
Last source activity
August 29, 2026 at 12:06
Detected SKILL.md language
English
Stars
299
Forks
36

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
13 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
static-analysis-integration
description
SARIF-first pre-pass stage for /code-review that runs available static analysis tools and normalizes their output to the unified finding envelope defined in security-primitives-contract v1.0.0. Deduplicates findings across tools and passes confirmed issues to AI agents so they can focus on semantic concerns.
role
worker
user-invocable
false
version
2.0.0
maintainers
["bdfinst","unassigned"]
required-primitives-contract
^1.0.0
# Static Analysis Integration (SARIF-first) Maintenance policies for adapters and rulesets live in `maintenance.md` — those are out-of-runtime concerns. ## Constraints 1. Collect and report only — read source and tool output; make no code edits. 2. Graceful degradation: if no tools are installed, return `status: skip`; absence is never a pipeline failure. 3. Deduplicate across tools: the same issue reported by multiple tools appears once, attributed to the higher-priority source. ## Tool tiers ### Tier 1 — required baseline (SARIF native) | Tool | SARIF invocation | Capability | |---|---|---| | semgrep | `semgrep scan --sarif --config auto` | SAST | | gitleaks | `gitleaks detect --report-format sarif --report-path - --no-verify` | secrets | | trivy | `trivy config --format sarif --skip-update --offline-scan <path>` + `trivy fs --format sarif --skip-update --offline-scan <path>` | IaC + supply-chain | | hadolint | `hadolint --format sarif <Dockerfile>` | IaC (Dockerfile) | | actionlint | binary emits JSON; thin adapter maps to SARIF `result` (see `references/tool-configs.md`) | CI/CD | | pmd | `pmd check -d . -R <resolved-ruleset> -f sarif --no-progress` — language-conditional: dispatched (and hinted when missing) only when `.java` files are in the target set; detection probes the repo-local `.pmd/` bin before PATH; ruleset resolution in `references/tool-configs.md` | Java code quality | | ruff | `ruff check --output-format sarif .` (ruff ≥ 0.3.1) | Python lint + autofix (language-conditional — see step 1) | ### Tier 2 — optional SARIF adapters Shipped in P2 Step 3b. Not part of the baseline. ### Tier 3 — bespoke JSON adapters Shipped in P2 Step 3b for tools without upstream SARIF. Each adapter ≤ 40 LOC. A thin adapter normalizes `security-review` agent output into the unified envelope (see `adapters/security-review-adapter.py`). Rule_id lookup is driven by `plugins/dev-team/knowledge/security-review-rule-map.yaml`; malformed categories hard-fail, well-formed-but-unmapped categories fall back to `security-review.*` with a WARN. mypy (≥ 1.11, `mypy --output json`) normalizes via `adapters/mypy-adapter.py` — rule ids `mypy.python.<error-code>`; on older mypy (no `--output json`) the adapter degrades to a skip-with-warning. Language-conditional like ruff (see step 1). Invocation and field mapping in `references/tool-configs.md`. ### Convention, accessibility, and performance probes (#1979) Two **additional invocations of tools already in the tiers above** — no new binaries, no new adapters, no network. Each exists to hand review agents a deterministic finding where a lens would otherwise re-derive one by inference, and each is separate from its tool's main lane so the main lane keeps reflecting the project's own gating configuration: | Probe | Invocation | Displaces | |---|---|---| | ruff convention | `ruff check --select N,PLR2004 --output-format sarif .` | the mechanical half of `naming-review` (PEP 8 naming, magic values in comparisons) | | oxlint frontend | `npx oxlint --jsx-a11y-plugin --react-plugin --react-perf-plugin -D perf --format sarif .` | the mechanical half of `a11y-review`; feeds `performance-review` on the JS/TS side (`no-await-in-loop` is the N+1 shape) | Both are language-conditional, never `[REQUIRED]`, and never gate — they are context, exactly like the rest of the pre-pass. Invocation details, the measured calibration behind them, and the JS/TS magic-number opt-in are in `references/tool-configs.md`. **Cross-language performance context.** When the Repowise MCP tools are available, the caller injects `get_health`'s static I/O-in-loop / N+1 findings into the pre-pass context alongside these, rather than having `performance-review` fetch them per dispatch. **Semgrep's registry performance rulesets (`p/performance`) are deliberately NOT used**: resolving a registry pack requires an outbound call to `semgrep.dev`, which would break the offline posture § 1b establishes for this pre-pass — and the pack the spike named does not resolve at all (`--config p/performance` returns HTTP 404, verified on semgrep 1.174.0). Shipping our own performance rules is possible but is a separate piece of work: every rule this repo ships needs passing/failing fixtures under the `semgrep rule fixtures` CI gate. The oxlint `perf` lane covers the JS/TS side offline; anything beyond it stays with the agent. **lizard** (complexity metrics) normalizes via `adapters/lizard-adapter.py` — rule ids `lizard.complexity.<metric>`; **jscpd** (copy-paste duplication) via `adapters/jscpd-adapter.py` — rule id `jscpd.duplication.clone`. Both are Tier 3 by this file's own taxonomy (bespoke adapter, not SARIF-native), but both are *baseline* capabilities rather than optional extras: they are the deterministic source for the two metric families the review panel would otherwise re-derive by inference on every run — function complexity/size/parameter count, and duplicated blocks (#1974). Neither is language-conditional; lizard covers ~15 languages and jscpd ~150, so both are probed on every target set. Invocation, thresholds, and field mapping in `references/tool-configs.md`. ### Tier 4 — legacy (pre-SARIF) ESLint / tsc remain callable via native JSON for older flows. Not part of the Step 3a baseline; migrate to SARIF when upstream lands. (The legacy Python entry is retired — ruff replaces it outright as the Tier 1 Python source.) ## Execution flow ### 1. Detect available tools For each Tier 1 tool, run `command -v <tool>`. Record presence in a tool-map. Missing Tier 1 tools surface as a **warning group** via the install-hint format below — never a pipeline failure. **Language-conditional tools** (ruff, mypy) are probed and dispatched — and their missing-tool hints surfaced — only when `.py` files are in the target set. A non-Python repo never sees a "ruff missing" or "mypy missing" warning, and language-conditional tools never carry the `[REQUIRED]` prefix. If no Tier 1 tools are present, return: ```json { "status": "skip", "tools": [], "findings": [], "summary": "No static analysis tools detected." } ``` ### 1b. Offline enforcement (gitleaks, trivy) These tools default to making network calls; in restricted-egress environments those calls hang or fail and take the pre-pass down with them. Both are pinned to local-only operation so the pre-pass never depends on outbound network access. **gitleaks** runs with `--no-verify`. Verification (confirming a detected secret is a live credential) is the only outbound call gitleaks makes; disabling it keeps detection fully pattern-based and offline. No preflight needed — the flag is always on. **trivy** runs with `--skip-update --offline-scan` on both `trivy config` and `trivy fs`. Before dispatching trivy, run the **offline DB preflight** against the local cache (`${TRIVY_CACHE_DIR:-$HOME/.cache/trivy}/db/trivy.db`): | Local DB state | Action | Warning | |---|---|---| | absent | skip trivy | `trivy local DB missing — run: trivy image --download-db-only` | | mtime age ≤ 7 days | run (fresh) | none | | mtime age > 7 days | run anyway | `trivy DB is N days old — consider refreshing with: trivy image --download-db-only` | `N` is the integer day count. The 7-day boundary is inclusive (exactly 7 days = fresh; strictly greater than 7 days = stale). A missing or stale DB is **never a hard pipeline failure** — it degrades to a skip or a warning, consistent with the graceful-degradation constraint. ### 2. Run available tools in parallel Dispatch each available tool's invocation. Each returns SARIF on stdout (or via its adapter). Collect SARIF documents keyed by tool name. **Target walk MUST include CI/CD workflow files** even when they live outside the walked tree (monorepo case — walk up to the repo root to find them). Glob list: [`knowledge/ci-cd-file-scope.md`](../../knowledge/ci-cd-file-scope.md). Invoke every Tier-1 tool that can process them — actionlint for GitHub Actions, trivy-config for any CI YAML, semgrep with `p/github-actions` (or the bundled `crypto-anti-patterns.yaml` rule that catches `printenv` in workflow `run:` blocks). Record which CI directories were scanned: ```json { "ci_dirs_scanned": [".github/workflows", ".gitlab-ci.yml"], ... } ``` If none found, record `"ci_dirs_scanned": []`. The caller can then surface "no CI files in scope" when a CI config would be expected. ### 3. Normalize to unified finding envelope The shared SARIF parser (`references/sarif-parser.md`) walks each SARIF document's `runs[*].results[*]` and emits one unified finding per result: | SARIF path | Unified finding field | Notes | |---|---|---| | `results[*].ruleId` | `rule_id` | Prefixed: `<tool>.<lang?>.<rule>` | | `results[*].locations[0].physicalLocation.artifactLocation.uri` | `file` | Repo-relative POSIX | | `results[*].locations[0].physicalLocation.region.startLine` | `line` | 1-indexed | | `results[*].locations[0].physicalLocation.region.startColumn` | `column` | 1-indexed, optional | | `results[*].level` | `severity` | `error`→`error`, `warning`→`warning`, `note`→`suggestion`, `none`/absent→`info` | | `results[*].message.text` | `message` | Truncated to 500 chars | | `runs[*].tool.driver.rules[ruleIndex].properties.cwe` | `cwe[]` | If present: `["CWE-N"]` | | `runs[*].tool.driver.name` | `metadata.source` | e.g. `"semgrep"` | | result-level `properties.confidence` | `metadata.confidence` | Default `medium` if absent | The parser MUST validate each emitted finding against unified-finding-v1 before returning; schema violations fail the run with a named tool + rule id. ### 4. Deduplicate Two findings are duplicates if they share `file`, `line`, and either (a) identical `rule_id`, or (b) `message` cosine similarity > 0.85 on normalized text. When duplicates exist, keep the higher-priority tool: ``` semgrep > gitleaks > trivy > hadolint > actionlint > ruff > mypy > oxlint > (legacy ESLint > tsc) > lizard > jscpd ``` lizard and jscpd rank last deliberately. They report *metric* classes no other tool reports (complexity numbers, clone locations), so a genuine collision is rare; when one does occur, a correctness or security finding at that line should always outrank a metric reading of it. ### 5. Consult ACCEPTED-RISKS.md If present at repo root, apply suppression per `plugins/dev-team/knowledge/accepted-risks-schema.md`. Suppressed findings are removed from the return value but logged to the audit trail. ### 6. Return structured result ```json { "status": "pass|warn|fail", "tools_available": ["semgrep", "hadolint"], "tools_missing": [ { "tool": "gitleaks", "install_hint": "gitleaks — secrets detection. install: brew install gitleaks" } ], "findings": [ /* unified finding envelope v1.0 objects */ ], "summary": "12 findings from 2 tools: 3 errors, 7 warnings, 2 suggestions" } ``` `status`: `fail` if any finding has `severity: error`; `warn` if warnings only; `pass` if no findings or no tools available. ## Install-hint format ``` <tool-name> — <capability-tier>. install: <package-manager> install <name> ``` Examples: ``` semgrep — SAST. install: pip install semgrep gitleaks — secrets detection. install: brew install gitleaks trivy — IaC + supply-chain scanning. install: brew install trivy hadolint — Dockerfile linting. install: brew install hadolint actionlint — GitHub Actions linting. install: brew install actionlint ruff — Python lint + autofix. install: python3 -m pip install ruff (project venv / dev requirements) mypy — Python type checking. install: python3 -m pip install mypy (project venv / dev requirements) lizard — complexity metrics. install: python3 -m pip install lizard (project venv / dev requirements) jscpd — copy-paste duplication detection. install: npm install -g jscpd ``` Print install-hints grouped by capability tier (secrets / IaC / CI-CD / supply-chain / SAST / data-flow). Required tools carry a `[REQUIRED]` prefix; absence of a required tool is a hard failure at install time, absence of an optional tool is a warning. Tier-implementation labels ("SARIF adapter", "bespoke-JSON adapter", "legacy") are internal vocabulary and never surface in user-facing text. ### Missing-tool remediation convention (#838) When a command hits a **missing REQUIRED tool**, the remediation must name the onboarding command first — the consistent, discoverable one-command entry point — and give the raw install hint alongside it: > Run `/project-init` (or `/setup`) to set up this repo's tooling, or install directly: `<raw install hint>`. Onboarding ownership: `/project-init` installs the static-analysis lane tools (ruff, mypy, oxlint, dotnet-format, PMD) **and** the detection-gated capability tools other skills depend on — semgrep, hadolint/trivy/grype, `adr`, `gh`, and Playwright + Chromium — per its `skills/project-init/references/capability-tools.md` registry. The pointer names `/project-init` as the one-command entry point and the tool-specific install command stays as the direct fallback. Machine output (e.g. semgrep-analyze's `--programmatic` JSON) stays prose-free: the onboarding pointer lives only in the human-facing branch. ## Agent context injection When findings are passed to review agents, format them so agents don't re-report confirmed static findings: ```text ## Static Analysis Pre-Pass Results The following issues were detected deterministically by static analysis. Do not re-report these issues. Focus on semantic and architectural concerns that static analysis cannot detect. | Tool | Severity | File | Line | Rule | Message | |------|----------|------|------|------|---------| | semgrep | error | src/api/handler.ts | 42 | javascript.express.audit.xss | Potential XSS | | gitleaks | error | .env.example | 3 | generic.aws-access-key | AWS key pattern in committed file | ``` If no findings: "Static analysis pre-pass ran (tools: <list>). No findings — all clear." This context goes to **all** review agents, not just security-review. ## Related - `references/tool-configs.md` — per-tool invocation commands, adapter scripts, install hints; hosts the "Build-time lanes" registry for `/build`'s static self-heal pass - `references/language-setup.md` — user-facing per-language setup guide for the build-time self-heal pass and this pre-pass - `references/sarif-parser.md` — normalized mapping from SARIF `result` to unified finding envelope v1.0 - `evals/static-analysis-tools/tier1-mocks/` — tier-1 mocked SARIF fixtures - `evals/static-analysis-tools/tier2-integration/` — tier-2 real-binary integration tests (nightly CI) - `knowledge/security-primitives-contract.md` — unified finding envelope v1.0 - `knowledge/accepted-risks-schema.md` — per-project suppression policy - `maintenance.md` — adapter and ruleset lifecycle policies
View on GitHub