Compare commits

...
Author SHA1 Message Date
yoni ba5cab24c2 docs(skill): plain wording for the unclear-chain case 2026-11-08 19:36:51 +00:00
yoni 0b78002686 fix(skill): walk every introducing chain before not_reached
For a transitive package the caller of the affected API is the package one
hop above it on each chain (body-parser for express > body-parser > qs),
not the direct dependency. Inspect that package on every chain; any chain
left unclear keeps the finding at imported.
2026-11-08 19:27:15 +00:00
yoni 7c490203ab feat(dependency): add the not_reached usage level
A dependency CVE can now be filed with reachability=not_reached: the
package is imported, but the usage analysis showed that the vulnerable
code of this advisory does not run in the codebase. It sits between
imported and not_imported on the evidence ladder and is decided per
advisory, with the check that showed it in reachability_evidence.

The dependency-cve-scanning skill explains when the level applies
(govulncheck finds no path, the parent of a transitive package never
references the affected API, the only call site is behind a condition
that is false here, build-only tooling) and keeps a partial or skipped
search at imported.
2026-11-06 12:20:53 +00:00
Samir Hanna Verza 09536cf148 fix(report): validate deduplication decisions 2026-11-04 21:57:34 +02:00
alex s cb581bf93d Feat/str 1646 opengrep (#1625)
* feat(sandbox): replace Semgrep with Opengrep and bundle offline rules

* fix(sandbox): keep opengrep-rules LICENSE and drop proprietary-licensed rules

* feat(sandbox): add Strix rule for GitHub-owned actions not pinned to a commit SHA

* chore: bump sandbox image to 1.4.0 (opengrep)

* fix(sandbox): resolve the gitleaks release tag without the GitHub API

* fix(sandbox): install a pinned, sha256-verified trivy release
2026-11-04 10:26:08 -05:00
23 changed files with 460 additions and 121 deletions

No files matched your search

+52 -4
View File
@@ -186,13 +186,27 @@ RUN set -eux; \
aarch64|arm64) GITLEAKS_ARCH="arm64" ;; \
*) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; \
esac; \
TAG="$(curl -fsSL https://api.github.com/repos/gitleaks/gitleaks/releases/latest | jq -r .tag_name)"; \
TAG="$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/gitleaks/gitleaks/releases/latest | sed 's#.*/##')"; \
curl -fsSL "https://github.com/gitleaks/gitleaks/releases/download/${TAG}/gitleaks_${TAG#v}_linux_${GITLEAKS_ARCH}.tar.gz" -o /tmp/gitleaks.tgz; \
tar -xzf /tmp/gitleaks.tgz -C /tmp; \
install -m 0755 /tmp/gitleaks /usr/local/bin/gitleaks; \
rm -f /tmp/gitleaks /tmp/gitleaks.tgz
RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
ARG TRIVY_VERSION=0.76.1
ARG TRIVY_SHA256_X86=c7cbe3137fb8a48a17574bede3dd6beb16742d1e9772929940fc90082c39d9c0
ARG TRIVY_SHA256_AARCH64=e174dff2a6dba5e3ae41f8d0537a6324405bd08132497340304c6b20e0f16a1f
RUN set -eux; \
case "$(uname -m)" in \
x86_64) TRIVY_ARCH="64bit"; TRIVY_SHA256="${TRIVY_SHA256_X86}" ;; \
aarch64|arm64) TRIVY_ARCH="ARM64"; TRIVY_SHA256="${TRIVY_SHA256_AARCH64}" ;; \
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; \
esac; \
curl -fsSL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-${TRIVY_ARCH}.tar.gz" -o /tmp/trivy.tgz; \
echo "${TRIVY_SHA256} /tmp/trivy.tgz" | sha256sum -c -; \
tar -xzf /tmp/trivy.tgz -C /tmp trivy; \
install -m 0755 /tmp/trivy /usr/local/bin/trivy; \
rm -f /tmp/trivy /tmp/trivy.tgz; \
trivy --version
RUN apt-get install -y wapiti
@@ -212,10 +226,44 @@ RUN set -eux; \
test "$(dpkg-query -W -f='${Version}' libbrotli1)" = "${BROTLI_DEB_VERSION}"; \
node -e 'const z=require("zlib");z.brotliDecompressSync(z.brotliCompressSync(Buffer.from("ok")))'
ARG OPENGREP_VERSION=1.31.0
ARG OPENGREP_SHA256_X86=dbbe3c2058dbe600c449a8ba22ea79a1643a6660658400090d0323a8d0d31147
ARG OPENGREP_SHA256_AARCH64=2b713a29a00237f85408b83002fc111db5b94c7fa8d263f0a93184a960655d9d
RUN set -eux; \
case "$(uname -m)" in \
x86_64) OPENGREP_ARCH="x86"; OPENGREP_SHA256="${OPENGREP_SHA256_X86}" ;; \
aarch64|arm64) OPENGREP_ARCH="aarch64"; OPENGREP_SHA256="${OPENGREP_SHA256_AARCH64}" ;; \
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; \
esac; \
curl -fsSL "https://github.com/opengrep/opengrep/releases/download/v${OPENGREP_VERSION}/opengrep_manylinux_${OPENGREP_ARCH}" -o /tmp/opengrep; \
echo "${OPENGREP_SHA256} /tmp/opengrep" | sha256sum -c -; \
install -m 0755 /tmp/opengrep /usr/local/bin/opengrep; \
rm -f /tmp/opengrep; \
opengrep --version
# Opengrep ships no rule registry of its own; bake the community ruleset so
# scans run offline. Keep only rule YAML and LICENSE; drop tests, fixtures,
# tooling, and rules marked with a proprietary license.
ARG OPENGREP_RULES_REF=f1d2b562b414783763fd02a6ed2736eaed622efa
RUN set -eux; \
mkdir -p /opt/opengrep-rules; \
curl -fsSL "https://codeload.github.com/opengrep/opengrep-rules/tar.gz/${OPENGREP_RULES_REF}" \
| tar -xz --strip-components=1 -C /opt/opengrep-rules; \
cd /opt/opengrep-rules; \
rm -rf .github scripts stats; \
find . -maxdepth 1 -type f ! -name LICENSE -delete; \
find . -mindepth 2 -type f \( ! \( -name '*.yaml' -o -name '*.yml' \) -o -name '*.test.yaml' -o -name '*.test.yml' -o -name '.*' \) -delete; \
grep -rlZ 'license: proprietary' . | xargs -0 -r rm -f; \
find . -type d -empty -delete; \
chmod -R a+rX /opt/opengrep-rules
# Strix-authored rules that close gaps in the community ruleset.
COPY containers/opengrep-rules/ /opt/opengrep-rules/strix/
RUN chmod -R a+rX /opt/opengrep-rules/strix
USER pentester
RUN pipx install semgrep && \
pipx install bandit
RUN pipx install bandit
RUN npm install -g jshint
@@ -0,0 +1,37 @@
rules:
- id: github-owned-action-not-pinned-to-commit-sha
patterns:
- pattern-inside: "{steps: ...}"
- pattern: |
uses: "$USES"
- metavariable-regex:
metavariable: $USES
regex: '^(actions|github)/'
- metavariable-pattern:
metavariable: $USES
language: generic
patterns:
- pattern-not-regex: "@[0-9a-f]{40}$"
message: >-
GitHub-owned action `$USES` is referenced by a mutable tag or branch
instead of a full-length commit SHA. Tags can be moved, so a compromised
or force-pushed release changes what this workflow runs. Pin to the
commit SHA of the release you reviewed and let a bot keep it current.
languages:
- yaml
severity: WARNING
metadata:
cwe:
- "CWE-1357: Reliance on Insufficiently Trustworthy Component"
- "CWE-353: Missing Support for Integrity Check"
owasp: A06:2021 - Vulnerable and Outdated Components
references:
- https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions
category: security
technology:
- github-actions
subcategory:
- vuln
likelihood: LOW
impact: LOW
confidence: HIGH
+1 -1
View File
@@ -186,7 +186,7 @@ When remote vars are set, Strix dual-writes telemetry to both local JSONL and th
## Docker Configuration
<ParamField path="STRIX_IMAGE" default="ghcr.io/usestrix/strix-sandbox:1.3.1" type="string">
<ParamField path="STRIX_IMAGE" default="ghcr.io/usestrix/strix-sandbox:1.4.0" type="string">
Docker image to use for the sandbox container.
</ParamField>
+1 -1
View File
@@ -49,7 +49,7 @@ Strix runs inside a Kali Linux-based Docker container with a comprehensive set o
| Tool | Description |
| ------------------------------------------------------- | --------------------------------------------- |
| [Semgrep](https://github.com/semgrep/semgrep) | Fast SAST and custom rule matching |
| [Opengrep](https://github.com/opengrep/opengrep) | Fast SAST and custom rule matching |
| [ast-grep](https://ast-grep.github.io) | Structural AST/CST-aware code search (`sg`) |
| [Tree-sitter](https://tree-sitter.github.io/tree-sitter/) | Syntax tree parsing and symbol extraction (Java/JS/TS/Python/Go/Bash/JSON/YAML grammars pre-configured) |
| [Bandit](https://bandit.readthedocs.io) | Python security linter |
+1 -1
View File
@@ -33,7 +33,7 @@ sqlmap -u "https://example.com/page?id=1"
```bash
# Fast SAST triage
semgrep --config auto ./src
opengrep scan --config /opt/opengrep-rules ./src
# Structural AST search
sg scan ./src
+1 -1
View File
@@ -46,7 +46,7 @@ Thorough penetration testing. Best for:
**Duration**: 1-4 hours depending on target complexity
**White-box behavior**: Runs broad source-aware triage (`semgrep`, AST structural search, secrets, supply-chain checks) and then systematically validates top candidates dynamically.
**White-box behavior**: Runs broad source-aware triage (`opengrep`, AST structural search, secrets, supply-chain checks) and then systematically validates top candidates dynamically.
<Note>
Deep mode is the default. It explores edge cases, chained vulnerabilities, and complex attack paths.
+1 -1
View File
@@ -4,7 +4,7 @@ set -euo pipefail
APP=strix
REPO="usestrix/strix"
STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:1.3.1"
STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:1.4.0"
MUTED='\033[0;2m'
RED='\033[0;31m'
+5 -5
View File
@@ -115,11 +115,11 @@ BLACK-BOX TESTING (domain/subdomain only):
WHITE-BOX TESTING (code provided):
- MUST perform BOTH static AND dynamic analysis
- Static: Use source-aware triage first to map risk quickly (`semgrep`, `ast-grep`, Tree-sitter tooling, `gitleaks`, `trufflehog`, `trivy fs`). Then review code for vulnerabilities
- Static: Use source-aware triage first to map risk quickly (`opengrep`, `ast-grep`, Tree-sitter tooling, `gitleaks`, `trufflehog`, `trivy fs`). Then review code for vulnerabilities
- Static coverage floor: execute at least one structural AST mapping pass (`sg` and/or Tree-sitter) per repository and keep artifact output
- Static coverage target per repository: run one `semgrep` pass, one secrets pass (`gitleaks` and/or `trufflehog`), one `trivy fs` pass, and one AST-structural pass (`sg` and/or Tree-sitter)
- Static coverage target per repository: run one `opengrep` pass, one secrets pass (`gitleaks` and/or `trufflehog`), one `trivy fs` pass, and one AST-structural pass (`sg` and/or Tree-sitter)
- Keep AST artifacts bounded and high-signal: scope to relevant paths/hypotheses, avoid whole-repo generic function dumps
- AST target selection rule: build `sg-targets.txt` from `semgrep.json` scope first (`paths.scanned`, fallback to unique `results[].path`), then run `xargs ... sg run` against that file list. Only use path-heuristic fallback if semgrep scope is unavailable.
- AST target selection rule: build `sg-targets.txt` from `opengrep.json` scope first (`paths.scanned`, fallback to unique `results[].path`), then run `xargs ... sg run` against that file list. Only use path-heuristic fallback if opengrep scope is unavailable.
- Dynamic: Run the application and test live to validate exploitability
- NEVER rely solely on static code analysis when dynamic validation is possible
- Begin with fast source triage and dynamic run preparation in parallel; use static findings to prioritize live testing.
@@ -188,7 +188,7 @@ EFFICIENCY TACTICS:
- For Caido proxy automation inside Python, explicitly import from
`caido_api`:
`from caido_api import list_requests, view_request, repeat_request, list_sitemap, view_sitemap_entry, scope_rules`
- Prefer established fuzzers/scanners where applicable: ffuf, sqlmap, nuclei, wapiti, arjun, httpx, katana, semgrep, bandit, trufflehog, nmap. Use scripts mainly to coordinate or validate around them, not to replace them without reason
- Prefer established fuzzers/scanners where applicable: ffuf, sqlmap, nuclei, wapiti, arjun, httpx, katana, opengrep, bandit, trufflehog, nmap. Use scripts mainly to coordinate or validate around them, not to replace them without reason
- For trial-heavy vectors (SQLi, XSS, XXE, SSRF, RCE, auth/JWT, deserialization), DO NOT iterate payloads manually in the browser. Always spray payloads via Python scripts through `exec_command` or terminal tools.
- When using established fuzzers/scanners, use the proxy for inspection where helpful
- Generate/adapt large payload corpora: combine encodings (URL, unicode, base64), comment styles, wrappers, time-based/differential probes. Expand with wordlists/templates
@@ -458,7 +458,7 @@ JAVASCRIPT ANALYSIS:
- js-beautify - JS beautifier/deobfuscator
CODE ANALYSIS:
- semgrep - Static analysis/SAST
- opengrep - Static analysis/SAST (bundled rules in /opt/opengrep-rules)
- ast-grep (sg) - Structural AST/CST-aware code search
- tree-sitter - Syntax-aware parsing and symbol extraction support
- bandit - Python security linter
+1 -1
View File
@@ -164,7 +164,7 @@ class RuntimeSettings(BaseSettings):
model_config = _BASE_CONFIG
image: str = Field(
default="ghcr.io/usestrix/strix-sandbox:1.3.1",
default="ghcr.io/usestrix/strix-sandbox:1.4.0",
alias="STRIX_IMAGE",
)
backend: str = Field(default="docker", alias="STRIX_RUNTIME_BACKEND")
+20 -2
View File
@@ -300,7 +300,15 @@ def _parse_dedupe_response(content: str) -> dict[str, Any]:
raise ValueError(f"No JSON object found in dedupe response: {content[:500]}")
parsed = json.loads(text[start : end + 1])
duplicate_id = str(parsed.get("duplicate_id") or "")[:64]
is_duplicate = parsed.get("is_duplicate")
if not isinstance(is_duplicate, bool):
raise TypeError("is_duplicate must be a boolean")
duplicate_id = parsed.get("duplicate_id") or ""
if is_duplicate and (not isinstance(duplicate_id, str) or not duplicate_id):
raise ValueError("A duplicate decision must name an existing report ID")
# Keep the ID verbatim: truncating or stripping it could turn an unknown ID
# into a different, existing one before the membership check below.
duplicate_id = str(duplicate_id)
reason = str(parsed.get("reason") or "")[:500]
try:
confidence = float(parsed.get("confidence", 0.0))
@@ -308,13 +316,22 @@ def _parse_dedupe_response(content: str) -> dict[str, Any]:
confidence = 0.0
return {
"is_duplicate": bool(parsed.get("is_duplicate", False)),
"is_duplicate": is_duplicate,
"duplicate_id": duplicate_id,
"confidence": confidence,
"reason": reason,
}
def _validate_duplicate_reference(
result: dict[str, Any], existing_reports: list[dict[str, Any]]
) -> None:
if result["is_duplicate"] and not any(
report.get("id") == result["duplicate_id"] for report in existing_reports
):
raise ValueError("Duplicate decision refers to an unknown report ID")
def _extract_text(response: ModelResponse) -> str:
parts: list[str] = []
for item in response.output:
@@ -397,6 +414,7 @@ async def check_duplicate(
}
result = _parse_dedupe_response(content)
_validate_duplicate_reference(result, existing_reports)
logger.info(
"Deduplication check: is_duplicate=%s, confidence=%.2f, reason=%s",
+1 -1
View File
@@ -40,7 +40,7 @@ The skills are dynamically injected into the agent's system prompt, allowing it
Notable source-aware skills:
- `source_aware_whitebox` (coordination): white-box orchestration playbook
- `source_aware_sast` (custom): semgrep/AST/secrets/supply-chain static triage workflow
- `source_aware_sast` (custom): opengrep/AST/secrets/supply-chain static triage workflow
- `dependency_cve_scanning` (custom): trivy-based SCA workflow for reporting known dependency CVEs via `create_dependency_report`
- `npx_confusion` (custom): npx/npm exec/bunx fallback and adjacent package-runner identity confusion, with runner-specific registry and reporting gates
- `semantic_confusion` (vulnerabilities): cross-boundary parser, normalization, and representation mismatch analysis
@@ -14,22 +14,22 @@ Increase white-box coverage by combining source-aware triage with dynamic valida
## Recommended Workflow
1. Build a quick source map before deep exploitation, including at least one AST-structural pass (`sg` or `tree-sitter`) scoped to relevant paths.
- For `sg` baseline, derive `sg-targets.txt` from `semgrep.json` scope first (`paths.scanned`, fallback to unique `results[].path`) and run `xargs ... sg run` on that list.
- Only fall back to path heuristics when semgrep scope is unavailable.
- For `sg` baseline, derive `sg-targets.txt` from `opengrep.json` scope first (`paths.scanned`, fallback to unique `results[].path`) and run `xargs ... sg run` on that list.
- Only fall back to path heuristics when opengrep scope is unavailable.
2. Run first-pass static triage to rank high-risk paths.
3. Use triage outputs to prioritize dynamic PoC validation.
4. Keep findings evidence-driven: no report without validation.
## Source-Aware Triage Stack
- `semgrep`: fast security-first triage and custom pattern scans
- `opengrep`: fast security-first triage and custom pattern scans
- `ast-grep` (`sg`): structural pattern hunting and targeted repo mapping
- `tree-sitter`: syntax-aware parsing support for symbol and route extraction
- `gitleaks` + `trufflehog`: complementary secret detection (working tree and history coverage)
- `trivy fs`: dependency, misconfiguration, license, and secret checks
Coverage target per repository:
- one `semgrep` pass
- one `opengrep` pass
- one AST structural pass (`sg` and/or `tree-sitter`)
- one secrets pass (`gitleaks` and/or `trufflehog`)
- one `trivy fs` pass
+43 -7
View File
@@ -136,8 +136,12 @@ fi
- A finding with a call stack ⇒ `reachability=reachable_call_path`, put the
call-path excerpt (entrypoint → vulnerable function) in
`reachability_evidence`.
- Listed as affecting a required module but with no reachable symbol ⇒ fall
back to the import/symbol checks below (`imported` / `not_imported`).
- Listed as affecting a required module but with no reachable symbol ⇒ the
call graph found no path to the vulnerable function: `not_reached` when
application code imports the module (cite the govulncheck result and the
import), `not_imported` when nothing imports it. When govulncheck could not
analyze the module (build errors, missing deps), fall back to the
import/symbol checks below instead of claiming a level.
**All other ecosystems — import check, then symbol match:**
@@ -148,15 +152,25 @@ fi
`PyYAML` → `yaml`). No hits ⇒ `not_imported`, with the search scope stated
in `reachability_evidence`. For a **transitive** dependency, the check is
whether application code imports it directly; if not, it is reachable only
through the direct dependency — check whether the direct dep's usage can
hit it (if unclear, use `imported` when the direct dep is used at all).
through the packages that depend on it — walk every introducing chain
from the lockfile (e.g. `express > body-parser > qs`) and inspect the code
of the package one hop above the vulnerable one on each chain
(`body-parser`, not just `express`; its installed sources, or the class
references of its jar) for the affected API. No chain references it ⇒
`not_reached` (below), citing every artifact you inspected; referenced ⇒
that package's call is the usage site, continue the trace upward from
there. If you could not inspect every chain, or one of them is unclear,
the package is still loaded, so the level is `imported`, never
`not_reached`.
2. **Symbol match — per CVE, not per package.** Read each CVE's own advisory
(GHSA/NVD/OSV `affected[].ecosystem_specific.imports` or the advisory
text) for the affected functions/classes/APIs. Search application code for
those symbols (`ast-grep` pattern or `rg -n`). Hits ⇒
`vulnerable_symbol_used`, with repo-relative `file:line` of each hit (up
to a handful) in `reachability_evidence`. Imported but no affected-symbol
usage found (or the advisory names no symbols) ⇒ `imported`.
to a handful) in `reachability_evidence`. Imported, search completed, and
you can say why the vulnerable code does not run here ⇒ `not_reached`
(below). Imported but the search was partial, the result is unclear, or
the advisory names no symbols ⇒ `imported`.
Different CVEs on the same package usually affect **different** symbols
(one hits a parser, another a header check) — never copy one CVE's
verdict/evidence onto its siblings; run the symbol search against each
@@ -182,6 +196,23 @@ fi
dynamic loading, unparsable sources) ⇒ `unknown` and say why in
`assumptions`.
**`not_reached` — loaded, but this CVE's vulnerable code does not run here.**
Use it when the package is imported (so `not_imported` is wrong) and you
showed that the code path the advisory describes cannot execute in this
codebase, and `reachability_evidence` says what you checked. The judgement is
per CVE: it rests on what triggers *this* flaw (an affected function, a mode,
an option, a kind of input) against what this code does. Situations that
commonly support it, as inspiration rather than a checklist: the affected API
has no caller in application code and the search was complete; the only call
site is behind a condition that is false here (a polyfill for an older
runtime, a flag that is off, a path only a dev tool runs); a transitive
package's parent never references the affected API; the package is build or
test tooling that the shipped artifact never executes; a library compiled into
the runtime whose flaw sits in a mode the application never uses (a TLS server
path in a client-only service). An inconclusive or skipped check stays
`imported`. `not_reached` is still an evidence level, not proof of safety:
dynamic dispatch, reflection and framework wiring can evade static search.
Cheap-first budgeting: the import check is one search per package — always do
it. Do the per-CVE symbol match for every CVE whose advisory names affected
symbols (they can be batched into one multi-pattern search per package);
@@ -189,13 +220,18 @@ prioritize `critical`/`high`/KEV when the budget is tight; a CVE whose symbol
search was skipped may still be reported as `imported` (the import check is
real evidence), but its `reachability_evidence` must state that the
affected-symbol check was not performed, so a skipped search is never
mistaken for a completed one with no hits. Never let this analysis stall
mistaken for a completed one with no hits, and is never filed as
`not_reached`. Never let this analysis stall
reporting — `unknown` with a reason beats an unverified claim.
Anti-overclaim rules:
- `not_imported` still does NOT mean safe (dynamic `import()`/reflection/
framework wiring evade static search) — never phrase it as "not exploitable".
- `not_reached` needs the check that shows the vulnerable code does not run,
named in `reachability_evidence`; "no call found" after a partial search is
`imported`, and one CVE's `not_reached` never carries over to a sibling CVE
on the same package.
- `reachable_call_path` is reserved for call-graph tools (govulncheck); a
symbol grep hit is `vulnerable_symbol_used`, no matter how convinced you are.
- The tool rejects any level other than `unknown` without
+14 -13
View File
@@ -1,6 +1,6 @@
---
name: source-aware-sast
description: Practical source-aware SAST and AST playbook for semgrep, ast-grep, gitleaks, and trivy fs
description: Practical source-aware SAST and AST playbook for opengrep, ast-grep, gitleaks, and trivy fs
---
# Source-Aware SAST Playbook
@@ -23,19 +23,19 @@ Run this baseline once per repository before deep narrowing:
ART=/workspace/.source-aware
mkdir -p "$ART"
semgrep scan --config p/default --config p/golang --config p/secrets \
--metrics=off --json --output "$ART/semgrep.json" .
# Build deterministic AST targets from semgrep scope (no hardcoded path guessing)
opengrep scan --config /opt/opengrep-rules --taint-intrafile \
--json --output "$ART/opengrep.json" .
# Build deterministic AST targets from opengrep scope (no hardcoded path guessing)
python3 - <<'PY'
import json
from pathlib import Path
art = Path("/workspace/.source-aware")
semgrep_json = art / "semgrep.json"
opengrep_json = art / "opengrep.json"
targets_file = art / "sg-targets.txt"
try:
data = json.loads(semgrep_json.read_text(encoding="utf-8"))
data = json.loads(opengrep_json.read_text(encoding="utf-8"))
except Exception:
targets_file.write_text("", encoding="utf-8")
raise
@@ -63,17 +63,18 @@ trivy fs --scanners vuln,misconfig --timeout 30m --offline-scan \
--format json --output "$ART/trivy-fs.json" . || true
```
## Semgrep First Pass
## Opengrep First Pass
Use Semgrep as the default static triage pass:
Use Opengrep as the default static triage pass. Rules are bundled offline in `/opt/opengrep-rules` (one directory per language, plus `generic/secrets`):
```bash
# Preferred deterministic profile set (works with --metrics=off)
semgrep scan --config p/default --config p/golang --config p/secrets \
--metrics=off --json --output /workspace/.source-aware/semgrep.json .
# Full bundled ruleset
opengrep scan --config /opt/opengrep-rules --taint-intrafile \
--json --output /workspace/.source-aware/opengrep.json .
# If you choose auto config, do not combine it with --metrics=off
semgrep scan --config auto --json --output /workspace/.source-aware/semgrep-auto.json .
# Narrow to the target stack when the full set is slow or noisy
opengrep scan --config /opt/opengrep-rules/python --config /opt/opengrep-rules/generic/secrets \
--taint-intrafile --json --output /workspace/.source-aware/opengrep-python.json .
```
If diff scope is active, restrict to changed files first, then expand only when needed.
+2 -2
View File
@@ -205,10 +205,10 @@ Jinja2 backend without autoescape: `{{7*7}}`, RCE gadgets if sandbox misconfigur
## Tooling
Static analysis is the fastest way to reach the sinks above in white-box scope. The sandbox ships `python`/`pipx`, `semgrep`, `bandit`, `ast-grep`, and `ripgrep`.
Static analysis is the fastest way to reach the sinks above in white-box scope. The sandbox ships `python`/`pipx`, `opengrep`, `bandit`, `ast-grep`, and `ripgrep`.
- **bandit** (preinstalled) — Python security linter; flags `mark_safe`, `extra()`, `RawSQL`, `subprocess`, weak crypto, hardcoded secrets: `bandit -r . -ll`
- **semgrep** (preinstalled) with the Django ruleset — higher-signal than bandit for framework-specific bugs (`.extra()`, `RawSQL`, `|safe`, `csrf_exempt`, `ALLOWED_HOSTS=['*']`): `semgrep --config p/django .`
- **opengrep** (preinstalled) with the bundled Django rules — higher-signal than bandit for framework-specific bugs (`.extra()`, `RawSQL`, `|safe`, `csrf_exempt`, `ALLOWED_HOSTS=['*']`): `opengrep scan --config /opt/opengrep-rules/python/django .`
- **pip-audit** (PyPA) — dependency CVE scanner for known-vuln Django/DRF/simplejwt versions: `pipx install pip-audit && pip-audit -r requirements.txt`
- **ast-grep** (preinstalled) — quick structural grep for risky calls without a full SAST run: `ast-grep run -p 'mark_safe($X)' -l python`
+1 -1
View File
@@ -15,7 +15,7 @@ Thorough understanding before exploitation. Test every parameter, every endpoint
**Whitebox (source available)**
- Map every file, module, and code path in the repository
- Start with broad source-aware triage (`semgrep`, `ast-grep`, `gitleaks`, `trufflehog`, `trivy fs`) and use outputs to drive deep review
- Start with broad source-aware triage (`opengrep`, `ast-grep`, `gitleaks`, `trufflehog`, `trivy fs`) and use outputs to drive deep review
- Execute at least one structural AST pass (`sg` and/or Tree-sitter) per repository and store artifacts for reuse
- Keep AST artifacts bounded and query-driven (target relevant paths/sinks first; avoid whole-repo generic function dumps)
- Use syntax-aware parsing (Tree-sitter tooling) to improve symbol, route, and sink extraction quality
+1 -1
View File
@@ -15,7 +15,7 @@ Optimize for fast feedback on critical security issues. Skip exhaustive enumerat
**Whitebox (source available)**
- Focus on recent changes: git diffs, new commits, modified files—these are most likely to contain fresh bugs
- Run a fast static triage on changed files first (`semgrep`, then targeted `sg` queries)
- Run a fast static triage on changed files first (`opengrep`, then targeted `sg` queries)
- Run at least one lightweight AST pass (`sg` or Tree-sitter) so structural mapping is not skipped
- Keep AST commands tightly scoped to changed or high-risk paths; avoid broad repository-wide pattern dumps
- Run quick secret and dependency checks (`gitleaks`, `trufflehog`, `trivy fs`) scoped to changed areas when possible
+1 -1
View File
@@ -15,7 +15,7 @@ Systematic testing across the full attack surface. Understand the application be
**Whitebox (source available)**
- Map codebase structure: modules, entry points, routing
- Run `semgrep` first-pass triage to prioritize risky flows before deep manual review
- Run `opengrep` first-pass triage to prioritize risky flows before deep manual review
- Run at least one AST-structural mapping pass (`sg` and/or Tree-sitter), then use outputs for route, sink, and trust-boundary mapping
- Keep AST output bounded to relevant paths and hypotheses; avoid whole-repo generic function dumps
- Identify architecture pattern (MVC, microservices, monolith)
+69
View File
@@ -0,0 +1,69 @@
---
name: opengrep
description: Exact Opengrep CLI structure, bundled offline rules, scoped rule selection, taint mode, and automation-safe output patterns.
---
# Opengrep CLI Playbook
Official docs:
- https://github.com/opengrep/opengrep
- https://github.com/opengrep/opengrep-rules
Canonical syntax:
`opengrep scan [flags] [targets]`
Bundled rules:
- `/opt/opengrep-rules` holds the full ruleset, one directory per language (`python`, `javascript`, `typescript`, `go`, `java`, `php`, `ruby`, …) plus `generic/secrets`, `dockerfile`, `terraform`, `yaml`, and `strix` (Strix-authored rules, e.g. GitHub-owned actions not pinned to a commit SHA)
- Framework rules sit under the language (`python/django`, `python/flask`, `javascript/express`, …)
High-signal flags:
- `--config <path>` rule file or directory; repeat for several
- `--taint-intrafile` intra-file inter-procedural taint (follows data across functions in one file)
- `--json` / `--sarif` output format
- `--output <file>` write findings to file
- `--severity <level>` filter by severity (`INFO`, `WARNING`, `ERROR`)
- `--error` return non-zero exit when findings exist
- `--quiet` suppress progress noise
- `--jobs <n>` parallel workers
- `--timeout <seconds>` per-rule, per-file timeout
- `--exclude <pattern>` / `--include <pattern>` path filters
- `--exclude-rule <rule_id>` suppress specific rule
- `--baseline-commit <sha>` only report findings introduced after baseline
Agent-safe baseline for automation:
`opengrep scan --config /opt/opengrep-rules --taint-intrafile --json --output opengrep.json --quiet --jobs 4 --timeout 20 /workspace`
Common patterns:
- Full bundled scan:
`opengrep scan --config /opt/opengrep-rules --json --output opengrep.json --quiet /workspace`
- High-severity focused pass:
`opengrep scan --config /opt/opengrep-rules --severity ERROR --json --output opengrep_high.json --quiet /workspace`
- SARIF output:
`opengrep scan --config /opt/opengrep-rules --sarif --output opengrep.sarif --quiet /workspace`
- Language- or framework-specific rules:
`opengrep scan --config /opt/opengrep-rules/python --config /opt/opengrep-rules/generic/secrets --json --output opengrep_python.json --quiet /workspace`
- Scoped directory scan:
`opengrep scan --config /opt/opengrep-rules --json --output opengrep_api.json --quiet /workspace/services/api`
- Custom rule:
`opengrep scan --config ./rules/my-rule.yaml --json --output opengrep_custom.json --quiet /workspace`
Critical correctness rules:
- Always provide an explicit `--config` from `/opt/opengrep-rules` or a local rule file. Registry shorthands (`p/...`, `auto`) fetch rules from the network; do not use them.
- Opengrep has no telemetry; there is no `--metrics` flag, and passing one fails the scan.
- Prefer `--json --output <file>` or `--sarif --output <file>` for machine-readable downstream processing. JSON keeps `paths.scanned` and `results[].path`.
- Keep the target path explicit; use an absolute or clearly scoped workspace path instead of `.` when possible.
Usage rules:
- Start with the full `/opt/opengrep-rules` set unless the task clearly calls for a narrower language or framework directory.
- Add `--taint-intrafile` for injection-class hunting (SQLi, command injection, SSRF, path traversal) in supported languages.
- Use `--quiet` in automation to reduce noisy logs.
- Use `--jobs` and `--timeout` explicitly for reproducible runtime behavior.
- `PartialParsing` errors on templates are normal; they do not invalidate the run.
Failure recovery:
- If scans are too slow, narrow the target path and the rule directories before changing engine settings.
- If scans time out, increase `--timeout` modestly or lower `--jobs`.
- If output is too broad, scope `--config`, add `--severity`, or exclude known irrelevant paths.
If uncertain, query web_search with:
`site:github.com/opengrep/opengrep <flag>`
-72
View File
@@ -1,72 +0,0 @@
---
name: semgrep
description: Exact Semgrep CLI structure, metrics-off scanning, scoped ruleset selection, and automation-safe output patterns.
---
# Semgrep CLI Playbook
Official docs:
- https://semgrep.dev/docs/cli-reference
- https://semgrep.dev/docs/getting-started/cli
- https://semgrep.dev/docs/semgrep-code/semgrep-pro-engine-intro
Canonical syntax:
`semgrep scan [flags]`
High-signal flags:
- `--config <rule_or_ruleset>` ruleset, registry pack, local rule file, or directory
- `--metrics=off` disable telemetry and metrics reporting
- `--json` JSON output
- `--sarif` SARIF output
- `--output <file>` write findings to file
- `--severity <level>` filter by severity
- `--error` return non-zero exit when findings exist
- `--quiet` suppress progress noise
- `--jobs <n>` parallel workers
- `--timeout <seconds>` per-file timeout
- `--exclude <pattern>` exclude path pattern
- `--include <pattern>` include path pattern
- `--exclude-rule <rule_id>` suppress specific rule
- `--baseline-commit <sha>` only report findings introduced after baseline
- `--pro` enable Pro engine if available
- `--oss-only` force OSS engine only
Agent-safe baseline for automation:
`semgrep scan --config p/default --metrics=off --json --output semgrep.json --quiet --jobs 4 --timeout 20 /workspace`
Common patterns:
- Default security scan:
`semgrep scan --config p/default --metrics=off --json --output semgrep.json --quiet /workspace`
- High-severity focused pass:
`semgrep scan --config p/default --severity ERROR --metrics=off --json --output semgrep_high.json --quiet /workspace`
- OWASP-oriented scan:
`semgrep scan --config p/owasp-top-ten --metrics=off --sarif --output semgrep.sarif --quiet /workspace`
- Language- or framework-specific rules:
`semgrep scan --config p/python --config p/secrets --metrics=off --json --output semgrep_python.json --quiet /workspace`
- Scoped directory scan:
`semgrep scan --config p/default --metrics=off --json --output semgrep_api.json --quiet /workspace/services/api`
- Pro engine check or run:
`semgrep scan --config p/default --pro --metrics=off --json --output semgrep_pro.json --quiet /workspace`
Critical correctness rules:
- Always include `--metrics=off`; Semgrep sends telemetry by default.
- Always provide an explicit `--config`; do not rely on vague or implied defaults.
- Prefer `--json --output <file>` or `--sarif --output <file>` for machine-readable downstream processing.
- Keep the target path explicit; use an absolute or clearly scoped workspace path instead of `.` when possible.
- If Pro availability matters, check it explicitly with a bounded command before assuming cross-file analysis exists.
Usage rules:
- Start with `p/default` unless the task clearly calls for a narrower pack.
- Add focused packs such as `p/secrets`, `p/python`, or `p/javascript` only when they match the target stack.
- Use `--quiet` in automation to reduce noisy logs.
- Use `--jobs` and `--timeout` explicitly for reproducible runtime behavior.
- Do not use `-h`/`--help` for routine operation unless absolutely necessary.
Failure recovery:
- If scans are too slow, narrow the target path and reduce the active rulesets before changing engine settings.
- If scans time out, increase `--timeout` modestly or lower `--jobs`.
- If output is too broad, scope `--config`, add `--severity`, or exclude known irrelevant paths.
- If Pro mode fails, rerun with `--oss-only` or without `--pro` and note the loss of cross-file coverage.
If uncertain, query web_search with:
`site:semgrep.dev semgrep <flag> cli`
+12 -2
View File
@@ -1693,6 +1693,7 @@ _VALID_REACHABILITY = frozenset(
{
"not_imported",
"imported",
"not_reached",
"vulnerable_symbol_used",
"reachable_call_path",
"unknown",
@@ -1840,6 +1841,9 @@ def _build_dependency_metadata(
_REACHABILITY_EVIDENCE_LABELS = {
"not_imported": "not imported by application code",
"imported": "imported by application code; affected API usage unconfirmed",
"not_reached": (
"imported by application code; the vulnerable code of this advisory is not reached"
),
"vulnerable_symbol_used": "the advisory's affected API is used in application code",
"reachable_call_path": (
"a call path from application code to the vulnerable function was proven"
@@ -2157,6 +2161,11 @@ async def create_dependency_report(
can evade static search).
- ``imported`` — application code imports the package, but usage of
the advisory's affected API was not confirmed.
- ``not_reached`` — application code imports the package, and the
analysis showed that the vulnerable code of this advisory does not
run here (the affected API has no caller, the only call site is
behind a condition that is false in this codebase, the parent
package never reaches it, ...). Per advisory, never per package.
- ``vulnerable_symbol_used`` — the advisory's affected
function/class/API appears in application code.
- ``reachable_call_path`` — a call-graph tool (e.g. ``govulncheck``)
@@ -2210,8 +2219,9 @@ async def create_dependency_report(
repository root. This binds the finding to its exact file so
remediation can target the right repository.
reachability: Usage-evidence level from static analysis — one of
``not_imported`` / ``imported`` / ``vulnerable_symbol_used`` /
``reachable_call_path`` / ``unknown``. Claim only what the
``not_imported`` / ``imported`` / ``not_reached`` /
``vulnerable_symbol_used`` / ``reachable_call_path`` /
``unknown``. Claim only what the
evidence proves; when in doubt use ``unknown``.
reachability_evidence: **Required.** The concrete proof for the
claimed level, or, for ``unknown``, what you searched and why
+159
View File
@@ -0,0 +1,159 @@
"""Only validated model decisions may suppress a finding."""
from __future__ import annotations
import json
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock
import pytest
from agents.items import ModelResponse
from agents.usage import Usage
from openai.types.responses import ResponseOutputMessage, ResponseOutputText
from strix.config import loader
from strix.report import dedupe
from strix.report.state import ReportState, get_global_report_state, set_global_report_state
from strix.tools.reporting.tool import _do_create
if TYPE_CHECKING:
from collections.abc import Iterator
from pathlib import Path
@pytest.fixture
def report_state(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[ReportState]:
monkeypatch.chdir(tmp_path)
previous = get_global_report_state()
state = ReportState(run_name="dedupe-decisions")
set_global_report_state(state)
state.add_vulnerability_report(title="Existing finding", severity="low")
try:
yield state
finally:
set_global_report_state(previous)
def _mock_judge(monkeypatch: pytest.MonkeyPatch, decision: dict[str, Any]) -> AsyncMock:
settings = loader.load_settings().model_copy(deep=True)
settings.llm.model = "openai/synthetic-model"
model = AsyncMock()
model.get_response.return_value = ModelResponse(
output=[
ResponseOutputMessage(
id="msg_synthetic",
type="message",
role="assistant",
status="completed",
content=[
ResponseOutputText(
type="output_text",
annotations=[],
text=json.dumps({"confidence": 0.95, "reason": "Synthetic", **decision}),
)
],
)
],
usage=Usage(),
response_id="synthetic",
)
monkeypatch.setattr(dedupe, "load_settings", lambda: settings)
monkeypatch.setattr(dedupe, "configure_sdk_model_defaults", lambda _: None)
monkeypatch.setattr(dedupe, "resolve_dedupe_model", lambda *_: model)
return model
async def _file_finding() -> dict[str, Any]:
return await _do_create(
title="New finding",
description="The endpoint reflects unencoded input.",
impact="The reflected script executes in a victim's browser.",
target="https://example.invalid",
technical_analysis="Input is interpolated into HTML without encoding.",
poc_description="Open the endpoint with the reflected parameter.",
poc_script_code="print('synthetic reproduction')",
remediation_steps="Encode output for its HTML context.",
evidence="The synthetic response contains the unencoded parameter.",
assumptions="A victim follows the crafted link.",
counterevidence="No encoding or content security policy is present.",
confidence="high",
severity_change_conditions="A strict content security policy would lower the impact.",
fix_effort="low",
cvss_breakdown={
"attack_vector": "N",
"attack_complexity": "L",
"privileges_required": "N",
"user_interaction": "R",
"scope": "U",
"confidentiality": "L",
"integrity": "L",
"availability": "N",
},
endpoint="/search",
method="GET",
cve=None,
cwe="CWE-79",
code_locations=None,
)
@pytest.mark.parametrize("value", ["false", "true", 0, 1, None, [], {}])
def test_dedupe_parser_requires_a_boolean(value: Any) -> None:
with pytest.raises(TypeError, match="is_duplicate"):
dedupe._parse_dedupe_response(json.dumps({"is_duplicate": value}))
@pytest.mark.parametrize(
"decision",
[
{"is_duplicate": "false", "duplicate_id": "vuln-0001"},
{"is_duplicate": "true", "duplicate_id": "vuln-0001"},
{"is_duplicate": 1, "duplicate_id": "vuln-0001"},
{"is_duplicate": True, "duplicate_id": "vuln-9999"},
{"is_duplicate": True, "duplicate_id": ""},
{"is_duplicate": True, "duplicate_id": None},
{"is_duplicate": True, "duplicate_id": ["vuln-0001"]},
{"is_duplicate": True},
],
)
async def test_invalid_dedupe_decision_does_not_suppress_finding(
report_state: ReportState, monkeypatch: pytest.MonkeyPatch, decision: dict[str, Any]
) -> None:
model = _mock_judge(monkeypatch, decision)
result = await _file_finding()
assert result["success"] is True
model.get_response.assert_awaited_once()
saved = json.loads((report_state.get_run_dir() / "vulnerabilities.json").read_text())
assert [report["title"] for report in saved] == ["Existing finding", "New finding"]
assert (report_state.get_run_dir() / "vulnerabilities" / "vuln-0002.md").is_file()
@pytest.mark.parametrize("is_duplicate", [False, True])
async def test_valid_dedupe_decisions_keep_their_meaning(
report_state: ReportState, monkeypatch: pytest.MonkeyPatch, is_duplicate: bool
) -> None:
# An extraneous ID must not turn a valid false decision into a duplicate.
_mock_judge(monkeypatch, {"is_duplicate": is_duplicate, "duplicate_id": "vuln-0001"})
result = await _file_finding()
assert result["success"] is not is_duplicate
saved = json.loads((report_state.get_run_dir() / "vulnerabilities.json").read_text())
assert len(saved) == (1 if is_duplicate else 2)
if is_duplicate:
assert result["duplicate_of"] == "vuln-0001"
async def test_dedupe_id_is_matched_without_truncation(monkeypatch: pytest.MonkeyPatch) -> None:
existing_id = "x" * 64
_mock_judge(monkeypatch, {"is_duplicate": True, "duplicate_id": existing_id + "-unknown"})
result = await dedupe.check_duplicate(
{"title": "New finding"}, [{"id": existing_id, "title": "Existing finding"}]
)
assert result["is_duplicate"] is False
assert "error" in result
+33
View File
@@ -668,6 +668,39 @@ async def test_dependency_report_records_reachability(report_state: ReportState)
assert report["severity"] == "high"
async def test_dependency_report_records_not_reached(report_state: ReportState) -> None:
result = await _do_create_dependency(
title="CVE-2021-23337 in lodash 4.17.20",
description="Command injection via template.",
target="repo/package.json",
cve="CVE-2021-23337",
package_name="lodash",
installed_version="4.17.20",
impact="Command injection where template is used.",
remediation_steps="Upgrade to 4.17.21.",
assumptions="Static search only; no dynamic dispatch into lodash was found.",
package_ecosystem="npm",
manifest_path="package-lock.json",
fixed_version="4.17.21",
cwe=None,
advisory_cvss=7.2,
technical_analysis=None,
fix_effort="low",
reachability="not_reached",
reachability_evidence=(
"src/util/strings.ts:3 imports lodash; a complete search of src/ finds no "
"call to _.template, the only API the advisory affects."
),
contextual_cvss_breakdown=_DEP_CONTEXT,
contextual_cvss_reasoning=_DEP_REASONING,
)
assert result["success"] is True
report = report_state.vulnerability_reports[0]
assert report["dependency_metadata"]["reachability"] == "not_reached"
assert "the vulnerable code of this advisory is not reached" in report["evidence"]
async def test_dependency_report_rejects_reachability_without_evidence(
report_state: ReportState,
) -> None: