Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/deep-code-review/references/tool-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ The agent sandbox denies installs and most network, so the owner provisions scan
| `detekt` | Not routed: Kotlin static analysis, Apache-2.0 (https://github.com/detekt/detekt LICENSE, on the OSI list at https://opensource.org/license/apache-2-0, fetched 2026-11-12). The CLI docs (https://detekt.dev/docs/gettingstarted/cli, fetched 2026-11-12) give `--input`, `--report sarif:PATH` (the XML-like format is `checkstyle`) and exits 0 clean, 1 unexpected error, 2 issues found, 3 invalid config. `Main.kt` and `CliArgs.kt` hold no HTTP, update or telemetry code, but the analysis core, the Kotlin compiler and plugin loading were not read, so its network-free behavior is `unverified` (not checked = not routed); it is also a code-quality linter whose default rule sets are not security rules | not run | not routed | not routed |
| `security-code-scan` | Not routed: a Roslyn analyzer (NuGet package or Visual Studio extension, LGPL-3.0 per the repository page, https://github.com/security-code-scan/security-code-scan, fetched 2026-11-12) that runs inside a project build; the stand-alone runner was not verified here and a build needs a NuGet restore (network). It cannot scan changed files only, so it is not routed; `devskim` is the routed .NET source scanner | not run | not routed | not routed |
| `bearer` | Not routed: Bearer CLI is under the Elastic License 2.0, which forbids offering the software to third parties as a hosted or managed service (https://github.com/Bearer/bearer, fetched 2026-11-12), and that license is not on the OSI approved-licenses list (https://opensource.org/licenses, fetched 2026-11-12), so it is not routed | not run | not routed | not routed |
| `snyk-agent-scan` | MCP and skill configs | not run | unverified: docs show no offline mode (it calls the Agent Scan API and can start MCP servers) | route-only |
| `snyk-agent-scan` | MCP and skill configs | not run | needs a `SNYK_TOKEN` and calls the Agent Scan API (no offline mode documented; it can also start MCP servers): never run by the hub (checked 2026-11-15, Apache-2.0, 0.6.11) | route-only |
| `opengrep` | SAST fallback: stands in for `semgrep` when `semgrep` is absent and `opengrep` is installed; same rules dir (repo config, else the bundled rules), never the registry pack; rows keep tool `semgrep` and the closing line says `opengrep` ran | `opengrep scan --config DIR --json --quiet -- FILES` (env `SEMGREP_SEND_METRICS=off`) | unverified (its README shows `scan` and `-f` only; `--config` and `--json` are inherited from semgrep, not confirmed); a crash is `not run`, never clean | route-only, local |
| `scorecard` | OpenSSF Scorecard for the `origin` GitHub repo; checks scoring 0-4 become rows (rule `scorecard-<Check>`, file `.`); -1 (inconclusive) is ignored; a report with no checks, or only -1 checks, is `not run` (`not-checked` row), never a clean score. Opt-in: `REVIEW_SCORECARD=1` plus `GITHUB_TOKEN`; no token is `not run` | `scorecard --repo=github.com/OWNER/REPO --format=json` (token passed as `GITHUB_AUTH_TOKEN`) | verified (`scorecard --help`, 2026-11-12) | route-only, opt-in, network |
| `nuclei` | Conservative, not passive: sends real requests to one running app. Opt-in `REVIEW_DAST_URL`, exact localhost origin only unless `REVIEW_DAST_ALLOW_REMOTE=1`. Allowlist: info and low http templates tagged `tech` or `exposure`, minus `default-login`, `xss`, `ssrf`, `oast`, `fuzz`, `fuzzing`, `takeover`, `cve`, `intrusive`, `dos`, `bruteforce`, `rce`, `sqli`. A failed `curl` probe of the target, or zero templates listed by `nuclei -tl`, is `NOT CHECKED` (an empty scan never reads as clean) | `nuclei -u URL -jsonl -silent -nc -ni -duc -rl 10 -s info,low -type http -tags tech,exposure -etags ...` | flags verified (`nuclei -h`, 2026-11-12); the tag choice is a conservative judgment, not a nuclei-certified safe set | route-only, opt-in, network |
Expand Down
17 changes: 17 additions & 0 deletions .claude/skills/deep-code-review/scripts/review_checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1574,6 +1574,23 @@ for t in data:
k = key(t) or fkey(t)
if not k or len(groups[k]) <= 3: out.append(row(t))
elif k not in done: done.add(k); out.append(summ(k, groups[k]) if key(t) else fsumm(k, groups[k]))
# Corroboration: several tools on the same file:line fold into one row (the highest severity named in the message prefix wins, the first row on a tie) that names every tool. Rows carry no category field, so the key is file:line; one tool's other rules on that line stay their own rows, and not-checked, line-0 and summary rows never fold.
SEV = {s: i for i, s in enumerate(("info", "low", "warning", "medium", "error", "high", "critical"))}
sv = lambda r: SEV.get((re.match(r"(\w+):", r["message"].lower()) or [0, ""])[1], -1)
fold, out2 = {}, []
for r in out:
k = (os.path.normpath(r["file"]), r["line"])
if r["rule"] == "not-checked" or r["rule"].endswith("-summary") or r["line"] <= 0 or r["file"] in ("", "-") or "_lines" in r: out2.append(r); continue
g = fold.get(k)
if g is None: fold[k] = g = {"best": r, "tools": [r["tool"]]}; out2.append(r)
elif r["tool"] in g["tools"]: out2.append(r)
else:
g["tools"].append(r["tool"])
if sv(r) > sv(g["best"]): out2[out2.index(g["best"])] = r; g["best"] = r
out = out2
for g in fold.values():
if len(g["tools"]) > 1:
b = g["best"]; b["tools"] = g["tools"]; t = f" [corroborated by: {', '.join(g['tools'])}]"; b["message"] += t; b["text"] += t
ML = [r.pop("_lines", None) for r in out] # per-file summary rows: every member line; popped here in every mode so the key never prints
ud = sys.argv[11] # `git diff -U0` of BASE...HEAD; "" under --full/--history or when a git read above failed (no "new" key)
if ud:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Bundled insecure-default (fail-open config) rules. Already covered elsewhere, so not repeated here: secret-env-fallback and tls-verify-off (py.yml, js.yml), cors-credentials-any-origin (js.yml).
# Added: py debug-enabled, py cors-credentials-wildcard, py/js jwt-unverified, py/js auth-fails-open. Test paths are skipped.
# ponytail: auth-fails-open only sees a try/except (catch) inside a function whose name says auth, token, permission, allowed or verify, and only a literal return True / next().
rules:
- id: perun.py.debug-enabled
languages: [python]
severity: WARNING
message: Debug mode is switched on in code (DEBUG = True or app.run(debug=True)). It exposes stack traces and an interactive console in production. Read it from the environment and default it to off.
pattern-either:
- pattern: DEBUG = True
- pattern: $APP.run(..., debug=True, ...)
- pattern: $APP.debug = True
paths:
exclude: ["test", "tests", "fixtures", "mocks", "e2e", "test_*", "*_test.py", "conftest.py"]
- id: perun.py.cors-credentials-wildcard
languages: [python]
severity: WARNING
message: CORS allows every origin and also allows credentials. Any site can then make authenticated requests as the user. List the trusted origins explicitly.
pattern-either:
- pattern: $A.add_middleware(CORSMiddleware, ..., allow_origins=["*"], ..., allow_credentials=True, ...)
- pattern: $A.add_middleware(CORSMiddleware, ..., allow_credentials=True, ..., allow_origins=["*"], ...)
- pattern: CORS($A, ..., origins="*", ..., supports_credentials=True, ...)
- pattern: CORS($A, ..., supports_credentials=True, ..., origins="*", ...)
- patterns:
- pattern: CORS_ALLOW_ALL_ORIGINS = True
- pattern-either:
- pattern-inside: |
CORS_ALLOW_CREDENTIALS = True
...
- pattern-inside: |
...
CORS_ALLOW_CREDENTIALS = True
paths:
exclude: ["test", "tests", "fixtures", "mocks", "e2e", "test_*", "*_test.py", "conftest.py"]
- id: perun.py.jwt-unverified
languages: [python]
severity: ERROR
message: JWT signature checking is off or the none algorithm is allowed, so anyone can forge a token. Verify with a fixed algorithm list and the real key.
pattern-either:
- pattern: '$J.decode(..., options={..., "verify_signature": False, ...}, ...)'
- pattern: $J.decode(..., verify=False, ...)
- pattern: $J.decode(..., algorithms=[..., "none", ...], ...)
- pattern: $J.encode(..., algorithm="none", ...)
paths:
exclude: ["test", "tests", "fixtures", "mocks", "e2e", "test_*", "*_test.py", "conftest.py"]
- id: perun.js.jwt-unverified
languages: [javascript, typescript]
severity: ERROR
message: The JWT none algorithm is allowed, so anyone can forge a token. Verify with a fixed algorithm list and the real key.
pattern-either:
- pattern: '$F(..., {..., algorithms: [..., "none", ...], ...})'
- pattern: '$F(..., {..., algorithm: "none", ...})'
paths:
exclude: ["test", "tests", "__tests__", "spec", "fixtures", "mocks", "__mocks__", "e2e", "*.test.*", "*.spec.*"]
- id: perun.py.auth-fails-open
languages: [python]
severity: WARNING
message: An auth check returns True from its exception handler, so any error (a bad token, a down key service) grants access. Deny on error.
patterns:
- pattern-inside: |
def $F(...):
...
- metavariable-regex:
metavariable: $F
regex: (?i).*(?:auth|token|permission|allowed|verify).*
- pattern: |
try:
...
except $E:
...
return True
paths:
exclude: ["test", "tests", "fixtures", "mocks", "e2e", "test_*", "*_test.py", "conftest.py"]
- id: perun.js.auth-fails-open
languages: [javascript, typescript]
severity: WARNING
message: An auth check lets the request through from its catch block (it returns true or calls next()), so any error grants access. Deny on error.
patterns:
- pattern-either:
- pattern-inside: |
function $F(...) {
...
}
- pattern-inside: |
const $F = (...) => {
...
}
- metavariable-regex:
metavariable: $F
regex: (?i).*(?:auth|token|permission|allowed|verify).*
- pattern-either:
- pattern: |
try { ... } catch ($E) { ... return true; }
- pattern: |
try { ... } catch ($E) { ... return next(); }
- pattern: |
try { ... } catch ($E) { ... next(); }
paths:
exclude: ["test", "tests", "__tests__", "spec", "fixtures", "mocks", "__mocks__", "e2e", "*.test.*", "*.spec.*"]
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,11 @@ jobs:
mkdir -p "$RUNNER_TEMP/bin"
for t in semgrep pysemgrep njsscan; do ln -s "$RUNNER_TEMP/venv-semgrep/bin/$t" "$RUNNER_TEMP/bin/$t"; done
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
- name: Run insecure-defaults semgrep fixtures (fail on SKIP, so the live check cannot silently skip)
run: |
set -euo pipefail
PATH="$RUNNER_TEMP/venv-semgrep/bin:$PATH" python3 scripts/test_semgrep_defaults.py | tee "$RUNNER_TEMP/semgrep-defaults.log"
if grep -q SKIP "$RUNNER_TEMP/semgrep-defaults.log"; then echo "semgrep fixture check skipped; failing"; exit 1; fi
- name: Install rubocop 1.69.2 (pinned, private GEM_HOME)
run: |
set -euo pipefail
Expand Down
2 changes: 2 additions & 0 deletions changelog.d/hub-corroboration-dedup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Changed
- review_checks.sh folds findings from several tools on the same file and line into one row that names every tool ("corroborated by: ..."), keeping the highest severity named in the message prefix. Different lines, one tool's own rows, not-checked rows and summary rows never fold. scripts/test-review-checks-hub-dedup.sh covers it.
2 changes: 2 additions & 0 deletions changelog.d/hub-receipt-not-installed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Added
- Regression test: a diff with .tf, .py and a Dockerfile and only bandit installed makes review_checks.sh name checkov and hadolint as not installed, with no clean verdict.
5 changes: 5 additions & 0 deletions changelog.d/hub-snyk-agent-scan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Tool hub: verify snyk-agent-scan and keep it route-only

Upstream (PyPI and GitHub, fetched 2026-11-15) shows Apache-2.0, version 0.6.11, a required SNYK_TOKEN and calls to the Agent Scan API, with no documented offline mode. The hub therefore never runs it; tool-hub.md states the verified reason and a new offline test asserts it is not invoked and emits no finding rows even with SNYK_TOKEN set.

size-budget-raise: .claude/skills/deep-code-review/references/tool-hub.md 59805→59884 snyk-agent-scan row states the verified key and network requirement
2 changes: 2 additions & 0 deletions changelog.d/semgrep-insecure-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Added
- Bundled semgrep rules for fail-open defaults (templates/semgrep-taint/defaults.yml): `perun.py.debug-enabled`, `perun.py.cors-credentials-wildcard`, `perun.py.jwt-unverified`, `perun.js.jwt-unverified`, `perun.py.auth-fails-open`, `perun.js.auth-fails-open`. Secret-env-fallback, TLS-verify-off and JS CORS-with-credentials were already covered and are not repeated. Tested by scripts/test_semgrep_defaults.py with seeded bad fixtures and clean twins.
1 change: 1 addition & 0 deletions docs/standards-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2016,3 +2016,4 @@ Verification date: **2026-11-05**. Raw-byte sha256 of each fetch and the item id
| cfn-lint — repository page and JSON formatter | https://github.com/aws-cloudformation/cfn-lint | Fetched 2026-11-13 (src/cfnlint/formatters/json.py via raw.githubusercontent.com): exit-code bitmask 0/2/4/6/8/10/12/14, `--format json`, per-match keys; MIT-0. |
| rubocop — CLI reference, formatters and Security cops | https://docs.rubocop.org/rubocop/latest/usage/cli_reference.html | Fetched 2026-11-13 (also /latest/formatters.html and /latest/cops_security.html): exit codes 0/1/2, `--only`, JSON keys and severity levels, Security cop names; `--force-default-config` was not on the page (unverified). |
| terrascan — repository page | https://github.com/tenable/terrascan | Fetched 2026-11-13: archived Nov 20, 2025, no longer maintained; first scan downloads policies; exit codes 0/1/3/4/5. |
| snyk-agent-scan — PyPI page and GitHub README | https://pypi.org/project/snyk-agent-scan/ | Fetched 2026-11-15 (also https://github.com/snyk/agent-scan): Apache-2.0; 0.6.11 (uploaded 2026-11-06); `uvx snyk-agent-scan@latest [path]`, `--json` (v0.6+ `scan_path_responses`); requires `SNYK_TOKEN`, sends component info to the Agent Scan API, no offline mode documented, can run stdio MCP servers. Needs a key: kept route-only, never run. |
2 changes: 2 additions & 0 deletions scripts/ci-tests.d/review-checks-hub-dedup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Registers the review_checks.sh hub corroboration dedup test (same file:line from several tools folds to one row).
bash scripts/test-review-checks-hub-dedup.sh
2 changes: 2 additions & 0 deletions scripts/ci-tests.d/review-checks-hub-receipt.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# review_checks.sh: receipt names uninstalled tools (checkov, hadolint) when only bandit is present.
bash scripts/test-review-checks-hub-receipt.sh
2 changes: 2 additions & 0 deletions scripts/ci-tests.d/review-checks-hub-snyk-agent-scan.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# review_checks.sh hub routing for snyk-agent-scan: route-only, never run (needs SNYK_TOKEN); stub binaries.
bash scripts/test-review-checks-hub-snyk-agent-scan.sh
2 changes: 2 additions & 0 deletions scripts/ci-tests.d/semgrep-defaults.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Bundled insecure-default (fail-open config) semgrep rules in defaults.yml: seeded bad fixtures (recall 100%) and clean twins (0 hits); live part SKIPs without semgrep.
python3 scripts/test_semgrep_defaults.py
52 changes: 52 additions & 0 deletions scripts/test-review-checks-hub-dedup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# review_checks.sh hub corroboration: semgrep, zizmor and actionlint rows on the same file:line fold into one row naming all three tools,
# keeping the highest severity (message prefix); a different line stays its own row, and one tool on two lines gives two rows. Stub tools on a
# hermetic PATH, fictional workflow content only.
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
RC=${REVIEW_CHECKS:-$ROOT/.claude/skills/deep-code-review/scripts/review_checks.sh}
T=$(mktemp -d "${TMPDIR:-/tmp}/hubdedup.XXXXXX") && [ -n "$T" ] && [ -d "$T" ] || { echo "no temp dir" >&2; exit 1; }
trap 'cd /; rm -r -- "$T"' EXIT
fail=0
export DCR_CACHE_ROOT="$T/nocache" DCR_USER_BIN= PERUN_HEAVY_DIR="$T/heavy" REVIEW_LEASE_WAIT=0 REVIEW_SEMGREP_REGISTRY=0
unset REVIEW_OSV REVIEW_OSV_DB_DIR OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY REVIEW_SEMGREP_CONFIG REVIEW_FIXTURE_ROWS REVIEW_TEST_CMD
G() { git -c user.email=t@example.com -c user.name=t "$@"; }
mkdir -p "$T/bin" "$T/zbin" "$T/sys"
for b in bash env git grep sed head tail cat rm mkdir mktemp dirname basename python3 perl timeout gtimeout jq true sort uniq paste tr cut wc awk date cp mv ls find touch tee readlink sleep cmp nice uname; do
p=$(type -P "$b") || continue; ln -s "$p" "$T/sys/$b" # only the system binaries review_checks.sh needs, never the host PATH
done
cat >"$T/bin/semgrep" <<'S'
#!/usr/bin/env bash
cat "$SG_JSON"
S
cat >"$T/bin/zizmor" <<'S'
#!/usr/bin/env bash
cat "$ZZ_JSON"; exit 13
S
cat >"$T/bin/actionlint" <<'S'
#!/usr/bin/env bash
cat "$AL_JSON"; exit 1
S
chmod +x "$T/bin/"*
WF=.github/workflows/ci.yml
mkdir -p "$T/repo" && cd "$T/repo" && git init -q . && G commit -q --allow-empty -m base && git branch base
mkdir -p .github/workflows && printf 'name: acme-ci\non: push\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: make test\n - run: make lint\n - run: make pack\n' >"$WF"
G add -A && G commit -q -m change
# sg LINE...: one semgrep row per line, message "low: ..."; zz ROW0...: zizmor rows (0-based); al LINE...: actionlint rows, message "error: ..."
sg() { local r= l; for l in "$@"; do r+="${r:+,}{\"check_id\":\"acme-rule\",\"path\":\"$WF\",\"start\":{\"line\":$l},\"extra\":{\"message\":\"low: mutable tag\"}}"; done; printf '{"results":[%s],"errors":[],"paths":{"scanned":["%s"]}}\n' "$r" "$WF"; }
zz() { local r= l; for l in "$@"; do r+="${r:+,}{\"ident\":\"unpinned-uses\",\"desc\":\"d\",\"locations\":[{\"symbolic\":{\"kind\":\"Primary\",\"key\":{\"Local\":{\"verbatim_path\":\"$WF\"}}},\"concrete\":{\"location\":{\"start_point\":{\"row\":$l}}}}]}"; done; printf '[%s]\n' "$r"; }
al() { local r= l; for l in "$@"; do r+="${r:+,}{\"message\":\"error: bad step\",\"filepath\":\"$WF\",\"line\":$l,\"kind\":\"expression\"}"; done; printf '[%s]\n' "$r"; }
go() { PATH="$T/bin:$T/sys" SG_JSON="$T/sg.json" ZZ_JSON="$T/zz.json" AL_JSON="$T/al.json" bash "$RC" --base base >"$T/out" 2>"$T/err" || true; }
at() { jq -c --argjson l "$1" '[.[] | select(.line==$l and .rule!="not-checked")]' "$T/out"; }
eq() { if [ "$1" = "$2" ]; then echo "ok - $3"; else echo "FAIL - $3 (got $1, want $2)"; fail=1; fi; }

sg 5 >"$T/sg.json"; zz 4 >"$T/zz.json"; al 5 >"$T/al.json"; go # all three on line 5
eq "$(at 5 | jq length)" 1 "3 tools, same line: one row"
eq "$(at 5 | jq -r '.[0].tools | sort | join(",")')" "actionlint,semgrep,zizmor" "the row names all 3 tools"
eq "$(at 5 | jq -r '.[0].message | test("corroborated by: ")')" true "message lists the tools"
eq "$(at 5 | jq -r '.[0].tool')" actionlint "max severity kept (error beats low)"

sg 5 9 >"$T/sg.json"; zz 99 >"$T/zz.json"; al 99 >"$T/al.json"; go # only semgrep has rows on 5 and 9
eq "$(at 5 | jq length)$(at 9 | jq length)" 11 "one tool, 2 lines: 2 rows"
eq "$(jq '[.[] | select(.tool=="semgrep")] | length' "$T/out")" 2 "no merge across lines"
exit $fail
Loading
Loading