@mmerterden/multi-agent-pipeline 11.4.0 → 12.0.0
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 +172 -0
- package/README.md +1 -0
- package/docs/adr/0007-multi-tool-adapter-framework.md +2 -2
- package/index.js +9 -2
- package/install/_common.mjs +107 -5
- package/install/_telemetry.mjs +5 -23
- package/install/claude.mjs +95 -11
- package/install/copilot.mjs +75 -6
- package/install/index.mjs +34 -1
- package/package.json +4 -10
- package/pipeline/commands/multi-agent/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +2 -1
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/finish/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +29 -3
- package/pipeline/commands/multi-agent/help/SKILL.md +3 -2
- package/pipeline/commands/multi-agent/issue/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/language/SKILL.md +20 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/log/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/purge/SKILL.md +27 -24
- package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review-jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/scan/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/search/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/stack/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/status/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +20 -2
- package/pipeline/commands/multi-agent/test/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +87 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -0
- package/pipeline/lib/account-resolver.sh +61 -23
- package/pipeline/lib/channels-multi-repo.sh +7 -2
- package/pipeline/lib/count-lib.sh +27 -0
- package/pipeline/lib/credential-store.sh +23 -6
- package/pipeline/lib/extract-conventions.sh +27 -21
- package/pipeline/lib/fetch-confluence.sh +25 -13
- package/pipeline/lib/fetch-crashlytics.sh +30 -8
- package/pipeline/lib/fetch-fortify.sh +11 -2
- package/pipeline/lib/fetch-swagger.sh +18 -7
- package/pipeline/lib/figma-mcp-refresh.sh +26 -11
- package/pipeline/lib/figma-screenshot.sh +11 -2
- package/pipeline/lib/issue-fetcher.sh +31 -6
- package/pipeline/lib/multi-repo-pipeline.sh +84 -20
- package/pipeline/lib/plan-todos.sh +12 -3
- package/pipeline/lib/post-pr-review.sh +5 -3
- package/pipeline/lib/repo-cache.sh +53 -9
- package/pipeline/lib/review-watch.sh +26 -6
- package/pipeline/lib/shadow-git.sh +45 -4
- package/pipeline/lib/vercel-deploy.sh +10 -1
- package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +12 -1
- package/pipeline/scripts/audit-log-rotate.sh +38 -10
- package/pipeline/scripts/check-md-links.mjs +34 -9
- package/pipeline/scripts/diff-risk-score.mjs +4 -1
- package/pipeline/scripts/evidence-gate.mjs +10 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +6 -4
- package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
- package/pipeline/scripts/gc-tmp.sh +67 -8
- package/pipeline/scripts/gc-worktrees.sh +140 -0
- package/pipeline/scripts/keychain-save.sh +13 -9
- package/pipeline/scripts/lint-skills.mjs +40 -2
- package/pipeline/scripts/localize-commands.mjs +130 -0
- package/pipeline/scripts/migrate-prefs.mjs +26 -7
- package/pipeline/scripts/phase-tracker.sh +71 -0
- package/pipeline/scripts/pre-commit-check.sh +39 -0
- package/pipeline/scripts/prune-logs.sh +100 -15
- package/pipeline/scripts/purge.sh +235 -0
- package/pipeline/scripts/scan-skills.sh +217 -127
- package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
- package/pipeline/scripts/smoke-description-tr.sh +82 -0
- package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
- package/pipeline/scripts/smoke-gc-tmp.sh +50 -4
- package/pipeline/scripts/smoke-gc-worktrees.sh +125 -0
- package/pipeline/scripts/smoke-install-layout.sh +11 -3
- package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
- package/pipeline/scripts/smoke-pack-contents.sh +140 -0
- package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
- package/pipeline/scripts/smoke-prune-logs.sh +57 -7
- package/pipeline/scripts/smoke-purge.sh +138 -0
- package/pipeline/scripts/smoke-shadow-git.sh +48 -1
- package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
- package/pipeline/scripts/smoke-update-check.sh +13 -0
- package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
- package/pipeline/scripts/test-gap-scan.mjs +12 -1
- package/pipeline/scripts/triage-memory.mjs +10 -1
- package/pipeline/scripts/uninstall.mjs +146 -42
- package/pipeline/scripts/update-check.sh +9 -0
- package/pipeline/scripts/update-issue-progress.sh +60 -41
- package/pipeline/scripts/write-state.mjs +14 -4
- package/pipeline/skills/.skill-manifest.json +7 -7
- package/pipeline/skills/.skills-index.json +9 -9
- package/pipeline/skills/shared/README.md +19 -10
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +74 -0
- package/pipeline/skills/skills-index.md +1 -1
- package/pipeline/commands/multi-agent/delete/SKILL.md +0 -67
- package/pipeline/skills/shared/core/multi-agent-delete/SKILL.md +0 -66
|
@@ -85,12 +85,24 @@ add_finding() {
|
|
|
85
85
|
FINDINGS+=("$sev|$file|$line|$pattern|$message")
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
# --- Pattern scanning
|
|
88
|
+
# --- Pattern scanning (single-pass tree-wide) ----------------------------
|
|
89
|
+
#
|
|
90
|
+
# Each pattern family runs as ONE grep over the whole file list instead of
|
|
91
|
+
# one grep per file (the per-file design spawned thousands of processes and
|
|
92
|
+
# dominated install time). Findings are tagged with (fileIdx, familyIdx,
|
|
93
|
+
# seq) sort keys and re-ordered afterwards so the output is byte-identical
|
|
94
|
+
# to the historical per-file scan order:
|
|
95
|
+
# per file: critical(1 shell-pipe, 2 base64-pipe, 3 eval-net, 4 bidi)
|
|
96
|
+
# high(5 js-eval / 6 py-exec, 7 credential, 8 pastebin, 9 chmod)
|
|
97
|
+
# medium(10 long-base64) low(11 unknown-endpoint, 12 SKILL.md)
|
|
89
98
|
|
|
90
99
|
# Files to consider: *.md, *.sh, *.py, *.mjs, *.js, *.ts (skill content)
|
|
91
100
|
# Exclude: binary, images, large fixtures, hidden
|
|
92
|
-
|
|
93
|
-
trap 'rm -
|
|
101
|
+
WORK="$(mktemp -d)"
|
|
102
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
103
|
+
SCAN_LIST="$WORK/files"
|
|
104
|
+
RAW="$WORK/raw"
|
|
105
|
+
: > "$RAW"
|
|
94
106
|
|
|
95
107
|
find "$ROOT" -type f \
|
|
96
108
|
\( -name "*.md" -o -name "*.sh" -o -name "*.py" -o -name "*.mjs" -o -name "*.js" -o -name "*.ts" \) \
|
|
@@ -98,156 +110,230 @@ find "$ROOT" -type f \
|
|
|
98
110
|
-not -path "*/.git/*" \
|
|
99
111
|
2>/dev/null > "$SCAN_LIST"
|
|
100
112
|
|
|
101
|
-
|
|
102
|
-
local f="$1"
|
|
103
|
-
# curl|wget piping to shell
|
|
104
|
-
while IFS= read -r hit; do
|
|
105
|
-
[ -z "$hit" ] && continue
|
|
106
|
-
local ln="${hit%%:*}"
|
|
107
|
-
add_finding critical "$f" "$ln" "shell-pipe-exec" "curl/wget piped to shell interpreter"
|
|
108
|
-
done < <(grep -nE '(curl|wget)[^|]*\|[[:space:]]*(sh|bash|zsh|ksh|ash|dash)([[:space:]]|$)' "$f" 2>/dev/null)
|
|
113
|
+
FILE_COUNT=$(wc -l < "$SCAN_LIST" | tr -d '[:space:]')
|
|
109
114
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
# One grep invocation for a whole pattern family. xargs preserves argument
|
|
116
|
+
# order, so hits come out grouped per file in SCAN_LIST order.
|
|
117
|
+
tree_grep() {
|
|
118
|
+
tr '\n' '\0' < "$SCAN_LIST" | xargs -0 grep -nHE -- "$1" 2>/dev/null
|
|
119
|
+
return 0
|
|
120
|
+
}
|
|
116
121
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
# stdin: "path:line:content" grep hits -> stdout: "fileIdx|path|line|content"
|
|
123
|
+
index_hits() {
|
|
124
|
+
awk -v listfile="$SCAN_LIST" '
|
|
125
|
+
BEGIN {
|
|
126
|
+
i = 0
|
|
127
|
+
while ((getline l < listfile) > 0) { i++; idx[l] = i }
|
|
128
|
+
close(listfile)
|
|
129
|
+
}
|
|
130
|
+
{
|
|
131
|
+
p = index($0, ":"); f = substr($0, 1, p - 1)
|
|
132
|
+
rest = substr($0, p + 1)
|
|
133
|
+
q = index(rest, ":"); ln = substr(rest, 1, q - 1)
|
|
134
|
+
printf "%d|%s|%s|%s\n", (f in idx ? idx[f] : 999999), f, ln, substr(rest, q + 1)
|
|
135
|
+
}'
|
|
136
|
+
}
|
|
123
137
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
# stdin: bare file paths -> stdout: "fileIdx|path"
|
|
139
|
+
index_files() {
|
|
140
|
+
awk -v listfile="$SCAN_LIST" '
|
|
141
|
+
BEGIN {
|
|
142
|
+
i = 0
|
|
143
|
+
while ((getline l < listfile) > 0) { i++; idx[l] = i }
|
|
144
|
+
close(listfile)
|
|
145
|
+
}
|
|
146
|
+
{ printf "%d|%s\n", ($0 in idx ? idx[$0] : 999999), $0 }'
|
|
128
147
|
}
|
|
129
148
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
emit_raw() {
|
|
150
|
+
# $1 fileIdx $2 familyIdx $3 seq $4 sev $5 file $6 line $7 pattern $8 message
|
|
151
|
+
printf '%s|%s|%s|%s|%s|%s|%s|%s\n' "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" >> "$RAW"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Split an indexed hit "fileIdx|path|line|content" into HIT_IDX/HIT_FILE/
|
|
155
|
+
# HIT_LINE/HIT_CONTENT (content may itself contain pipes - it is the rest).
|
|
156
|
+
parse_hit() {
|
|
157
|
+
HIT_IDX="${1%%|*}"; local rest="${1#*|}"
|
|
158
|
+
HIT_FILE="${rest%%|*}"; rest="${rest#*|}"
|
|
159
|
+
HIT_LINE="${rest%%|*}"
|
|
160
|
+
HIT_CONTENT="${rest#*|}"
|
|
161
|
+
}
|
|
133
162
|
|
|
163
|
+
# --- critical families (rank 0 - always at/above threshold) ------------
|
|
164
|
+
|
|
165
|
+
seq=0
|
|
166
|
+
while IFS= read -r hit; do
|
|
167
|
+
[ -z "$hit" ] && continue
|
|
168
|
+
parse_hit "$hit"
|
|
169
|
+
seq=$((seq+1))
|
|
170
|
+
emit_raw "$HIT_IDX" 1 "$seq" critical "$HIT_FILE" "$HIT_LINE" "shell-pipe-exec" "curl/wget piped to shell interpreter"
|
|
171
|
+
done < <(tree_grep '(curl|wget)[^|]*\|[[:space:]]*(sh|bash|zsh|ksh|ash|dash)([[:space:]]|$)' | index_hits)
|
|
172
|
+
|
|
173
|
+
seq=0
|
|
174
|
+
while IFS= read -r hit; do
|
|
175
|
+
[ -z "$hit" ] && continue
|
|
176
|
+
parse_hit "$hit"
|
|
177
|
+
seq=$((seq+1))
|
|
178
|
+
emit_raw "$HIT_IDX" 2 "$seq" critical "$HIT_FILE" "$HIT_LINE" "base64-pipe-exec" "base64 decoded output piped to shell"
|
|
179
|
+
done < <(tree_grep '(base64|openssl[[:space:]]+base64)[^|]*-d[^|]*\|[[:space:]]*(sh|bash|zsh|eval|sudo)' | index_hits)
|
|
180
|
+
|
|
181
|
+
seq=0
|
|
182
|
+
while IFS= read -r hit; do
|
|
183
|
+
[ -z "$hit" ] && continue
|
|
184
|
+
parse_hit "$hit"
|
|
185
|
+
seq=$((seq+1))
|
|
186
|
+
emit_raw "$HIT_IDX" 3 "$seq" critical "$HIT_FILE" "$HIT_LINE" "eval-of-network" "eval of network-fetched content"
|
|
187
|
+
done < <(tree_grep 'eval[[:space:]]+(\$\([[:space:]]*(curl|wget|fetch)|`[[:space:]]*(curl|wget|fetch))' | index_hits)
|
|
188
|
+
|
|
189
|
+
# Unicode bidi override characters - invisible injection attack
|
|
190
|
+
seq=0
|
|
191
|
+
while IFS= read -r row; do
|
|
192
|
+
[ -z "$row" ] && continue
|
|
193
|
+
seq=$((seq+1))
|
|
194
|
+
emit_raw "${row%%|*}" 4 "$seq" critical "${row#*|}" "0" "unicode-bidi" "bidirectional control chars (U+202D-202E, U+2066-2069) - trojan source risk"
|
|
195
|
+
done < <(tr '\n' '\0' < "$SCAN_LIST" \
|
|
196
|
+
| LC_ALL=C xargs -0 grep -l $'\xe2\x80\xad\|\xe2\x80\xae\|\xe2\x81\xa6\|\xe2\x81\xa7\|\xe2\x81\xa8\|\xe2\x81\xa9' 2>/dev/null \
|
|
197
|
+
| index_files; true)
|
|
198
|
+
|
|
199
|
+
# --- high families (rank 1) ----------------------------------------------
|
|
200
|
+
|
|
201
|
+
if [ "$THRESHOLD_RANK" -ge 1 ]; then
|
|
134
202
|
# JavaScript/TypeScript: eval(, new Function(, Function(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
203
|
+
seq=0
|
|
204
|
+
while IFS= read -r hit; do
|
|
205
|
+
[ -z "$hit" ] && continue
|
|
206
|
+
parse_hit "$hit"
|
|
207
|
+
case "$HIT_FILE" in *.js|*.mjs|*.ts) ;; *) continue ;; esac
|
|
208
|
+
seq=$((seq+1))
|
|
209
|
+
emit_raw "$HIT_IDX" 5 "$seq" high "$HIT_FILE" "$HIT_LINE" "js-dynamic-eval" "JavaScript dynamic code execution"
|
|
210
|
+
done < <(tree_grep '\b(eval|new[[:space:]]+Function|Function)\s*\(' | index_hits)
|
|
142
211
|
|
|
143
212
|
# Python: exec(, eval( on non-literal - exclude re.compile (regex) and subprocess
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
213
|
+
seq=0
|
|
214
|
+
while IFS= read -r hit; do
|
|
215
|
+
[ -z "$hit" ] && continue
|
|
216
|
+
parse_hit "$hit"
|
|
217
|
+
case "$HIT_FILE" in *.py) ;; *) continue ;; esac
|
|
218
|
+
# Skip re.compile (standard regex) and subprocess.* (legitimate process invocation)
|
|
219
|
+
echo "$HIT_CONTENT" | grep -qE '(\bre\.compile|\bre2\.compile|subprocess\.|typing\.)' && continue
|
|
220
|
+
seq=$((seq+1))
|
|
221
|
+
emit_raw "$HIT_IDX" 6 "$seq" high "$HIT_FILE" "$HIT_LINE" "py-dynamic-exec" "Python dynamic code execution"
|
|
222
|
+
done < <(tree_grep '(^|[^a-zA-Z0-9_.])(exec|eval)[[:space:]]*\(' | index_hits)
|
|
154
223
|
|
|
155
224
|
# Hardcoded API keys - AWS, OpenAI, GitHub, generic sk-* with length
|
|
156
225
|
# Skip lines in FORBIDDEN/NEVER/example blocks (false positives from docs)
|
|
226
|
+
seq=0
|
|
157
227
|
while IFS= read -r hit; do
|
|
158
228
|
[ -z "$hit" ] && continue
|
|
159
|
-
|
|
160
|
-
# Check
|
|
161
|
-
|
|
229
|
+
parse_hit "$hit"
|
|
230
|
+
# Check +/-3 lines around match for documentation context markers
|
|
231
|
+
ctx_start=$((HIT_LINE - 3))
|
|
162
232
|
[ "$ctx_start" -lt 1 ] && ctx_start=1
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
ctx=$(sed -n "${ctx_start},${ctx_end}p" "$f" 2>/dev/null)
|
|
233
|
+
ctx_end=$((HIT_LINE + 3))
|
|
234
|
+
ctx=$(sed -n "${ctx_start},${ctx_end}p" "$HIT_FILE" 2>/dev/null)
|
|
166
235
|
if echo "$ctx" | grep -qiE '(FORBIDDEN|NEVER do|don.t do|example:|placeholder|sample credential|DO NOT|✗|XXX|YYY|dummy|fake.?key)'; then
|
|
167
236
|
continue
|
|
168
237
|
fi
|
|
169
|
-
|
|
170
|
-
|
|
238
|
+
seq=$((seq+1))
|
|
239
|
+
emit_raw "$HIT_IDX" 7 "$seq" high "$HIT_FILE" "$HIT_LINE" "hardcoded-credential" "possible hardcoded API credential"
|
|
240
|
+
done < <(tree_grep '(AKIA[0-9A-Z]{16}|sk-(live|test|proj|ant|or)-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82}|gho_[A-Za-z0-9]{36}|xox[bp]-[A-Za-z0-9-]{10,})' | index_hits)
|
|
171
241
|
|
|
172
242
|
# Pastebin / URL shortener raw content fetching
|
|
243
|
+
seq=0
|
|
173
244
|
while IFS= read -r hit; do
|
|
174
245
|
[ -z "$hit" ] && continue
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
246
|
+
parse_hit "$hit"
|
|
247
|
+
seq=$((seq+1))
|
|
248
|
+
emit_raw "$HIT_IDX" 8 "$seq" high "$HIT_FILE" "$HIT_LINE" "pastebin-fetch" "fetch from ephemeral/obscured content host"
|
|
249
|
+
done < <(tree_grep 'https?://(pastebin\.com/raw|paste\.ee|ghostbin|hastebin|bit\.ly|tinyurl\.com|goo\.gl|t\.co|is\.gd|ow\.ly|rebrand\.ly|gist\.github\.com/[^/]+/[a-f0-9]+/raw)' | index_hits)
|
|
178
250
|
|
|
179
251
|
# chmod +x followed by execution on same file in same block
|
|
252
|
+
seq=0
|
|
180
253
|
while IFS= read -r hit; do
|
|
181
254
|
[ -z "$hit" ] && continue
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
255
|
+
parse_hit "$hit"
|
|
256
|
+
seq=$((seq+1))
|
|
257
|
+
emit_raw "$HIT_IDX" 9 "$seq" high "$HIT_FILE" "$HIT_LINE" "chmod-then-exec" "script made executable and immediately invoked"
|
|
258
|
+
done < <(tree_grep 'chmod[[:space:]]+\+x[[:space:]]+[^&;]+[[:space:]]*(&&|;)[[:space:]]*\./' | index_hits)
|
|
259
|
+
fi
|
|
186
260
|
|
|
187
|
-
|
|
188
|
-
local f="$1"
|
|
261
|
+
# --- medium families (rank 2) ---------------------------------------------
|
|
189
262
|
|
|
263
|
+
if [ "$THRESHOLD_RANK" -ge 2 ]; then
|
|
190
264
|
# Long base64 blobs - obfuscation indicator.
|
|
191
265
|
# Skip .md files (documentation often shows example b64) - only scan executable content types.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
esac
|
|
202
|
-
}
|
|
266
|
+
seq=0
|
|
267
|
+
while IFS= read -r hit; do
|
|
268
|
+
[ -z "$hit" ] && continue
|
|
269
|
+
parse_hit "$hit"
|
|
270
|
+
case "$HIT_FILE" in *.md) continue ;; esac
|
|
271
|
+
seq=$((seq+1))
|
|
272
|
+
emit_raw "$HIT_IDX" 10 "$seq" medium "$HIT_FILE" "$HIT_LINE" "long-base64" "base64-looking blob >200 chars (possible obfuscation)"
|
|
273
|
+
done < <(tree_grep '[A-Za-z0-9+/]{200,}={0,2}' | index_hits)
|
|
274
|
+
fi
|
|
203
275
|
|
|
204
|
-
|
|
205
|
-
local f="$1"
|
|
276
|
+
# --- low families (rank 3) -------------------------------------------------
|
|
206
277
|
|
|
278
|
+
if [ "$THRESHOLD_RANK" -ge 3 ]; then
|
|
207
279
|
# Unknown network endpoints - LOW severity (informational). URLs in skills
|
|
208
280
|
# are usually doc references, not exfil channels. Only the critical
|
|
209
281
|
# shell-pipe-exec patterns detect active network abuse.
|
|
282
|
+
# Historical behavior: at most the first 20 URL-bearing lines per file.
|
|
283
|
+
url_re='https?://[a-zA-Z0-9._-]+'
|
|
284
|
+
allow_re="^${ALLOW_DOMAINS}\$"
|
|
285
|
+
seq=0
|
|
286
|
+
prev_file=""
|
|
287
|
+
url_lines=0
|
|
210
288
|
while IFS= read -r hit; do
|
|
211
289
|
[ -z "$hit" ] && continue
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
290
|
+
parse_hit "$hit"
|
|
291
|
+
if [ "$HIT_FILE" != "$prev_file" ]; then
|
|
292
|
+
prev_file="$HIT_FILE"
|
|
293
|
+
url_lines=0
|
|
294
|
+
fi
|
|
295
|
+
url_lines=$((url_lines+1))
|
|
296
|
+
[ "$url_lines" -gt 20 ] && continue
|
|
297
|
+
[[ "$HIT_CONTENT" =~ $url_re ]] || continue
|
|
298
|
+
url="${BASH_REMATCH[0]}"
|
|
299
|
+
host="${url#*://}"
|
|
218
300
|
host="${host%%/*}"
|
|
219
|
-
if !
|
|
220
|
-
|
|
301
|
+
if ! [[ "$host" =~ $allow_re ]]; then
|
|
302
|
+
seq=$((seq+1))
|
|
303
|
+
emit_raw "$HIT_IDX" 11 "$seq" low "$HIT_FILE" "$HIT_LINE" "unknown-endpoint" "network endpoint not in allow-list: $host"
|
|
221
304
|
fi
|
|
222
|
-
done < <(
|
|
223
|
-
}
|
|
305
|
+
done < <(tree_grep 'https?://[a-zA-Z0-9._-]+' | index_hits)
|
|
224
306
|
|
|
225
|
-
scan_low() {
|
|
226
|
-
local f="$1"
|
|
227
307
|
# Missing SKILL.md frontmatter (only check files named SKILL.md)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
308
|
+
seq=0
|
|
309
|
+
file_idx=0
|
|
310
|
+
while IFS= read -r file; do
|
|
311
|
+
file_idx=$((file_idx+1))
|
|
312
|
+
[ -z "$file" ] && continue
|
|
313
|
+
case "$file" in
|
|
314
|
+
*/SKILL.md)
|
|
315
|
+
if ! head -1 "$file" | grep -Fxq -- "---"; then
|
|
316
|
+
seq=$((seq+1))
|
|
317
|
+
emit_raw "$file_idx" 12 "$seq" low "$file" "1" "missing-frontmatter" "SKILL.md without YAML frontmatter"
|
|
318
|
+
elif ! grep -qE '^description:' "$file"; then
|
|
319
|
+
seq=$((seq+1))
|
|
320
|
+
emit_raw "$file_idx" 12 "$seq" low "$file" "0" "missing-description" "SKILL.md frontmatter missing 'description'"
|
|
321
|
+
fi
|
|
322
|
+
;;
|
|
323
|
+
esac
|
|
324
|
+
done < "$SCAN_LIST"
|
|
325
|
+
fi
|
|
240
326
|
|
|
241
|
-
|
|
242
|
-
while IFS= read -r
|
|
243
|
-
[ -z "$
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
done < "$
|
|
327
|
+
# Re-order into the historical per-file scan order and load into FINDINGS.
|
|
328
|
+
while IFS= read -r row; do
|
|
329
|
+
[ -z "$row" ] && continue
|
|
330
|
+
rest="${row#*|}"; rest="${rest#*|}"; rest="${rest#*|}" # drop idx|fam|seq|
|
|
331
|
+
sev="${rest%%|*}"; rest="${rest#*|}"
|
|
332
|
+
file="${rest%%|*}"; rest="${rest#*|}"
|
|
333
|
+
line="${rest%%|*}"; rest="${rest#*|}"
|
|
334
|
+
pat="${rest%%|*}"; msg="${rest#*|}"
|
|
335
|
+
add_finding "$sev" "$file" "$line" "$pat" "$msg"
|
|
336
|
+
done < <(sort -t'|' -k1,1n -k2,2n -k3,3n "$RAW")
|
|
251
337
|
|
|
252
338
|
# --- Report -------------------------------------------------------------
|
|
253
339
|
|
|
@@ -261,20 +347,24 @@ if [ "$JSON" -eq 1 ]; then
|
|
|
261
347
|
printf ' "findings": [\n'
|
|
262
348
|
local_i=0
|
|
263
349
|
total=${#FINDINGS[@]}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
350
|
+
# Guard the expansion: on an empty array "${FINDINGS[@]:-}" yields one empty
|
|
351
|
+
# word, which used to emit a spurious all-empty findings element.
|
|
352
|
+
if [ "$total" -gt 0 ]; then
|
|
353
|
+
for f in "${FINDINGS[@]}"; do
|
|
354
|
+
local_i=$((local_i+1))
|
|
355
|
+
sev="${f%%|*}"; rest="${f#*|}"
|
|
356
|
+
file="${rest%%|*}"; rest="${rest#*|}"
|
|
357
|
+
line="${rest%%|*}"; rest="${rest#*|}"
|
|
358
|
+
pat="${rest%%|*}"; msg="${rest#*|}"
|
|
359
|
+
# json-escape file/msg minimally
|
|
360
|
+
jf=$(printf '%s' "$file" | sed 's/"/\\"/g')
|
|
361
|
+
jm=$(printf '%s' "$msg" | sed 's/"/\\"/g')
|
|
362
|
+
printf ' { "severity": "%s", "file": "%s", "line": "%s", "pattern": "%s", "message": "%s" }' \
|
|
363
|
+
"$sev" "$jf" "$line" "$pat" "$jm"
|
|
364
|
+
[ "$local_i" -lt "$total" ] && printf ','
|
|
365
|
+
printf '\n'
|
|
366
|
+
done
|
|
367
|
+
fi
|
|
278
368
|
printf ' ]\n}\n'
|
|
279
369
|
else
|
|
280
370
|
# Colored text report
|
|
@@ -295,7 +385,7 @@ else
|
|
|
295
385
|
printf ' ✓ clean (0 findings)\n'
|
|
296
386
|
else
|
|
297
387
|
printf ' %sfound %d%s: critical=%d high=%d medium=%d low=%d\n\n' "$C_BLD" "$total" "$C_RST" "$CRIT" "$HIGH" "$MED" "$LOW"
|
|
298
|
-
for f in "${FINDINGS[@]
|
|
388
|
+
for f in "${FINDINGS[@]}"; do
|
|
299
389
|
sev="${f%%|*}"; rest="${f#*|}"
|
|
300
390
|
file="${rest%%|*}"; rest="${rest#*|}"
|
|
301
391
|
line="${rest%%|*}"; rest="${rest#*|}"
|
|
@@ -307,7 +397,7 @@ else
|
|
|
307
397
|
low) ico="· "; col="$C_DIM" ;;
|
|
308
398
|
*) ico="? "; col="" ;;
|
|
309
399
|
esac
|
|
310
|
-
rel="${file
|
|
400
|
+
rel="${file#"$REPO_ROOT"/}"
|
|
311
401
|
printf ' %s%s%s %-8s %s:%s [%s]\n' "$col" "$ico" "$C_RST" "$sev" "$rel" "$line" "$pat"
|
|
312
402
|
printf ' %s%s%s\n' "$C_DIM" "$msg" "$C_RST"
|
|
313
403
|
done
|
|
@@ -66,11 +66,20 @@ cat > "$TMP/fixture.json" <<'EOF'
|
|
|
66
66
|
EOF
|
|
67
67
|
|
|
68
68
|
# Stub curl that captures invocation and returns the fixture. PATH-shadows the
|
|
69
|
-
# real curl for this smoke only.
|
|
69
|
+
# real curl for this smoke only. Auth travels via `-K <config-file>` (so the
|
|
70
|
+
# token never hits argv); the stub dumps the config file so the smoke can
|
|
71
|
+
# assert its contents.
|
|
70
72
|
cat > "$TMP/curl" <<EOF
|
|
71
73
|
#!/usr/bin/env bash
|
|
72
|
-
# Capture every invocation so we can assert the URL + auth
|
|
74
|
+
# Capture every invocation so we can assert the URL + auth config shape.
|
|
73
75
|
echo "INVOCATION:" "\$@" >> "$TMP/curl-calls.log"
|
|
76
|
+
prev=""
|
|
77
|
+
for arg in "\$@"; do
|
|
78
|
+
if [ "\$prev" = "-K" ]; then
|
|
79
|
+
echo "AUTH-CONFIG: \$(cat "\$arg" 2>/dev/null)" >> "$TMP/curl-calls.log"
|
|
80
|
+
fi
|
|
81
|
+
prev="\$arg"
|
|
82
|
+
done
|
|
74
83
|
# Echo the fixture body (BB Server response) to stdout - same as real curl -sf.
|
|
75
84
|
cat "$TMP/fixture.json"
|
|
76
85
|
exit 0
|
|
@@ -153,10 +162,17 @@ else
|
|
|
153
162
|
fail "limit param missing"
|
|
154
163
|
fi
|
|
155
164
|
|
|
156
|
-
if grep -q "test-bb-user:test-bb-token" "$TMP/curl-calls.log"; then
|
|
157
|
-
ok "Basic auth uses {user}:{token}
|
|
165
|
+
if grep -q 'AUTH-CONFIG: user = "test-bb-user:test-bb-token"' "$TMP/curl-calls.log"; then
|
|
166
|
+
ok "Basic auth uses {user}:{token} via curl -K config (not argv)"
|
|
158
167
|
else
|
|
159
|
-
fail "auth
|
|
168
|
+
fail "auth config wrong; captured: $(grep 'AUTH-CONFIG' "$TMP/curl-calls.log" || echo none)"
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# The raw token must never appear on curl argv (visible to ps).
|
|
172
|
+
if grep "^INVOCATION:" "$TMP/curl-calls.log" | grep -q "test-bb-token"; then
|
|
173
|
+
fail "token leaked onto curl argv"
|
|
174
|
+
else
|
|
175
|
+
ok "token absent from curl argv"
|
|
160
176
|
fi
|
|
161
177
|
|
|
162
178
|
# --- Assert: shape conversion produced canonical schema ---------------------
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smoke-description-tr.sh - localized picker-description contract.
|
|
3
|
+
#
|
|
4
|
+
# Verifies: every shipped command SKILL.md carries a description-tr line
|
|
5
|
+
# (coverage), repo description: lines stay English with no description-en
|
|
6
|
+
# sidecars committed (hygiene), and localize-commands.mjs apply/restore is
|
|
7
|
+
# an idempotent, byte-identical round trip (mechanics).
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
13
|
+
CMD_DIR="$REPO_ROOT/pipeline/commands/multi-agent"
|
|
14
|
+
LOCALIZER="$REPO_ROOT/pipeline/scripts/localize-commands.mjs"
|
|
15
|
+
|
|
16
|
+
PASS=0
|
|
17
|
+
FAIL=0
|
|
18
|
+
record_pass() { PASS=$((PASS + 1)); printf ' \342\234\223 %s\n' "$1"; }
|
|
19
|
+
record_fail() { FAIL=$((FAIL + 1)); printf ' \342\234\227 %s\n' "$1"; }
|
|
20
|
+
|
|
21
|
+
printf '\342\206\222 smoke-description-tr: localized picker-description contract\n'
|
|
22
|
+
|
|
23
|
+
# 1. Coverage: every command SKILL.md (incl. the dispatcher) has description-tr
|
|
24
|
+
missing=""
|
|
25
|
+
count=0
|
|
26
|
+
for f in "$CMD_DIR/SKILL.md" "$CMD_DIR"/*/SKILL.md; do
|
|
27
|
+
[ -f "$f" ] || continue
|
|
28
|
+
count=$((count + 1))
|
|
29
|
+
grep -q '^description-tr:' "$f" || missing="$missing ${f#"$CMD_DIR/"}"
|
|
30
|
+
done
|
|
31
|
+
if [ -z "$missing" ]; then
|
|
32
|
+
record_pass "all $count command SKILL.md files carry description-tr"
|
|
33
|
+
else
|
|
34
|
+
record_fail "missing description-tr in:$missing"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# 2. Hygiene: repo descriptions stay English, no sidecars committed
|
|
38
|
+
if grep -rn '^description:.*[çğıöşüÇĞİÖŞÜ]' "$CMD_DIR" >/dev/null 2>&1; then
|
|
39
|
+
record_fail "repo description: lines must stay English (Turkish characters found)"
|
|
40
|
+
else
|
|
41
|
+
record_pass "repo description: lines are English"
|
|
42
|
+
fi
|
|
43
|
+
if grep -rln '^description-en:' "$CMD_DIR" >/dev/null 2>&1; then
|
|
44
|
+
record_fail "description-en sidecar committed to the repo (installed-tree artifact)"
|
|
45
|
+
else
|
|
46
|
+
record_pass "no description-en sidecar in the repo"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# 3. Translation length within the agent-skills description budget
|
|
50
|
+
long=$(awk '/^description-tr:/ && length($0) > 1044 { print FILENAME }' "$CMD_DIR"/*/SKILL.md "$CMD_DIR/SKILL.md" 2>/dev/null | head -1)
|
|
51
|
+
if [ -z "$long" ]; then
|
|
52
|
+
record_pass "every description-tr is within the 1024-char budget"
|
|
53
|
+
else
|
|
54
|
+
record_fail "description-tr exceeds budget in $long"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# 4. Round trip: apply is idempotent, restore is byte-identical
|
|
58
|
+
tmpdir=$(mktemp -d)
|
|
59
|
+
trap 'rm -rf "$tmpdir"' EXIT
|
|
60
|
+
cp -R "$CMD_DIR" "$tmpdir/cmds"
|
|
61
|
+
node "$LOCALIZER" apply --dir "$tmpdir/cmds" >/dev/null 2>&1
|
|
62
|
+
if grep -q '^description:.*[çğıöşüÇĞİÖŞÜ]' "$tmpdir/cmds/dev/SKILL.md" \
|
|
63
|
+
&& grep -q '^description-en:' "$tmpdir/cmds/dev/SKILL.md"; then
|
|
64
|
+
record_pass "apply swaps description and writes the description-en sidecar"
|
|
65
|
+
else
|
|
66
|
+
record_fail "apply did not localize the sample file"
|
|
67
|
+
fi
|
|
68
|
+
second=$(node "$LOCALIZER" apply --dir "$tmpdir/cmds" 2>/dev/null)
|
|
69
|
+
if printf '%s' "$second" | grep -q ' 0 file(s) changed'; then
|
|
70
|
+
record_pass "second apply is a no-op (idempotent)"
|
|
71
|
+
else
|
|
72
|
+
record_fail "second apply changed files again: $second"
|
|
73
|
+
fi
|
|
74
|
+
node "$LOCALIZER" restore --dir "$tmpdir/cmds" >/dev/null 2>&1
|
|
75
|
+
if diff -r "$CMD_DIR" "$tmpdir/cmds" >/dev/null 2>&1; then
|
|
76
|
+
record_pass "restore returns a byte-identical tree"
|
|
77
|
+
else
|
|
78
|
+
record_fail "restore left differences vs the original tree"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
printf '\n\342\225\220\342\225\220 smoke-description-tr: %d passed, %d failed \342\225\220\342\225\220\n' "$PASS" "$FAIL"
|
|
82
|
+
[ "$FAIL" -eq 0 ]
|