crewforth 3.4.0 → 3.5.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +273 -0
- package/README.md +3 -2
- package/VERSION +1 -1
- package/adopt.sh +40 -5
- package/bin/cli.js +138 -2
- package/kit/AGENT_TEMPLATE.md +15 -0
- package/kit/CLAUDE.md +50 -18
- package/kit/agents/crew-backend-expert.md +4 -3
- package/kit/agents/crew-commit-agent.md +2 -0
- package/kit/agents/crew-database-expert.md +2 -1
- package/kit/agents/crew-devops-expert.md +1 -0
- package/kit/agents/crew-docs-agent.md +3 -1
- package/kit/agents/crew-frontend-expert.md +2 -0
- package/kit/agents/crew-performance-expert.md +7 -2
- package/kit/agents/crew-planner.md +15 -3
- package/kit/agents/crew-privacy-agent.md +2 -0
- package/kit/agents/crew-review-agent.md +8 -1
- package/kit/agents/crew-security-expert.md +4 -3
- package/kit/agents/crew-test-expert.md +6 -2
- package/kit/eval/doctor.sh +201 -1
- package/kit/eval/gate-report.sh +28 -0
- package/kit/eval/golden-holdout.txt +2 -0
- package/kit/eval/golden-routing.txt +17 -0
- package/kit/eval/lib/crew-map/contracts.mjs +77 -0
- package/kit/eval/lib/crew-map/crew-map.sh +2 -1
- package/kit/eval/lib/crew-map/detect.mjs +2 -1
- package/kit/eval/lib/crew-map/narrative.mjs +14 -3
- package/kit/eval/lib/crew-map/project.mjs +45 -6
- package/kit/eval/lib/crew-map/scan.mjs +153 -14
- package/kit/eval/lib/findings/findings.schema.json +11 -1
- package/kit/eval/lib/findings/findings.sh +24 -1
- package/kit/eval/lib/hook-inject.sh +159 -0
- package/kit/eval/lib/lockfiles.sh +71 -0
- package/kit/eval/lib/payload-check.sh +29 -0
- package/kit/eval/lib/preview.sh +3 -3
- package/kit/eval/lib/review-bench/bench.mjs +14 -2
- package/kit/eval/lib/review-prepass/prepass.mjs +32 -3
- package/kit/eval/lib/settings-json.awk +18 -0
- package/kit/eval/lib/statusline.sh +22 -5
- package/kit/eval/lib/todo.awk +167 -0
- package/kit/eval/lib/todo.sh +120 -0
- package/kit/eval/lib/untrusted-text.awk +38 -0
- package/kit/eval/scan-skill.sh +67 -20
- package/kit/eval/smoke-test.sh +1759 -41
- package/kit/eval/utilization.sh +77 -3
- package/kit/hooks/agent-outcome.sh +9 -1
- package/kit/hooks/board-sync.sh +49 -3
- package/kit/hooks/board.sh +8 -8
- package/kit/hooks/branch-fresh.sh +101 -0
- package/kit/hooks/floor-blocklist.txt +19 -1
- package/kit/hooks/guard-bash.sh +92 -8
- package/kit/hooks/guard-question.sh +2 -1
- package/kit/hooks/loop-guard.sh +445 -0
- package/kit/hooks/map-stale.sh +437 -0
- package/kit/hooks/pre-commit +48 -7
- package/kit/hooks/precompact-snapshot.sh +102 -6
- package/kit/hooks/prompt-approval.sh +50 -14
- package/kit/hooks/route-hint.sh +36 -7
- package/kit/hooks/secret-blocklist.txt +55 -1
- package/kit/hooks/session-guard.sh +45 -15
- package/kit/hooks/session-rehydrate.sh +34 -11
- package/kit/hooks/session-stats.sh +169 -8
- package/kit/hooks/session-update-check.sh +5 -1
- package/kit/hooks/skill-trust.sh +23 -14
- package/kit/hooks/turn-check.sh +190 -0
- package/kit/owned-blobs.tsv +4 -4
- package/kit/settings.json +64 -0
- package/kit/skills/a11y/SKILL.md +50 -3
- package/kit/skills/a11y/references/recipes.md +41 -0
- package/kit/skills/adr/SKILL.md +14 -2
- package/kit/skills/api-design/SKILL.md +6 -1
- package/kit/skills/api-design/references/idempotency.md +39 -0
- package/kit/skills/brainstorm/SKILL.md +4 -2
- package/kit/skills/brainstorm/references/asking.md +47 -0
- package/kit/skills/ci-pipeline/SKILL.md +11 -1
- package/kit/skills/ci-pipeline/references/agent-job.md +33 -0
- package/kit/skills/commit-message/SKILL.md +9 -3
- package/kit/skills/confidence-check/SKILL.md +9 -7
- package/kit/skills/confidence-check/references/source-check.md +37 -0
- package/kit/skills/crew-approve/SKILL.md +3 -0
- package/kit/skills/crew-audit/SKILL.md +63 -0
- package/kit/skills/crew-board/SKILL.md +2 -2
- package/kit/skills/crew-code-review/SKILL.md +27 -4
- package/kit/skills/crew-code-review/references/panel-mode.md +9 -9
- package/kit/skills/crew-code-review/references/simplify.md +28 -0
- package/kit/skills/crew-doctor/SKILL.md +3 -2
- package/kit/skills/crew-gates/SKILL.md +3 -0
- package/kit/skills/crew-gates/references/enforcers.md +30 -0
- package/kit/skills/crew-interview/SKILL.md +42 -0
- package/kit/skills/crew-map/SKILL.md +26 -6
- package/kit/skills/crew-plan/SKILL.md +10 -2
- package/kit/skills/crew-pr/SKILL.md +57 -0
- package/kit/skills/crew-pr/references/checks.md +35 -0
- package/kit/skills/crew-pr/references/threads.md +23 -0
- package/kit/skills/crew-review/SKILL.md +34 -1
- package/kit/skills/crew-ship/SKILL.md +5 -2
- package/kit/skills/crew-skill/SKILL.md +26 -1
- package/kit/skills/crew-todo/SKILL.md +31 -0
- package/kit/skills/crew-update/SKILL.md +4 -1
- package/kit/skills/db-migration/SKILL.md +15 -0
- package/kit/skills/db-migration/references/expand-contract.md +52 -0
- package/kit/skills/dependency-audit/SKILL.md +34 -1
- package/kit/skills/dependency-audit/scripts/record.sh +37 -0
- package/kit/skills/dependency-upgrade/SKILL.md +29 -1
- package/kit/skills/deploy/SKILL.md +18 -0
- package/kit/skills/deploy/references/topology-a.md +7 -0
- package/kit/skills/deploy/references/topology-b.md +2 -2
- package/kit/skills/docs-writer/SKILL.md +37 -2
- package/kit/skills/eval-grader/SKILL.md +9 -0
- package/kit/skills/eval-grader/references/method.md +26 -0
- package/kit/skills/frontend/SKILL.md +5 -2
- package/kit/skills/frontend/references/from-design.md +29 -0
- package/kit/skills/frontend/references/page-metadata.md +17 -0
- package/kit/skills/frontend/references/verify-contract.md +18 -1
- package/kit/skills/frontend/references/worst-data.md +35 -0
- package/kit/skills/frontend-design/SKILL.md +20 -4
- package/kit/skills/frontend-design/references/design-review.md +21 -0
- package/kit/skills/frontend-design/references/detection.md +44 -0
- package/kit/skills/frontend-design/references/motion.md +54 -0
- package/kit/skills/frontend-design/references/ui-copy.md +35 -0
- package/kit/skills/frontend-rn-expo/SKILL.md +3 -0
- package/kit/skills/handoff/SKILL.md +28 -3
- package/kit/skills/handoff/references/cold-reader.md +13 -6
- package/kit/skills/i18n-integrity/SKILL.md +3 -0
- package/kit/skills/iterate/SKILL.md +7 -2
- package/kit/skills/mcp-builder/SKILL.md +21 -4
- package/kit/skills/mcp-builder/references/building.md +74 -13
- package/kit/skills/mcp-builder/references/cli-for-agents.md +26 -0
- package/kit/skills/observability/SKILL.md +13 -1
- package/kit/skills/performance/SKILL.md +25 -1
- package/kit/skills/performance/references/bottlenecks.md +11 -0
- package/kit/skills/performance/references/web-vitals.md +31 -0
- package/kit/skills/red-team/SKILL.md +25 -3
- package/kit/skills/reflect/SKILL.md +7 -2
- package/kit/skills/release/SKILL.md +13 -0
- package/kit/skills/security-scan/SKILL.md +6 -2
- package/kit/skills/security-scan/references/ai-agents.md +16 -0
- package/kit/skills/security-scan/references/fronts.md +59 -2
- package/kit/skills/security-scan/references/reporting.md +13 -0
- package/kit/skills/security-scan/references/verify.md +9 -0
- package/kit/skills/sonarqube-check/SKILL.md +32 -2
- package/kit/skills/sonarqube-check/references/ratchet.md +32 -0
- package/kit/skills/spec-planning/SKILL.md +32 -8
- package/kit/skills/systematic-debugging/SKILL.md +15 -7
- package/kit/skills/systematic-debugging/references/techniques.md +5 -0
- package/kit/skills/teamboard/SKILL.md +3 -3
- package/kit/skills/testing/SKILL.md +18 -3
- package/kit/skills/testing/references/browser-headless.md +21 -0
- package/kit/skills/token-budget/SKILL.md +26 -5
- package/kit/skills/worktree/SKILL.md +13 -5
- package/kit/studio/server/lib/session.js +23 -2
- package/kit/studio/server/lib/worktree-changes.js +64 -0
- package/kit/studio/web/agent-spend.js +32 -0
- package/kit/studio/web/app.js +245 -236
- package/kit/studio/web/approvals.js +14 -13
- package/kit/studio/web/ask.js +10 -9
- package/kit/studio/web/canvas.js +56 -54
- package/kit/studio/web/chat.js +41 -41
- package/kit/studio/web/convo.js +33 -16
- package/kit/studio/web/dock.js +20 -19
- package/kit/studio/web/graph-plan.js +62 -22
- package/kit/studio/web/i18n-tr.js +760 -0
- package/kit/studio/web/i18n.js +79 -0
- package/kit/studio/web/index.html +1 -0
- package/kit/studio/web/inspect.js +27 -24
- package/kit/studio/web/list-plan.js +16 -15
- package/kit/studio/web/list.js +6 -5
- package/kit/studio/web/liveness.js +11 -9
- package/kit/studio/web/map-plan.js +86 -36
- package/kit/studio/web/map.js +85 -72
- package/kit/studio/web/models-plan.js +47 -42
- package/kit/studio/web/models.js +21 -20
- package/kit/studio/web/names.js +13 -10
- package/kit/studio/web/nav.js +34 -33
- package/kit/studio/web/newsession.js +25 -24
- package/kit/studio/web/style.css +2 -0
- package/kit/studio/web/theme.js +5 -2
- package/kit/studio/web/timeline-plan.js +19 -18
- package/kit/studio/web/timeline.js +23 -22
- package/kit/studio/web/usage.js +36 -32
- package/package.json +5 -4
- package/start.sh +11 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# untrusted-text.awk — make text other people wrote safe to put into a model's context, line by line.
|
|
2
|
+
#
|
|
3
|
+
# LC_ALL=C awk -f untrusted-text.awk < text
|
|
4
|
+
#
|
|
5
|
+
# Text a hook injects (a teammate's board title, a decision's author) reaches the model with the authority of
|
|
6
|
+
# the hook that carried it. This does not decide what the text means; it removes what can hide or disguise it:
|
|
7
|
+
# - C0 controls other than tab, DEL, and the C1 controls (U+0080-U+009F), which a terminal or a renderer acts on;
|
|
8
|
+
# - zero-width and direction marks: U+200B-U+200F, U+202A-U+202E, U+2066-U+2069 (a bidi override shows a line
|
|
9
|
+
# in an order other than the order the model reads it in);
|
|
10
|
+
# - `<` and `>`, so no line can open or close a tag-shaped block, which become ‹ and ›;
|
|
11
|
+
# - a line that starts with `#`, three backticks or `---`, which could pass for a heading, a fence or a frontmatter
|
|
12
|
+
# break, gets a leading `| `.
|
|
13
|
+
# Run under LC_ALL=C: every pattern is a byte sequence of UTF-8, so the result does not depend on the locale.
|
|
14
|
+
BEGIN {
|
|
15
|
+
bad = ""
|
|
16
|
+
# U+200B-U+200F: E2 80 8B..8F · U+202A-U+202E: E2 80 AA..AE · U+2066-U+2069: E2 81 A6..A9
|
|
17
|
+
for (b = 139; b <= 143; b++) bad = bad (bad == "" ? "" : "|") sprintf("%c%c%c", 226, 128, b)
|
|
18
|
+
for (b = 170; b <= 174; b++) bad = bad "|" sprintf("%c%c%c", 226, 128, b)
|
|
19
|
+
for (b = 166; b <= 169; b++) bad = bad "|" sprintf("%c%c%c", 226, 129, b)
|
|
20
|
+
# C1 controls as UTF-8: C2 80..9F
|
|
21
|
+
for (b = 128; b <= 159; b++) bad = bad "|" sprintf("%c%c", 194, b)
|
|
22
|
+
ctl = ""
|
|
23
|
+
for (b = 1; b <= 31; b++) if (b != 9) ctl = ctl (ctl == "" ? "" : "|") sprintf("%c", b)
|
|
24
|
+
ctl = ctl "|" sprintf("%c", 127)
|
|
25
|
+
lt = sprintf("%c%c%c", 226, 128, 185) # ‹
|
|
26
|
+
gt = sprintf("%c%c%c", 226, 128, 186) # ›
|
|
27
|
+
}
|
|
28
|
+
{
|
|
29
|
+
sub(/\r$/, "")
|
|
30
|
+
# Control bytes first, then the marks until none is left: removing one can join the bytes around it into
|
|
31
|
+
# another (E2 01 80 AE, or E2 · E2 80 8B · 80 AE, both become U+202E after one pass).
|
|
32
|
+
gsub(ctl, "")
|
|
33
|
+
while (gsub(bad, "") > 0) ;
|
|
34
|
+
gsub(/</, lt); gsub(/>/, gt)
|
|
35
|
+
t = $0; sub(/^[ \t]+/, "", t)
|
|
36
|
+
if (t ~ /^#/ || t ~ /^```/ || t ~ /^---/) $0 = "| " $0
|
|
37
|
+
print
|
|
38
|
+
}
|
package/kit/eval/scan-skill.sh
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
# are different answers, and only one of them belongs on a security surface.
|
|
16
16
|
# Usage: bash scan-skill.sh [path] (default: .claude)
|
|
17
17
|
set -uo pipefail
|
|
18
|
+
# The C locale, so the byte patterns below are matched as bytes and `-i` folds ASCII only (as in every other gate).
|
|
19
|
+
export LC_ALL=C
|
|
18
20
|
TARGET="${1:-.claude}"
|
|
19
21
|
|
|
20
22
|
# --- pattern groups (ERE) ------------------------------------------------------------------------------------
|
|
@@ -31,11 +33,51 @@ CRIT='(curl|wget|fetch)[^|]*\|[[:space:]]*(sudo[[:space:]]+)?(bash|sh|zsh|python
|
|
|
31
33
|
# an ordinary outbound URL stays LOW: measured against Crewforth's own payload, this flags 0 of 59 files, and the
|
|
32
34
|
# two places Crewforth itself names WebFetch (a `tools:` line and a `Requires-tool` marker) are untouched.
|
|
33
35
|
HIGH='ignore[[:space:]]+(all[[:space:]]+)?(the[[:space:]]+)?(previous|prior|above)[[:space:]]+(instruction|prompt)|disregard[[:space:]]+(the[[:space:]]+|all[[:space:]]+)?(previous|above|prior)|ignore[[:space:]]+your[[:space:]]+(system[[:space:]]+)?(prompt|instruction)|(cat|less|more|tail|head|base64|xxd|od|strings|curl|wget|scp|cp|rsync)[^|]*(~/\.ssh|id_rsa|/etc/(passwd|shadow)|\.aws/credentials|\.netrc|\.git-credentials)|(~/\.ssh|id_rsa|/etc/(passwd|shadow)|\.aws/credentials|\.netrc|\.git-credentials)[^|]{0,80}(curl|wget|scp|nc |netcat|base64|exfiltrat)|base64[[:space:]]+-[A-Za-z]*d[^|]*\||(curl|wget)[[:space:]][^|]*https?://[^[:space:]]*\.(md|txt|ya?ml|json)|(WebFetch|web_fetch)[^|]{0,120}(instruction|prompt|then follow|steps to follow)'
|
|
36
|
+
# A HIDDEN INSTRUCTION in an HTML comment: it renders as nothing in a Markdown preview and in a PR's rich diff, and the
|
|
37
|
+
# model reads it like any other line. Only a comment that OPENS on an imperative is flagged, so Crewforth's own
|
|
38
|
+
# machine-readable markers (`<!-- routing-eval …`, `<!-- Requires-tool …`) and explanatory comments stay clean.
|
|
39
|
+
HIGH="$HIGH"'|<!--[[:space:]]*(ignore|disregard|forget|override|run|execute|send|post|upload|fetch|download|install|delete|remove|exfiltrate|you[[:space:]]+(must|should|are|will)|do[[:space:]]+not|always|never)([^a-z]|$)'
|
|
40
|
+
# INVISIBLE TEXT, as UTF-8 bytes (one more grep, in the C locale): Unicode tag characters (U+E0000-E007F, which spell
|
|
41
|
+
# ASCII nobody sees), bidi overrides and isolates (U+202A-202E, U+2066-2069, which reorder what an editor shows), and a
|
|
42
|
+
# zero-width character (U+200B-200D, U+2060, U+FEFF) BETWEEN two letters, where it splits a word a pattern would catch
|
|
43
|
+
# (`ign<ZWSP>ore previous instructions`). A BOM at the start of a file is not between letters and is not flagged.
|
|
44
|
+
# Reported as a count only: printing the decoded text would put the hidden instruction in front of the reader.
|
|
45
|
+
_ZW='('$'\xe2\x80\x8b''|'$'\xe2\x80\x8c''|'$'\xe2\x80\x8d''|'$'\xe2\x81\xa0''|'$'\xef\xbb\xbf'')'
|
|
46
|
+
INVIS=$'\xf3\xa0[\x80\x81]|\xe2\x80[\xaa\xab\xac\xad\xae]|\xe2\x81[\xa6\xa7\xa8\xa9]|[A-Za-z]'"$_ZW"'+[A-Za-z]'
|
|
34
47
|
# MED: named cloud/CI secret env vars, process.env secret access, chmod 777, code eval/exec.
|
|
35
48
|
MED='(GITHUB_TOKEN|AWS_SECRET|AWS_ACCESS_KEY|NPM_TOKEN|OPENAI_API_KEY|ANTHROPIC_API_KEY|SLACK_TOKEN)|process\.env\.[A-Za-z_]*(TOKEN|SECRET|KEY|PASSWORD)|chmod[[:space:]]+(-R[[:space:]]+)?0?777|[^a-zA-Z](eval|exec)[[:space:]]*\('
|
|
36
49
|
# LOW: an outbound URL fetch or a raw socket — common in benign deploy/example prose, hence low weight.
|
|
37
50
|
LOW='(^|[^a-zA-Z])(curl|wget)[[:space:]]+[^|]*https?://|/dev/tcp/|[^a-zA-Z]nc[[:space:]]+-[A-Za-z]*e'
|
|
38
51
|
|
|
52
|
+
# FRONTMATTER: what a component asks the HOST for, read from its YAML header — one awk over all files. A `hooks:` or
|
|
53
|
+
# `mcpServers:` key (it brings its own code to run), a `permissionMode:` other than default/plan/manual, and an
|
|
54
|
+
# `allowed-tools:` entry that is a bare `Bash` or a `Bash(…)` rule with `*` in it (any command, or any arguments).
|
|
55
|
+
# The one `*` rule not counted is a call of the component's OWN script: `Bash(bash ${CLAUDE_SKILL_DIR}/…)` reaches
|
|
56
|
+
# only files that ship inside the skill, and the scan already reads those. Anywhere in the file, a `!`cmd`` line
|
|
57
|
+
# counts too: it runs when the component loads, before anyone reads it.
|
|
58
|
+
FM_AWK='
|
|
59
|
+
function tools(v, n, m, inner) {
|
|
60
|
+
n = 0; sub(/^[[:space:]]*-[[:space:]]*/, "", v)
|
|
61
|
+
if (v ~ /(^|[^A-Za-z])Bash([^A-Za-z(]|$)/) n++
|
|
62
|
+
if (v ~ /(^|[,[:space:]])\*([,[:space:]]|$)/) n++
|
|
63
|
+
while (match(v, /Bash\([^)]*\)/)) {
|
|
64
|
+
inner = substr(v, RSTART + 5, RLENGTH - 6); v = substr(v, RSTART + RLENGTH)
|
|
65
|
+
if (inner ~ /\*/ && inner !~ /^((bash|sh)[[:space:]]+)?\$\{CLAUDE_SKILL_DIR\}\//) n++
|
|
66
|
+
}
|
|
67
|
+
return n }
|
|
68
|
+
FNR == 1 { if (NR > 1) print f ":" c; f = FILENAME; c = 0; key = ""; sub(/^\357\273\277/, ""); fm = ($0 ~ /^---[[:space:]]*$/); if (fm) next }
|
|
69
|
+
fm && /^---[[:space:]]*$/ { fm = 0; next }
|
|
70
|
+
fm {
|
|
71
|
+
if ($0 ~ /^[A-Za-z]/) { key = $0; sub(/:.*/, "", key) }
|
|
72
|
+
if ($0 ~ /^(hooks|mcpServers):/) c++
|
|
73
|
+
if ($0 ~ /^permissionMode:/) { v = $0; sub(/^permissionMode:/, "", v); gsub(/["\047[:space:]]/, "", v)
|
|
74
|
+
if (v != "" && v != "default" && v != "plan" && v != "manual") c++ }
|
|
75
|
+
if (key == "allowed-tools") { v = $0; sub(/^allowed-tools:/, "", v); c += tools(v) }
|
|
76
|
+
}
|
|
77
|
+
/(^|[[:space:]])!`[^`]+`/ { c++ }
|
|
78
|
+
END { if (NR > 0) print f ":" c }'
|
|
79
|
+
fm_counts(){ awk "$FM_AWK" "$@" 2>/dev/null; return 0; } # `path:count` per non-empty file, in the order given
|
|
80
|
+
|
|
39
81
|
FAIL=0; N=0
|
|
40
82
|
# The score ranks; the SEVERITY decides. A single HIGH costs 10 points and lands on exactly 90 — the SAFE line —
|
|
41
83
|
# so one credential-exfil line or one prompt-injection directive used to pass as safe on arithmetic alone. That
|
|
@@ -81,36 +123,38 @@ emit(){ # prints one line per file, in FILES order, from the four count a
|
|
|
81
123
|
while [ "$i" -lt "${#FILES[@]}" ]; do
|
|
82
124
|
f="${FILES[$i]}"
|
|
83
125
|
c="${CNT_C[$i]:-0}"; h="${CNT_H[$i]:-0}"; m="${CNT_M[$i]:-0}"; l="${CNT_L[$i]:-0}"
|
|
126
|
+
report "$f" "$c" "$h" "${CNT_I[$i]:-0}" "${CNT_F[$i]:-0}" "$m" "$l"
|
|
84
127
|
i=$((i+1))
|
|
85
|
-
score=$((100 - c*20 - h*10 - m*3 - l*1)); [ "$score" -lt 0 ] && score=0
|
|
86
|
-
v="$(verdict "$score" "$c" "$h")"
|
|
87
|
-
N=$((N+1))
|
|
88
|
-
case "$v" in
|
|
89
|
-
SAFE) printf ' ✅ %-3s %s\n' "$score" "$f" ;;
|
|
90
|
-
REVIEW) printf ' ⚠️ %-3s %s (crit:%s high:%s med:%s low:%s) — REVIEW\n' "$score" "$f" "$c" "$h" "$m" "$l"; FAIL=1 ;;
|
|
91
|
-
DANGER) printf ' ❌ %-3s %s (crit:%s high:%s med:%s low:%s) — DANGER\n' "$score" "$f" "$c" "$h" "$m" "$l"; FAIL=1 ;;
|
|
92
|
-
esac
|
|
93
128
|
done
|
|
94
129
|
}
|
|
95
130
|
|
|
131
|
+
# One file's line. Invisible text and frontmatter requests score as HIGH and are named beside the counts: the reader
|
|
132
|
+
# cannot see the first in an editor, and should not have to guess which header line the second is.
|
|
133
|
+
report(){ # $1 file, $2 crit, $3 high, $4 invisible, $5 frontmatter, $6 med, $7 low
|
|
134
|
+
local f="$1" c="$2" h=$(( $3 + $4 + $5 )) m="$6" l="$7" score v x=""
|
|
135
|
+
[ "$4" -gt 0 ] && x="$x · invisible characters: $4"
|
|
136
|
+
[ "$5" -gt 0 ] && x="$x · frontmatter requests: $5"
|
|
137
|
+
score=$((100 - c*20 - h*10 - m*3 - l*1)); [ "$score" -lt 0 ] && score=0
|
|
138
|
+
v="$(verdict "$score" "$c" "$h")"
|
|
139
|
+
N=$((N+1))
|
|
140
|
+
case "$v" in
|
|
141
|
+
SAFE) printf ' ✅ %-3s %s\n' "$score" "$f" ;;
|
|
142
|
+
REVIEW) printf ' ⚠️ %-3s %s (crit:%s high:%s med:%s low:%s%s) — REVIEW\n' "$score" "$f" "$c" "$h" "$m" "$l" "$x"; FAIL=1 ;;
|
|
143
|
+
DANGER) printf ' ❌ %-3s %s (crit:%s high:%s med:%s low:%s%s) — DANGER\n' "$score" "$f" "$c" "$h" "$m" "$l" "$x"; FAIL=1 ;;
|
|
144
|
+
esac
|
|
145
|
+
}
|
|
146
|
+
|
|
96
147
|
# A single file keeps the old path: grep prints a bare count with one argument, so the batch parser would have to
|
|
97
148
|
# special-case it anyway, and one file means one process either way.
|
|
98
149
|
scan_file(){
|
|
99
150
|
local f="$1"
|
|
100
|
-
local c h m l
|
|
151
|
+
local c h m l iv fm
|
|
101
152
|
# grep -c prints the count (0 on no match) and exits 1 on no match; that non-zero exit is fine here (no set -e),
|
|
102
153
|
# and a `|| echo 0` would DOUBLE the printed 0 and break the arithmetic — so don't add one.
|
|
103
154
|
c=$(grep -icE "$CRIT" "$f" 2>/dev/null); h=$(grep -icE "$HIGH" "$f" 2>/dev/null)
|
|
104
155
|
m=$(grep -icE "$MED" "$f" 2>/dev/null); l=$(grep -icE "$LOW" "$f" 2>/dev/null)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
local v; v="$(verdict "$score" "$c" "$h")"
|
|
108
|
-
N=$((N+1))
|
|
109
|
-
case "$v" in
|
|
110
|
-
SAFE) printf ' ✅ %-3s %s\n' "$score" "$f" ;;
|
|
111
|
-
REVIEW) printf ' ⚠️ %-3s %s (crit:%s high:%s med:%s low:%s) — REVIEW\n' "$score" "$f" "$c" "$h" "$m" "$l"; FAIL=1 ;;
|
|
112
|
-
DANGER) printf ' ❌ %-3s %s (crit:%s high:%s med:%s low:%s) — DANGER\n' "$score" "$f" "$c" "$h" "$m" "$l"; FAIL=1 ;;
|
|
113
|
-
esac
|
|
156
|
+
iv=$(grep -cE "$INVIS" "$f" 2>/dev/null); fm=$(fm_counts "$f"); fm="${fm##*:}"
|
|
157
|
+
report "$f" "${c:-0}" "${h:-0}" "${iv:-0}" "${fm:-0}" "${m:-0}" "${l:-0}"
|
|
114
158
|
}
|
|
115
159
|
|
|
116
160
|
echo "== skill/agent security scan: $TARGET =="
|
|
@@ -141,12 +185,15 @@ elif [ -d "$TARGET" ]; then
|
|
|
141
185
|
ST="$(mktemp -d)"
|
|
142
186
|
counts_of "$CRIT" "$ST/c"; counts_of "$HIGH" "$ST/h"
|
|
143
187
|
counts_of "$MED" "$ST/m"; counts_of "$LOW" "$ST/l"
|
|
188
|
+
grep -cHE "$INVIS" -- "${FILES[@]}" > "$ST/i" 2>/dev/null; fm_counts "${FILES[@]}" > "$ST/f"
|
|
144
189
|
read_counts "$ST/c" CNT_C; NC=$RC_N; read_counts "$ST/h" CNT_H; NH=$RC_N
|
|
145
190
|
read_counts "$ST/m" CNT_M; NM=$RC_N; read_counts "$ST/l" CNT_L; NL=$RC_N
|
|
191
|
+
read_counts "$ST/i" CNT_I; NI=$RC_N; read_counts "$ST/f" CNT_F; NF=$RC_N
|
|
146
192
|
rm -rf "$ST"
|
|
147
193
|
# If grep skipped a file (unreadable, vanished mid-run) the rows no longer line up with FILES, and a silently
|
|
148
|
-
# misaligned count would score the wrong file. Fall back to the per-file path rather than report a guess.
|
|
149
|
-
|
|
194
|
+
# misaligned count would score the wrong file. Fall back to the per-file path rather than report a guess. awk
|
|
195
|
+
# prints nothing for an empty file, so one empty file sends the whole run down that path: correct, only slower.
|
|
196
|
+
if [ "$NC" = "${#FILES[@]}" ] && [ "$NH" = "$NC" ] && [ "$NM" = "$NC" ] && [ "$NL" = "$NC" ] && [ "$NI" = "$NC" ] && [ "$NF" = "$NC" ]; then
|
|
150
197
|
emit
|
|
151
198
|
else
|
|
152
199
|
for f in "${FILES[@]}"; do scan_file "$f"; done
|