trackfw 6.10.0 → 7.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/README.md +0 -3
- package/package.json +1 -1
- package/src/branch/runner.js +33 -20
- package/src/changelog/index.js +114 -0
- package/src/commands/branch.js +12 -9
- package/src/commands/changelog.js +29 -0
- package/src/commands/commit.js +60 -0
- package/src/commands/discover.js +69 -0
- package/src/commands/index.js +22 -4
- package/src/commands/integrations.js +7 -0
- package/src/commands/ship.js +4 -3
- package/src/commands/thirdparty.js +384 -0
- package/src/commands/update-harness.js +3 -1
- package/src/commands/update.js +3 -1
- package/src/commit/runner.js +352 -0
- package/src/config/index.js +27 -0
- package/src/generators/hooks.js +479 -2
- package/src/generators/init.js +28 -6
- package/src/i18n/locales/en-US.json +1 -19
- package/src/i18n/locales/es-ES.json +1 -19
- package/src/i18n/locales/pt-BR.json +1 -19
- package/src/integrations/index.js +19 -2
- package/src/integrations/manager.js +15 -2
- package/src/lib/unknown-command.js +82 -0
- package/src/ship/runner.js +213 -26
- package/src/thirdparty/fetch.js +140 -0
- package/src/thirdparty/markers.js +207 -0
- package/src/thirdparty/provenance.js +114 -0
- package/src/thirdparty/quarantine.js +113 -0
- package/src/thirdparty/references.js +194 -0
- package/src/validator/index.js +715 -16
- package/src/commands/plugins.js +0 -184
package/src/validator/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const fs = require('fs')
|
|
4
|
+
const os = require('os')
|
|
4
5
|
const path = require('path')
|
|
6
|
+
const crypto = require('crypto')
|
|
5
7
|
const { gitOutput } = require('./git-exec')
|
|
6
8
|
const config = require('../config')
|
|
7
9
|
const { checkTraceIds } = require('./traceid')
|
|
10
|
+
const { loadProvenance } = require('../thirdparty/provenance')
|
|
8
11
|
|
|
9
12
|
const STALE_WIP_DAYS = 7
|
|
10
13
|
let staleWipNowMs = () => Date.now()
|
|
@@ -1152,6 +1155,13 @@ function validateNoteOrphan(cwd) {
|
|
|
1152
1155
|
// procura dentro dos comandos de hook de projeto.
|
|
1153
1156
|
const CREDENTIAL_GUARD_SCRIPT_MARKER = 'trackfw-credential-guard.sh'
|
|
1154
1157
|
|
|
1158
|
+
// GIT_BRANCH_GUARD_SCRIPT_MARKER é o nome do script que a regra git_branch_guard_hook_resolvable
|
|
1159
|
+
// procura dentro dos comandos de hook de projeto (ROADMAP-2026-08-15-trackfw-validate-deve-
|
|
1160
|
+
// detectar-scripts-de-hook-ausentes-ou-desatualizados, ML-2A — port de
|
|
1161
|
+
// internal/validator/validator_git_branch_guard.go's gitBranchGuardScriptMarker). Mesmo padrão de
|
|
1162
|
+
// CREDENTIAL_GUARD_SCRIPT_MARKER — só o nome do arquivo muda.
|
|
1163
|
+
const GIT_BRANCH_GUARD_SCRIPT_MARKER = 'trackfw-git-branch-guard.sh'
|
|
1164
|
+
|
|
1155
1165
|
// CREDENTIAL_GUARD_HOOK_FILES é a lista fechada dos arquivos de hook de PROJETO que o trackfw
|
|
1156
1166
|
// gera hoje e que podem conter uma entrada de credential-guard (ROADMAP-2026-08-12-mitigacao-do
|
|
1157
1167
|
// -fail-open-do-credential-guard, ML-1A). Hooks de escopo GLOBAL (~/.trackfw/..., trackfw update
|
|
@@ -1203,30 +1213,49 @@ function resolveCredentialGuardHookPath(raw, root) {
|
|
|
1203
1213
|
return null
|
|
1204
1214
|
}
|
|
1205
1215
|
|
|
1206
|
-
//
|
|
1207
|
-
//
|
|
1208
|
-
// campo que o contém.
|
|
1216
|
+
// collectCommandsWithMarker percorre recursivamente um valor JSON já decodificado e coleta todo
|
|
1217
|
+
// valor-string que contém marker, independentemente do nome do campo que o contém.
|
|
1209
1218
|
//
|
|
1210
1219
|
// Os 6 formatos de hook usam campos diferentes para o comando: "command" (Claude/Codex/
|
|
1211
1220
|
// Gemini/Cursor), "bash" (GitHub Copilot CLI), "action.command" (Kiro). Varrer por VALOR em vez
|
|
1212
1221
|
// de por caminho de chave evita acoplar esta regra à forma exata de cada schema.
|
|
1213
|
-
|
|
1222
|
+
//
|
|
1223
|
+
// Generalizado (ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-
|
|
1224
|
+
// desatualizados, ML-2A, port de collectCommandsWithMarker em
|
|
1225
|
+
// internal/validator/validator_git_branch_guard.go) para aceitar qualquer marker — originalmente
|
|
1226
|
+
// collectCredentialGuardCommands, hardcoded para CREDENTIAL_GUARD_SCRIPT_MARKER; reusado agora
|
|
1227
|
+
// também para GIT_BRANCH_GUARD_SCRIPT_MARKER, sem duplicar a travessia recursiva.
|
|
1228
|
+
function collectCommandsWithMarker(value, marker, out) {
|
|
1214
1229
|
if (typeof value === 'string') {
|
|
1215
|
-
if (value.includes(
|
|
1230
|
+
if (value.includes(marker)) out.push(value)
|
|
1216
1231
|
return
|
|
1217
1232
|
}
|
|
1218
1233
|
if (Array.isArray(value)) {
|
|
1219
|
-
for (const item of value)
|
|
1234
|
+
for (const item of value) collectCommandsWithMarker(item, marker, out)
|
|
1220
1235
|
return
|
|
1221
1236
|
}
|
|
1222
1237
|
if (value && typeof value === 'object') {
|
|
1223
|
-
for (const key of Object.keys(value))
|
|
1238
|
+
for (const key of Object.keys(value)) collectCommandsWithMarker(value[key], marker, out)
|
|
1224
1239
|
}
|
|
1225
1240
|
}
|
|
1226
1241
|
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1229
|
-
//
|
|
1242
|
+
// collectCredentialGuardCommands é o wrapper específico de credential-guard sobre
|
|
1243
|
+
// collectCommandsWithMarker — preservado para compatibilidade de assinatura (exportada
|
|
1244
|
+
// historicamente com 2 argumentos).
|
|
1245
|
+
function collectCredentialGuardCommands(value, out) {
|
|
1246
|
+
collectCommandsWithMarker(value, CREDENTIAL_GUARD_SCRIPT_MARKER, out)
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// validateGuardHookResolvable é a implementação genérica compartilhada pelas regras
|
|
1250
|
+
// "credential_guard_hook_resolvable" e "git_branch_guard_hook_resolvable": para cada arquivo de
|
|
1251
|
+
// hook de PROJETO que existir, extrai os comandos que referenciam scriptMarker, resolve o caminho
|
|
1252
|
+
// e verifica que o script existe e é executável.
|
|
1253
|
+
//
|
|
1254
|
+
// Generalizado a partir da antiga validateCredentialGuardHookResolvable (ROADMAP-2026-08-15-
|
|
1255
|
+
// trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados, ML-2A, port de
|
|
1256
|
+
// validateGuardHookResolvable em internal/validator/validator_credential_guard.go) — a lógica de
|
|
1257
|
+
// resolução de caminho por CLI é idêntica para os 2 scripts, só o marker e o texto da mensagem
|
|
1258
|
+
// mudam.
|
|
1230
1259
|
//
|
|
1231
1260
|
// Riscos de regressão mapeados no roadmap:
|
|
1232
1261
|
// - A regra só avalia entradas que EXISTEM. Ausência de entrada de guard é estado legítimo
|
|
@@ -1234,7 +1263,12 @@ function collectCredentialGuardCommands(value, out) {
|
|
|
1234
1263
|
// - Arquivo de hook ausente é pulado em silêncio.
|
|
1235
1264
|
// - Arquivo de hook presente mas com JSON inválido é pulado em silêncio — validar a forma do
|
|
1236
1265
|
// JSON não é escopo desta regra.
|
|
1237
|
-
|
|
1266
|
+
//
|
|
1267
|
+
// process.cwd() no Node já retorna o caminho FÍSICO (resolve symlinks via getcwd(3) diretamente),
|
|
1268
|
+
// diferente de os.Getwd() do Go — ver o comentário sobre EvalSymlinks em
|
|
1269
|
+
// internal/validator/validator_git_branch_guard.go's validateGuardHookResolvable equivalente
|
|
1270
|
+
// (validator_credential_guard.go). Nenhuma resolução extra de symlink é necessária aqui.
|
|
1271
|
+
function validateGuardHookResolvable(scriptMarker, cwd) {
|
|
1238
1272
|
const root = cwd || process.cwd()
|
|
1239
1273
|
const msgs = []
|
|
1240
1274
|
|
|
@@ -1256,7 +1290,7 @@ function validateCredentialGuardHookResolvable(cwd) {
|
|
|
1256
1290
|
}
|
|
1257
1291
|
|
|
1258
1292
|
const commands = []
|
|
1259
|
-
|
|
1293
|
+
collectCommandsWithMarker(parsed, scriptMarker, commands)
|
|
1260
1294
|
|
|
1261
1295
|
const seen = new Set()
|
|
1262
1296
|
for (const raw of commands) {
|
|
@@ -1274,9 +1308,9 @@ function validateCredentialGuardHookResolvable(cwd) {
|
|
|
1274
1308
|
}
|
|
1275
1309
|
|
|
1276
1310
|
if (!stat) {
|
|
1277
|
-
msgs.push(`${hf.relPath} (${hf.cli}) references
|
|
1311
|
+
msgs.push(`${hf.relPath} (${hf.cli}) references ${scriptMarker} resolved to "${resolved}", but the script does not exist — run \`trackfw update\` to regenerate it`)
|
|
1278
1312
|
} else if ((stat.mode & 0o111) === 0) {
|
|
1279
|
-
msgs.push(`${hf.relPath} (${hf.cli}) references
|
|
1313
|
+
msgs.push(`${hf.relPath} (${hf.cli}) references ${scriptMarker} resolved to "${resolved}", but the script is not executable — run \`trackfw update\` to regenerate it`)
|
|
1280
1314
|
}
|
|
1281
1315
|
}
|
|
1282
1316
|
}
|
|
@@ -1284,6 +1318,18 @@ function validateCredentialGuardHookResolvable(cwd) {
|
|
|
1284
1318
|
return msgs
|
|
1285
1319
|
}
|
|
1286
1320
|
|
|
1321
|
+
// validateCredentialGuardHookResolvable é a regra "credential_guard_hook_resolvable" — ver
|
|
1322
|
+
// validateGuardHookResolvable para a implementação compartilhada.
|
|
1323
|
+
function validateCredentialGuardHookResolvable(cwd) {
|
|
1324
|
+
return validateGuardHookResolvable(CREDENTIAL_GUARD_SCRIPT_MARKER, cwd)
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// validateGitBranchGuardHookResolvable é a regra "git_branch_guard_hook_resolvable" — ver
|
|
1328
|
+
// validateGuardHookResolvable para a implementação compartilhada.
|
|
1329
|
+
function validateGitBranchGuardHookResolvable(cwd) {
|
|
1330
|
+
return validateGuardHookResolvable(GIT_BRANCH_GUARD_SCRIPT_MARKER, cwd)
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1287
1333
|
function isGitWorktree(dir) {
|
|
1288
1334
|
try {
|
|
1289
1335
|
const out = gitOutput(dir, ['rev-parse', '--is-inside-work-tree'])
|
|
@@ -1486,6 +1532,511 @@ function validateCredentialGuardScriptIntegrity(cwd) {
|
|
|
1486
1532
|
]
|
|
1487
1533
|
}
|
|
1488
1534
|
|
|
1535
|
+
// ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
|
|
1536
|
+
// ML-2A: port of internal/validator/validator_git_branch_guard.go — adds git-branch-guard
|
|
1537
|
+
// coverage to the two existing credential-guard checks (existence/executability via
|
|
1538
|
+
// validateGuardHookResolvable above, and content-drift integrity via
|
|
1539
|
+
// validateGitBranchGuardScriptIntegrity below), plus the GLOBAL-scope check that was missing for
|
|
1540
|
+
// BOTH guards before this ML (same gap the Go implementation closed first).
|
|
1541
|
+
|
|
1542
|
+
// GBG_REF_BACKTICK — validator-local copy of the same escaped-backtick helper hooks.js's
|
|
1543
|
+
// GBG_BACKTICK uses (npm/src/generators/hooks.js), to embed REASON's `` `cmd` `` inside the
|
|
1544
|
+
// script's double-quoted shell string without breaking this JS template literal.
|
|
1545
|
+
const GBG_REF_BACKTICK = '\\`'
|
|
1546
|
+
|
|
1547
|
+
// GIT_BRANCH_GUARD_SCRIPT_REFERENCE is a validator-local copy of the
|
|
1548
|
+
// scripts/trackfw-git-branch-guard.sh template composed in npm/src/generators/hooks.js
|
|
1549
|
+
// (GIT_BRANCH_GUARD_SCRIPT const). Not required directly from generators/hooks.js: like
|
|
1550
|
+
// CREDENTIAL_GUARD_SCRIPT_REFERENCE above, generateGitBranchGuardScript() writes to disk AND
|
|
1551
|
+
// console.log()s a success line on every call — calling it from inside `trackfw validate` would
|
|
1552
|
+
// leak a stray " ✓ scripts/trackfw-git-branch-guard.sh" line into validate's output. There is no
|
|
1553
|
+
// import-cycle constraint in Node (unlike Go's internal/validator ↔ internal/generators cycle —
|
|
1554
|
+
// see gitBranchGuardScriptReference's doc comment in
|
|
1555
|
+
// internal/validator/validator_git_branch_guard_reference.go) but the console.log side effect is
|
|
1556
|
+
// reason enough on its own to keep a local copy here, matching the choice already made for
|
|
1557
|
+
// credential-guard in this same file.
|
|
1558
|
+
//
|
|
1559
|
+
// Unlike CREDENTIAL_GUARD_SCRIPT_REFERENCE, GIT_BRANCH_GUARD_SCRIPT_REFERENCE is used VERBATIM for
|
|
1560
|
+
// both the project scope and the global scope (generateGitBranchGuardScript /
|
|
1561
|
+
// generateGlobalGitBranchGuardScript write the exact same GIT_BRANCH_GUARD_SCRIPT constant) — so
|
|
1562
|
+
// this single reference constant covers both git_branch_guard_script_integrity (project,
|
|
1563
|
+
// scripts/trackfw-git-branch-guard.sh) and the global integrity check
|
|
1564
|
+
// (~/.trackfw/scripts/trackfw-git-branch-guard.sh), no second reference constant needed.
|
|
1565
|
+
//
|
|
1566
|
+
// Drift between this copy and the real generator is caught by
|
|
1567
|
+
// "GIT_BRANCH_GUARD_SCRIPT_REFERENCE é byte-idêntico ao que generateGitBranchGuardScript emite"
|
|
1568
|
+
// (npm/tests/git_branch_guard.test.js): it regenerates the script via generateGitBranchGuardScript
|
|
1569
|
+
// into a temp dir and asserts byte-equality against this constant.
|
|
1570
|
+
const GIT_BRANCH_GUARD_SCRIPT_REFERENCE = `#!/usr/bin/env bash
|
|
1571
|
+
# trackfw git branch guard — bloqueia git commit/push/checkout -b brutos por subagente
|
|
1572
|
+
set -euo pipefail
|
|
1573
|
+
set -f
|
|
1574
|
+
|
|
1575
|
+
# --- 1. Obter o comando git bruto ------------------------------------------------------------
|
|
1576
|
+
if [ "$#" -gt 0 ]; then
|
|
1577
|
+
CMD_RAW="$*"
|
|
1578
|
+
else
|
|
1579
|
+
INPUT=$(cat 2>/dev/null || true)
|
|
1580
|
+
TRIMMED=$(printf '%s' "$INPUT" | sed -e 's/^[[:space:]]*//')
|
|
1581
|
+
case "$TRIMMED" in
|
|
1582
|
+
\\{*)
|
|
1583
|
+
CMD_RAW=""
|
|
1584
|
+
if command -v jq >/dev/null 2>&1; then
|
|
1585
|
+
CMD_RAW=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // .command // .tool_info.command_line // .hook_input.command // empty' 2>/dev/null || true)
|
|
1586
|
+
fi
|
|
1587
|
+
if [ -z "$CMD_RAW" ] || [ "$CMD_RAW" = "null" ]; then
|
|
1588
|
+
CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"tool_input"[[:space:]]*:[[:space:]]*{[^}]*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
|
|
1589
|
+
fi
|
|
1590
|
+
if [ -z "$CMD_RAW" ]; then
|
|
1591
|
+
CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"tool_info"[[:space:]]*:[[:space:]]*{[^}]*"command_line"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
|
|
1592
|
+
fi
|
|
1593
|
+
if [ -z "$CMD_RAW" ]; then
|
|
1594
|
+
CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"hook_input"[[:space:]]*:[[:space:]]*{[^}]*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
|
|
1595
|
+
fi
|
|
1596
|
+
if [ -z "$CMD_RAW" ]; then
|
|
1597
|
+
CMD_RAW=$(printf '%s' "$INPUT" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p' | head -1)
|
|
1598
|
+
fi
|
|
1599
|
+
;;
|
|
1600
|
+
*)
|
|
1601
|
+
CMD_RAW="$INPUT"
|
|
1602
|
+
;;
|
|
1603
|
+
esac
|
|
1604
|
+
fi
|
|
1605
|
+
|
|
1606
|
+
if [ -z "$CMD_RAW" ]; then
|
|
1607
|
+
CMD_RAW="\${TRACKFW_GIT_COMMAND:-}"
|
|
1608
|
+
fi
|
|
1609
|
+
|
|
1610
|
+
[ -n "$CMD_RAW" ] || exit 0
|
|
1611
|
+
|
|
1612
|
+
# --- 2. Casar contra "git (commit|push|checkout -b)", segmento por segmento -----------------
|
|
1613
|
+
# Cada segmento é um comando real (dividido por ; && || | e por quebra de linha). "git" só
|
|
1614
|
+
# conta se for o PRIMEIRO token do segmento (por basename, então /usr/bin/git também casa) —
|
|
1615
|
+
# nunca uma ocorrência solta em qualquer posição da string inteira. Isso evita: (a) o segundo
|
|
1616
|
+
# comando de uma cadeia escapar da checagem, (b) um path absoluto para o git escapar por
|
|
1617
|
+
# comparação de igualdade exata, e (c) texto de prosa (ex.: mensagem de commit mencionando
|
|
1618
|
+
# "git commit" no meio de uma frase) ser tratado como comando.
|
|
1619
|
+
match_subcommand() {
|
|
1620
|
+
normalized=$(printf '%s' "$1" | sed -e 's/&&/\\n/g' -e 's/||/\\n/g' -e 's/[;|]/\\n/g')
|
|
1621
|
+
while IFS= read -r seg; do
|
|
1622
|
+
seg_trimmed=$(printf '%s' "$seg" | sed -e 's/^[[:space:]]*//')
|
|
1623
|
+
[ -n "$seg_trimmed" ] || continue
|
|
1624
|
+
|
|
1625
|
+
set -- $seg_trimmed
|
|
1626
|
+
first="$1"
|
|
1627
|
+
base="\${first##*/}"
|
|
1628
|
+
[ "$base" = "git" ] || continue
|
|
1629
|
+
shift
|
|
1630
|
+
|
|
1631
|
+
sub=""
|
|
1632
|
+
while [ "$#" -gt 0 ]; do
|
|
1633
|
+
tok="$1"
|
|
1634
|
+
case "$tok" in
|
|
1635
|
+
-C|-c|--work-tree|--git-dir|--namespace)
|
|
1636
|
+
if [ "$#" -ge 2 ]; then shift 2; else shift; fi
|
|
1637
|
+
continue
|
|
1638
|
+
;;
|
|
1639
|
+
-*)
|
|
1640
|
+
shift
|
|
1641
|
+
continue
|
|
1642
|
+
;;
|
|
1643
|
+
*)
|
|
1644
|
+
sub="$tok"
|
|
1645
|
+
shift
|
|
1646
|
+
break
|
|
1647
|
+
;;
|
|
1648
|
+
esac
|
|
1649
|
+
done
|
|
1650
|
+
|
|
1651
|
+
case "$sub" in
|
|
1652
|
+
commit)
|
|
1653
|
+
echo "commit"
|
|
1654
|
+
return 0
|
|
1655
|
+
;;
|
|
1656
|
+
push)
|
|
1657
|
+
echo "push"
|
|
1658
|
+
return 0
|
|
1659
|
+
;;
|
|
1660
|
+
checkout)
|
|
1661
|
+
if [ "\${1:-}" = "-b" ]; then
|
|
1662
|
+
echo "checkout-b"
|
|
1663
|
+
return 0
|
|
1664
|
+
fi
|
|
1665
|
+
;;
|
|
1666
|
+
esac
|
|
1667
|
+
done <<EOF
|
|
1668
|
+
$normalized
|
|
1669
|
+
EOF
|
|
1670
|
+
return 1
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
SUBCOMMAND=$(match_subcommand "$CMD_RAW") || exit 0
|
|
1674
|
+
|
|
1675
|
+
case "$SUBCOMMAND" in
|
|
1676
|
+
checkout-b)
|
|
1677
|
+
REASON="trackfw: git checkout -b bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw branch new <type>/<slug>` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
|
|
1678
|
+
;;
|
|
1679
|
+
commit)
|
|
1680
|
+
REASON="trackfw: git commit bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw commit -m '<mensagem>'` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
|
|
1681
|
+
;;
|
|
1682
|
+
push)
|
|
1683
|
+
REASON="trackfw: git push bruto bloqueado. Use ` + GBG_REF_BACKTICK + `trackfw ship` + GBG_REF_BACKTICK + `. Ver CLAUDE.md §1."
|
|
1684
|
+
;;
|
|
1685
|
+
*)
|
|
1686
|
+
exit 0
|
|
1687
|
+
;;
|
|
1688
|
+
esac
|
|
1689
|
+
|
|
1690
|
+
printf '{"decision":"block","reason":"%s"}\\n' "$REASON"
|
|
1691
|
+
echo "$REASON" >&2
|
|
1692
|
+
exit 2
|
|
1693
|
+
`
|
|
1694
|
+
|
|
1695
|
+
// validateGitBranchGuardScriptIntegrity é a regra "git_branch_guard_script_integrity": compara
|
|
1696
|
+
// scripts/trackfw-git-branch-guard.sh em disco contra o template que esta versão do trackfw
|
|
1697
|
+
// geraria. Espelha validateCredentialGuardScriptIntegrity exatamente — mesmo contrato de silêncio
|
|
1698
|
+
// na ausência (existência é responsabilidade de git_branch_guard_hook_resolvable, não desta regra).
|
|
1699
|
+
function validateGitBranchGuardScriptIntegrity(cwd) {
|
|
1700
|
+
const root = cwd || process.cwd()
|
|
1701
|
+
const relPath = 'scripts/trackfw-git-branch-guard.sh'
|
|
1702
|
+
let content
|
|
1703
|
+
try {
|
|
1704
|
+
content = fs.readFileSync(path.join(root, relPath), 'utf8')
|
|
1705
|
+
} catch (e) {
|
|
1706
|
+
if (e.code === 'ENOENT') return []
|
|
1707
|
+
return [inspectionDiagnostic('git_branch_guard_script_integrity', relPath, e)]
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
if (content === GIT_BRANCH_GUARD_SCRIPT_REFERENCE) return []
|
|
1711
|
+
|
|
1712
|
+
return [
|
|
1713
|
+
`${relPath} content diverges from the template this version of trackfw generates — ` +
|
|
1714
|
+
'if you did not edit this file by hand, run `trackfw update` to regenerate it',
|
|
1715
|
+
]
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE is a validator-local copy of the GLOBAL-scope
|
|
1719
|
+
// ~/.trackfw/scripts/trackfw-credential-guard.sh template composed in npm/src/generators/hooks.js
|
|
1720
|
+
// (GLOBAL_CREDENTIAL_GUARD_SCRIPT = CG_HEADER + CG_DETECTION_CORE + CG_GLOBAL_TAIL). This is a
|
|
1721
|
+
// DIFFERENT template than CREDENTIAL_GUARD_SCRIPT_REFERENCE (the project-scope variant): the
|
|
1722
|
+
// global variant omits the project-guard block ("no-op outside a trackfw.yaml project") and
|
|
1723
|
+
// defaults credential_guard.mode to "block" instead of "warn" — mirrors Go's
|
|
1724
|
+
// credentialGuardGlobalScriptReference
|
|
1725
|
+
// (internal/validator/validator_credential_guard_global_reference.go). Comparing the global
|
|
1726
|
+
// on-disk script against CREDENTIAL_GUARD_SCRIPT_REFERENCE (the project template) would be a
|
|
1727
|
+
// guaranteed false positive for every user with the global harness installed.
|
|
1728
|
+
const CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE = `#!/usr/bin/env bash
|
|
1729
|
+
# trackfw credential guard — PreToolUse/PostToolUse hook
|
|
1730
|
+
set -euo pipefail
|
|
1731
|
+
|
|
1732
|
+
INPUT=$(cat)
|
|
1733
|
+
|
|
1734
|
+
JWT_PATTERN='eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+'
|
|
1735
|
+
AWS_KEY_PATTERN='AKIA[0-9A-Z]{16}'
|
|
1736
|
+
|
|
1737
|
+
MATCH=""
|
|
1738
|
+
if printf '%s' "$INPUT" | grep -qE "$JWT_PATTERN"; then
|
|
1739
|
+
MATCH="JWT"
|
|
1740
|
+
elif printf '%s' "$INPUT" | grep -qE "$AWS_KEY_PATTERN"; then
|
|
1741
|
+
MATCH="AWS access key"
|
|
1742
|
+
fi
|
|
1743
|
+
|
|
1744
|
+
# The raw payload is JSON: any double quote inside the underlying tool_input.command is
|
|
1745
|
+
# escaped as \\" -- unescape those before scanning for redirect targets, or a quoted target
|
|
1746
|
+
# like "$TMPFILE" is seen as starting with a literal backslash instead of a variable
|
|
1747
|
+
# reference.
|
|
1748
|
+
RAW=$(printf '%s' "$INPUT" | sed 's/\\\\"/"/g')
|
|
1749
|
+
|
|
1750
|
+
# Ignore matches that are only ever written to an ephemeral destination
|
|
1751
|
+
# (mktemp-derived path or /dev/null). A match with no redirect at all
|
|
1752
|
+
# (printed to stdout, e.g.) or redirected to a plain file path still
|
|
1753
|
+
# alerts -- that is the incident this hook guards against.
|
|
1754
|
+
is_ephemeral_target() {
|
|
1755
|
+
local target
|
|
1756
|
+
target=$(printf '%s' "$1" | tr -d "\\"'" | sed -E 's/[},]+$//')
|
|
1757
|
+
case "$target" in
|
|
1758
|
+
/dev/null) return 0 ;;
|
|
1759
|
+
*mktemp*) return 0 ;;
|
|
1760
|
+
esac
|
|
1761
|
+
if printf '%s' "$target" | grep -qE '^\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?$'; then
|
|
1762
|
+
local varname pattern
|
|
1763
|
+
varname=$(printf '%s' "$target" | sed -E 's/^\\$\\{?([A-Za-z_][A-Za-z0-9_]*)\\}?$/\\1/')
|
|
1764
|
+
pattern="*\${varname}="'$(mktemp'"*"
|
|
1765
|
+
case "$RAW" in
|
|
1766
|
+
$pattern) return 0 ;;
|
|
1767
|
+
esac
|
|
1768
|
+
fi
|
|
1769
|
+
return 1
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
REDIRECTS=$(printf '%s' "$RAW" | grep -oE '[0-9]?>>?[[:space:]]*[^[:space:]|&;,:]+' || true)
|
|
1773
|
+
|
|
1774
|
+
# Second detection layer: only runs when the payload scan above found nothing -- keeps the common
|
|
1775
|
+
# case (match already found) cheap and avoids reading files unnecessarily. Files above the size cap
|
|
1776
|
+
# are skipped silently.
|
|
1777
|
+
scan_file_for_pattern() {
|
|
1778
|
+
local path size
|
|
1779
|
+
path=$(printf '%s' "$1" | tr -d "\\"'" | sed -E 's/[},]+$//')
|
|
1780
|
+
[ -n "$path" ] && [ -f "$path" ] || return 1
|
|
1781
|
+
size=$(wc -c < "$path" 2>/dev/null | tr -d '[:space:]')
|
|
1782
|
+
size=\${size:-0}
|
|
1783
|
+
[ "$size" -lt 1048576 ] || return 1
|
|
1784
|
+
if grep -qE "$JWT_PATTERN" "$path" 2>/dev/null; then
|
|
1785
|
+
MATCH="JWT"
|
|
1786
|
+
return 0
|
|
1787
|
+
fi
|
|
1788
|
+
if grep -qE "$AWS_KEY_PATTERN" "$path" 2>/dev/null; then
|
|
1789
|
+
MATCH="AWS access key"
|
|
1790
|
+
return 0
|
|
1791
|
+
fi
|
|
1792
|
+
return 1
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
if [ -z "$MATCH" ] && [ -n "$REDIRECTS" ]; then
|
|
1796
|
+
while IFS= read -r line; do
|
|
1797
|
+
if [ -z "$line" ]; then
|
|
1798
|
+
continue
|
|
1799
|
+
fi
|
|
1800
|
+
target=$(printf '%s' "$line" | sed -E 's/^[0-9]?>>?[[:space:]]*//')
|
|
1801
|
+
if ! is_ephemeral_target "$target"; then
|
|
1802
|
+
scan_file_for_pattern "$target" && break
|
|
1803
|
+
fi
|
|
1804
|
+
done <<< "$REDIRECTS"
|
|
1805
|
+
fi
|
|
1806
|
+
|
|
1807
|
+
if [ -z "$MATCH" ]; then
|
|
1808
|
+
CMD_LINE=$(printf '%s' "$RAW" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
|
|
1809
|
+
if [ -n "$CMD_LINE" ]; then
|
|
1810
|
+
set -- $CMD_LINE
|
|
1811
|
+
cmd_name="\${1:-}"
|
|
1812
|
+
case "$cmd_name" in
|
|
1813
|
+
cat|head|tail|jq|grep)
|
|
1814
|
+
shift
|
|
1815
|
+
for token in "$@"; do
|
|
1816
|
+
scan_file_for_pattern "$token" && break
|
|
1817
|
+
done
|
|
1818
|
+
;;
|
|
1819
|
+
esac
|
|
1820
|
+
fi
|
|
1821
|
+
fi
|
|
1822
|
+
|
|
1823
|
+
[ -n "$MATCH" ] || exit 0
|
|
1824
|
+
|
|
1825
|
+
HAS_REDIRECT=0
|
|
1826
|
+
EXEMPT=1
|
|
1827
|
+
if [ -n "$REDIRECTS" ]; then
|
|
1828
|
+
while IFS= read -r line; do
|
|
1829
|
+
if [ -z "$line" ]; then
|
|
1830
|
+
continue
|
|
1831
|
+
fi
|
|
1832
|
+
HAS_REDIRECT=1
|
|
1833
|
+
target=$(printf '%s' "$line" | sed -E 's/^[0-9]?>>?[[:space:]]*//')
|
|
1834
|
+
if ! is_ephemeral_target "$target"; then
|
|
1835
|
+
EXEMPT=0
|
|
1836
|
+
fi
|
|
1837
|
+
done <<< "$REDIRECTS"
|
|
1838
|
+
fi
|
|
1839
|
+
|
|
1840
|
+
if [ "$HAS_REDIRECT" -eq 1 ] && [ "$EXEMPT" -eq 1 ]; then
|
|
1841
|
+
exit 0
|
|
1842
|
+
fi
|
|
1843
|
+
|
|
1844
|
+
DEFAULT_MODE="block"
|
|
1845
|
+
MODE=$(grep -A 5 '^credential_guard:' trackfw.yaml 2>/dev/null | grep 'mode:' | head -1 | sed -E 's/^[[:space:]]*mode:[[:space:]]*//; s/[[:space:]]*#.*$//' | tr -d "\\"'" || true)
|
|
1846
|
+
case "$MODE" in
|
|
1847
|
+
warn|block) ;;
|
|
1848
|
+
*) MODE="$DEFAULT_MODE" ;;
|
|
1849
|
+
esac
|
|
1850
|
+
|
|
1851
|
+
if [ "$MODE" = "block" ]; then
|
|
1852
|
+
echo "trackfw-credential-guard: blocked - possible $MATCH detected in tool payload." >&2
|
|
1853
|
+
exit 2
|
|
1854
|
+
fi
|
|
1855
|
+
|
|
1856
|
+
echo "trackfw-credential-guard: warning - possible $MATCH detected in tool payload." >&2
|
|
1857
|
+
|
|
1858
|
+
ROADMAP_DIR="docs/roadmaps"
|
|
1859
|
+
if [ ! -d "$ROADMAP_DIR" ]; then
|
|
1860
|
+
exit 0
|
|
1861
|
+
fi
|
|
1862
|
+
|
|
1863
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1864
|
+
MSG="Possible $MATCH detected in tool payload - review before materializing credentials in plain text."
|
|
1865
|
+
MSG_ESC=$(echo "$MSG" | tr -d '\\000-\\037' | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g')
|
|
1866
|
+
|
|
1867
|
+
mkdir -p "$ROADMAP_DIR"
|
|
1868
|
+
printf '{"tool":"credential-guard","message":"%s","level":"action_required","timestamp":"%s"}\\n' \\
|
|
1869
|
+
"$MSG_ESC" \\
|
|
1870
|
+
"$TIMESTAMP" > "$ROADMAP_DIR/.trackfw-credential-guard.json"
|
|
1871
|
+
|
|
1872
|
+
exit 0
|
|
1873
|
+
`
|
|
1874
|
+
|
|
1875
|
+
// globalGuardConfigFile associates a GLOBAL (per-CLI, $HOME-rooted) hook/settings config file with
|
|
1876
|
+
// the CLI that consumes it, for the global-scope guard checks below. Distinct from
|
|
1877
|
+
// CREDENTIAL_GUARD_HOOK_FILES, whose relPath is rooted at the PROJECT root, not $HOME.
|
|
1878
|
+
//
|
|
1879
|
+
// GLOBAL_GUARD_CONFIG_FILES is the closed list of GLOBAL hook/settings files `trackfw update
|
|
1880
|
+
// harness` can write a guard entry into — the global-scope counterpart of
|
|
1881
|
+
// CREDENTIAL_GUARD_HOOK_FILES. Paths and CLI labels ported from Go's globalGuardConfigFiles
|
|
1882
|
+
// (internal/validator/validator_git_branch_guard.go) — note this list DIFFERS from
|
|
1883
|
+
// CREDENTIAL_GUARD_HOOK_FILES for Copilot and Kiro (global scope uses .copilot/settings.json and
|
|
1884
|
+
// .kiro/hooks/trackfw-credential-guard.json, not the project-scope attention-hook paths).
|
|
1885
|
+
const GLOBAL_GUARD_CONFIG_FILES = [
|
|
1886
|
+
{ relPath: '.claude/settings.json', cli: 'Claude Code' },
|
|
1887
|
+
{ relPath: '.codex/hooks.json', cli: 'Codex CLI' },
|
|
1888
|
+
{ relPath: '.gemini/settings.json', cli: 'Gemini CLI' },
|
|
1889
|
+
{ relPath: '.cursor/hooks.json', cli: 'Cursor' },
|
|
1890
|
+
{ relPath: '.copilot/settings.json', cli: 'GitHub Copilot CLI' },
|
|
1891
|
+
{ relPath: '.kiro/hooks/trackfw-credential-guard.json', cli: 'Kiro' },
|
|
1892
|
+
]
|
|
1893
|
+
|
|
1894
|
+
// validateGuardGlobalHookResolvable is the GLOBAL-scope counterpart of
|
|
1895
|
+
// validateGuardHookResolvable: for each of the 6 GLOBAL_GUARD_CONFIG_FILES that exists AND
|
|
1896
|
+
// references scriptMarker, verifies the referenced script exists and is executable. Port of Go's
|
|
1897
|
+
// validateGuardGlobalHookResolvable (internal/validator/validator_git_branch_guard.go) — see that
|
|
1898
|
+
// function's doc comment for the full trigger-condition and fail-open rationale.
|
|
1899
|
+
//
|
|
1900
|
+
// Global entries are written by npm/src/generators (harnessCredentialGuardTarget*-equivalent code
|
|
1901
|
+
// in agentfiles-equivalent generators) as fully resolved absolute paths, never a placeholder like
|
|
1902
|
+
// $CLAUDE_PROJECT_DIR — so, unlike the project-scope resolveCredentialGuardHookPath, no
|
|
1903
|
+
// prefix-stripping is needed here: any matched command that is NOT already an absolute path is
|
|
1904
|
+
// skipped (never treated as a violation).
|
|
1905
|
+
//
|
|
1906
|
+
// Fail-open: unresolvable $HOME, unreadable file, or invalid JSON all skip that file in silence —
|
|
1907
|
+
// same contract validateGuardHookResolvable already has for project-scope files.
|
|
1908
|
+
function validateGuardGlobalHookResolvable(scriptMarker) {
|
|
1909
|
+
const home = os.homedir()
|
|
1910
|
+
if (!home) return []
|
|
1911
|
+
|
|
1912
|
+
const msgs = []
|
|
1913
|
+
for (const gf of GLOBAL_GUARD_CONFIG_FILES) {
|
|
1914
|
+
const fullPath = path.join(home, gf.relPath)
|
|
1915
|
+
let content
|
|
1916
|
+
try {
|
|
1917
|
+
content = fs.readFileSync(fullPath, 'utf8')
|
|
1918
|
+
} catch (e) {
|
|
1919
|
+
if (e.code === 'ENOENT') continue
|
|
1920
|
+
continue
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
let parsed
|
|
1924
|
+
try {
|
|
1925
|
+
parsed = JSON.parse(content)
|
|
1926
|
+
} catch (_) {
|
|
1927
|
+
continue
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const commands = []
|
|
1931
|
+
collectCommandsWithMarker(parsed, scriptMarker, commands)
|
|
1932
|
+
|
|
1933
|
+
const seen = new Set()
|
|
1934
|
+
for (const raw of commands) {
|
|
1935
|
+
if (seen.has(raw)) continue
|
|
1936
|
+
seen.add(raw)
|
|
1937
|
+
|
|
1938
|
+
if (!path.isAbsolute(raw)) continue
|
|
1939
|
+
|
|
1940
|
+
let stat = null
|
|
1941
|
+
try {
|
|
1942
|
+
stat = fs.statSync(raw)
|
|
1943
|
+
} catch (_) {
|
|
1944
|
+
stat = null
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
if (!stat) {
|
|
1948
|
+
msgs.push(`~/${gf.relPath} (${gf.cli}, global scope) references ${scriptMarker} resolved to "${raw}", but the script does not exist — run \`trackfw update harness\` to regenerate it`)
|
|
1949
|
+
} else if ((stat.mode & 0o111) === 0) {
|
|
1950
|
+
msgs.push(`~/${gf.relPath} (${gf.cli}, global scope) references ${scriptMarker} resolved to "${raw}", but the script is not executable — run \`trackfw update harness\` to regenerate it`)
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
return msgs
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// validateGuardGlobalScriptIntegrity is the GLOBAL-scope counterpart of
|
|
1959
|
+
// validateCredentialGuardScriptIntegrity/validateGitBranchGuardScriptIntegrity: for each of the 6
|
|
1960
|
+
// GLOBAL_GUARD_CONFIG_FILES that references scriptMarker, verifies the referenced script's content
|
|
1961
|
+
// matches referenceContent byte-for-byte. Port of Go's validateGuardGlobalScriptIntegrity
|
|
1962
|
+
// (internal/validator/validator_git_branch_guard.go) — same trigger condition and fail-open
|
|
1963
|
+
// contract as validateGuardGlobalHookResolvable above; does not re-validate existence/
|
|
1964
|
+
// executability, only content, matching the project-scope split between *_hook_resolvable and
|
|
1965
|
+
// *_script_integrity.
|
|
1966
|
+
function validateGuardGlobalScriptIntegrity(scriptMarker, referenceContent) {
|
|
1967
|
+
const home = os.homedir()
|
|
1968
|
+
if (!home) return []
|
|
1969
|
+
|
|
1970
|
+
const msgs = []
|
|
1971
|
+
for (const gf of GLOBAL_GUARD_CONFIG_FILES) {
|
|
1972
|
+
const fullPath = path.join(home, gf.relPath)
|
|
1973
|
+
let content
|
|
1974
|
+
try {
|
|
1975
|
+
content = fs.readFileSync(fullPath, 'utf8')
|
|
1976
|
+
} catch (e) {
|
|
1977
|
+
if (e.code === 'ENOENT') continue
|
|
1978
|
+
continue
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
let parsed
|
|
1982
|
+
try {
|
|
1983
|
+
parsed = JSON.parse(content)
|
|
1984
|
+
} catch (_) {
|
|
1985
|
+
continue
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
const commands = []
|
|
1989
|
+
collectCommandsWithMarker(parsed, scriptMarker, commands)
|
|
1990
|
+
|
|
1991
|
+
const seen = new Set()
|
|
1992
|
+
for (const raw of commands) {
|
|
1993
|
+
if (seen.has(raw)) continue
|
|
1994
|
+
seen.add(raw)
|
|
1995
|
+
|
|
1996
|
+
if (!path.isAbsolute(raw)) continue
|
|
1997
|
+
|
|
1998
|
+
let scriptContent
|
|
1999
|
+
try {
|
|
2000
|
+
scriptContent = fs.readFileSync(raw, 'utf8')
|
|
2001
|
+
} catch (_) {
|
|
2002
|
+
// Existence/readability is validateGuardGlobalHookResolvable's job — do not double-report
|
|
2003
|
+
// the same underlying condition under two rule names.
|
|
2004
|
+
continue
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
if (scriptContent === referenceContent) continue
|
|
2008
|
+
|
|
2009
|
+
msgs.push(`${raw} (global scope, referenced from ~/${gf.relPath}, ${gf.cli}) content diverges from the template this version of trackfw generates — if you did not edit this file by hand, run \`trackfw update harness\` to regenerate it`)
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
return msgs
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// validateCredentialGuardGlobalHookResolvable / validateCredentialGuardGlobalScriptIntegrity /
|
|
2017
|
+
// validateGitBranchGuardGlobalHookResolvable / validateGitBranchGuardGlobalScriptIntegrity are the
|
|
2018
|
+
// 4 thin wrappers wired in validateUnfiltered below — each folds its messages into the SAME rule
|
|
2019
|
+
// name as its project-scope counterpart (credential_guard_hook_resolvable,
|
|
2020
|
+
// credential_guard_script_integrity, git_branch_guard_hook_resolvable,
|
|
2021
|
+
// git_branch_guard_script_integrity respectively), so no new rules: entries in trackfw.yaml are
|
|
2022
|
+
// needed. Port of the 4 equivalent wrappers in
|
|
2023
|
+
// internal/validator/validator_git_branch_guard.go.
|
|
2024
|
+
function validateCredentialGuardGlobalHookResolvable() {
|
|
2025
|
+
return validateGuardGlobalHookResolvable(CREDENTIAL_GUARD_SCRIPT_MARKER)
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function validateCredentialGuardGlobalScriptIntegrity() {
|
|
2029
|
+
return validateGuardGlobalScriptIntegrity(CREDENTIAL_GUARD_SCRIPT_MARKER, CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE)
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
function validateGitBranchGuardGlobalHookResolvable() {
|
|
2033
|
+
return validateGuardGlobalHookResolvable(GIT_BRANCH_GUARD_SCRIPT_MARKER)
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function validateGitBranchGuardGlobalScriptIntegrity() {
|
|
2037
|
+
return validateGuardGlobalScriptIntegrity(GIT_BRANCH_GUARD_SCRIPT_MARKER, GIT_BRANCH_GUARD_SCRIPT_REFERENCE)
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1489
2040
|
// CREDENTIAL_GUARD_MODE_LOOKBEHIND_LINES mirrors the shell script's own resolution of
|
|
1490
2041
|
// credential_guard.mode (`grep -A 5 '^credential_guard:'`): the mode key is found on the matched
|
|
1491
2042
|
// line or within the 5 lines following it.
|
|
@@ -1600,6 +2151,12 @@ const RULE_DEFAULTS = {
|
|
|
1600
2151
|
// credential_guard_mode_downgrade is deliberately absent: it falls through to ruleSeverity's
|
|
1601
2152
|
// 'error' default.
|
|
1602
2153
|
credential_guard_script_integrity: 'warning',
|
|
2154
|
+
// ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
|
|
2155
|
+
// ML-2A: same rationale as credential_guard_script_integrity above — the script carries no
|
|
2156
|
+
// version marker, so this rule cannot tell legitimate drift from tampering.
|
|
2157
|
+
// git_branch_guard_hook_resolvable is deliberately absent from this map (falls through to
|
|
2158
|
+
// 'error'), mirroring credential_guard_hook_resolvable.
|
|
2159
|
+
git_branch_guard_script_integrity: 'warning',
|
|
1603
2160
|
}
|
|
1604
2161
|
|
|
1605
2162
|
// ROADMAP-2026-08-12-ancorar-rules-no-head-para-as-regras-de-credential-guard, ML-1A.
|
|
@@ -1719,6 +2276,122 @@ function saveBaseline(violations, warnings) {
|
|
|
1719
2276
|
fs.writeFileSync(BASELINE_FILE, JSON.stringify(bf, null, 2), 'utf8')
|
|
1720
2277
|
}
|
|
1721
2278
|
|
|
2279
|
+
function sha256Hex(buf) {
|
|
2280
|
+
return crypto.createHash('sha256').update(buf).digest('hex')
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// THIRDPARTY_ORIGIN é o valor de Claim.origin que marca um artefato do manifest como instalado
|
|
2284
|
+
// por `third-party install` (ADR-2026-08-15 D11).
|
|
2285
|
+
const THIRDPARTY_ORIGIN = 'thirdparty'
|
|
2286
|
+
|
|
2287
|
+
// validateThirdPartyArtifactHasProvenance implementa a regra "thirdparty_artifact_has_provenance"
|
|
2288
|
+
// (ADR-2026-08-15 D2) — a detecção real, ancorada em git, por trás do guardrail
|
|
2289
|
+
// TRACKFW_ORCHESTRATOR_SESSION. NUNCA faz fetch de rede (D6): lê só
|
|
2290
|
+
// .trackfw/integrations-manifest.json, .trackfw/thirdparty-provenance.json e
|
|
2291
|
+
// .trackfw/thirdparty-quarantine/<checksum>.json, todos já em disco (e, por convenção deste
|
|
2292
|
+
// projeto, versionados no repositório).
|
|
2293
|
+
//
|
|
2294
|
+
// Duas ramificações, ambas fatais (error — a regra está deliberadamente ausente de RULE_DEFAULTS):
|
|
2295
|
+
// 1. um artifact do manifest carrega um claim com origin === "thirdparty" mas
|
|
2296
|
+
// thirdparty-provenance.json não tem entrada chaveada por aquele destino;
|
|
2297
|
+
// 2. existe entrada de proveniência, mas seu installed_sha256 não pode ser reconciliado com o que
|
|
2298
|
+
// de fato está em disco no destino declarado.
|
|
2299
|
+
//
|
|
2300
|
+
// Sobre a ramificação 2 (ADR-2026-08-15 D2-bis, ML-3B) — checksum_sha256 é sha256 dos bytes BRUTOS
|
|
2301
|
+
// (D6), mas o arquivo instalado é sempre NormalizeThirdPartyContent(raw) — não é a função
|
|
2302
|
+
// identidade em geral, então comparar checksum_sha256 direto contra sha256(arquivo instalado)
|
|
2303
|
+
// produz falso-positivo em toda instalação legítima cujo conteúdo bruto não fosse já exatamente
|
|
2304
|
+
// TrimSpace+newline único. A resolução ML-3A usava o registro de quarentena como ponte entre os
|
|
2305
|
+
// dois domínios — correta, mas tornava um artefato de ESTÁGIO (.trackfw/thirdparty-quarantine/,
|
|
2306
|
+
// destinado a ser podado) dependência obrigatória de um gate PERMANENTE. D2-bis resolve isso com
|
|
2307
|
+
// um segundo campo na entrada de proveniência, installed_sha256 = sha256(bytes NORMALIZADOS),
|
|
2308
|
+
// calculado no momento do install pelo mesmo código que grava o arquivo (npm/src/commands/thirdparty.js).
|
|
2309
|
+
// checksum_sha256 permanece intocado, é a âncora de aprovação D8c. A ramificação 2 agora compara
|
|
2310
|
+
// sha256(arquivo instalado) diretamente contra entry.installed_sha256 — dois domínios já
|
|
2311
|
+
// normalizados, sem ponte via quarentena. A ausência da quarentena deixou de ser erro desta regra.
|
|
2312
|
+
function validateThirdPartyArtifactHasProvenance(cwd) {
|
|
2313
|
+
const root = cwd || process.cwd()
|
|
2314
|
+
|
|
2315
|
+
const manifestPath = path.join(root, '.trackfw', 'integrations-manifest.json')
|
|
2316
|
+
let manifest
|
|
2317
|
+
try {
|
|
2318
|
+
const raw = fs.readFileSync(manifestPath, 'utf8')
|
|
2319
|
+
manifest = JSON.parse(raw)
|
|
2320
|
+
} catch (err) {
|
|
2321
|
+
if (err.code === 'ENOENT') return []
|
|
2322
|
+
throw new Error(`thirdparty_artifact_has_provenance: read ${manifestPath}: ${err.message}`)
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
const destinations = []
|
|
2326
|
+
for (const [destination, artifact] of Object.entries(manifest.artifacts || {})) {
|
|
2327
|
+
const claims = artifact.claims || []
|
|
2328
|
+
if (claims.some(claim => claim.origin === THIRDPARTY_ORIGIN)) destinations.push(destination)
|
|
2329
|
+
}
|
|
2330
|
+
if (destinations.length === 0) return []
|
|
2331
|
+
destinations.sort()
|
|
2332
|
+
|
|
2333
|
+
let prov
|
|
2334
|
+
try {
|
|
2335
|
+
prov = loadProvenance(root)
|
|
2336
|
+
} catch (err) {
|
|
2337
|
+
throw new Error(`thirdparty_artifact_has_provenance: ${err.message}`)
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
const msgs = []
|
|
2341
|
+
for (const destination of destinations) {
|
|
2342
|
+
// Provenance keys are NOT the manifest's absolute destination —
|
|
2343
|
+
// verified empirically against the real install command
|
|
2344
|
+
// (npm/src/commands/thirdparty.js): verifyApproval/upsertProvenanceEntry
|
|
2345
|
+
// are called with the project-root-relative (or "~/"-prefixed,
|
|
2346
|
+
// global-scope) destination string BEFORE IntegrationManager.resolve()
|
|
2347
|
+
// joins it against root to produce the absolute manifest key. Every
|
|
2348
|
+
// claim reached here came from the PROJECT manifest, so its scope is
|
|
2349
|
+
// always "project" (a global-scope claim lives in the home manifest
|
|
2350
|
+
// instead, which this rule intentionally never reads). path.relative
|
|
2351
|
+
// inverts resolve()'s path.resolve(root, relative) exactly. Mirrors
|
|
2352
|
+
// internal/validator/validator_thirdparty_provenance.go.
|
|
2353
|
+
const provenanceKey = path.relative(root, destination)
|
|
2354
|
+
const entry = (prov.entries || {})[provenanceKey]
|
|
2355
|
+
if (!entry) {
|
|
2356
|
+
msgs.push(
|
|
2357
|
+
`thirdparty_artifact_has_provenance: "${destination}" is claimed as a third-party artifact but has no ` +
|
|
2358
|
+
'entry in .trackfw/thirdparty-provenance.json — obtain a favorable hades-tf review and record an ' +
|
|
2359
|
+
'approved provenance entry for this destination before this can pass validate (D2 branch i)'
|
|
2360
|
+
)
|
|
2361
|
+
continue
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
let installed
|
|
2365
|
+
try {
|
|
2366
|
+
installed = fs.readFileSync(destination)
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
msgs.push(
|
|
2369
|
+
`thirdparty_artifact_has_provenance: "${destination}" is claimed as a third-party artifact with an ` +
|
|
2370
|
+
`approved provenance entry, but the destination file could not be read (${err.message})`
|
|
2371
|
+
)
|
|
2372
|
+
continue
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// entry.installed_sha256 may be `undefined` (key absent — e.g. an
|
|
2376
|
+
// approver-authored entry from before `install` ever ran, or a partial
|
|
2377
|
+
// install that failed between manager.Install and the installed_sha256
|
|
2378
|
+
// upsert). Coerce to '' so the comparison and the message text match
|
|
2379
|
+
// Go (zero-value "") and Python (.get(..., "")) byte-for-byte — do NOT
|
|
2380
|
+
// interpolate entry.installed_sha256 directly, that renders the
|
|
2381
|
+
// JS-only literal "undefined" into the violation message.
|
|
2382
|
+
const installedSHA256 = entry.installed_sha256 || ''
|
|
2383
|
+
if (sha256Hex(installed) !== installedSHA256) {
|
|
2384
|
+
msgs.push(
|
|
2385
|
+
`thirdparty_artifact_has_provenance: "${destination}" — installed content does not match ` +
|
|
2386
|
+
`installed_sha256 ${installedSHA256} recorded in .trackfw/thirdparty-provenance.json — the ` +
|
|
2387
|
+
'artifact was modified after approval or installed outside the fetch/install flow (D2 branch ii, D2-bis)'
|
|
2388
|
+
)
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
return msgs
|
|
2393
|
+
}
|
|
2394
|
+
|
|
1722
2395
|
// validateUnfiltered executa todas as validações e retorna { violations, warnings } sem ratchet.
|
|
1723
2396
|
async function validateUnfiltered() {
|
|
1724
2397
|
resetMeta()
|
|
@@ -1740,9 +2413,18 @@ async function validateUnfiltered() {
|
|
|
1740
2413
|
applyRule('blocked_by_draft_adr', validateREQsNotBlockedByDraftADRs(), violations, warnings)
|
|
1741
2414
|
applyRule('adr_accepted_when_req_done', validateADRAcceptedWhenREQDone(), violations, warnings)
|
|
1742
2415
|
applyRule('note_orphan', validateNoteOrphan(), violations, warnings)
|
|
1743
|
-
applyRule('credential_guard_hook_resolvable', validateCredentialGuardHookResolvable(), violations, warnings)
|
|
1744
|
-
applyRule('credential_guard_script_integrity', validateCredentialGuardScriptIntegrity(), violations, warnings)
|
|
2416
|
+
applyRule('credential_guard_hook_resolvable', validateCredentialGuardHookResolvable().concat(validateCredentialGuardGlobalHookResolvable()), violations, warnings)
|
|
2417
|
+
applyRule('credential_guard_script_integrity', validateCredentialGuardScriptIntegrity().concat(validateCredentialGuardGlobalScriptIntegrity()), violations, warnings)
|
|
1745
2418
|
applyRule('credential_guard_mode_downgrade', validateCredentialGuardModeDowngrade(), violations, warnings)
|
|
2419
|
+
// ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
|
|
2420
|
+
// ML-2A — port de internal/validator/validator.go's wiring das 4 regras/wrappers em
|
|
2421
|
+
// validator_git_branch_guard.go.
|
|
2422
|
+
applyRule('git_branch_guard_hook_resolvable', validateGitBranchGuardHookResolvable().concat(validateGitBranchGuardGlobalHookResolvable()), violations, warnings)
|
|
2423
|
+
applyRule('git_branch_guard_script_integrity', validateGitBranchGuardScriptIntegrity().concat(validateGitBranchGuardGlobalScriptIntegrity()), violations, warnings)
|
|
2424
|
+
|
|
2425
|
+
// ADR-2026-08-15-gate-de-duas-fases-..., ML-3A (D2): detecção ancorada em git por trás do
|
|
2426
|
+
// guardrail TRACKFW_ORCHESTRATOR_SESSION.
|
|
2427
|
+
applyRule('thirdparty_artifact_has_provenance', validateThirdPartyArtifactHasProvenance(), violations, warnings)
|
|
1746
2428
|
|
|
1747
2429
|
// Regras configuráveis via applyRule (popula _itemMeta automaticamente)
|
|
1748
2430
|
applyRule('req_has_adr', validateREQsHaveADR(), violations, warnings)
|
|
@@ -2012,4 +2694,21 @@ module.exports = {
|
|
|
2012
2694
|
validateCredentialGuardModeDowngrade,
|
|
2013
2695
|
extractCredentialGuardMode,
|
|
2014
2696
|
CREDENTIAL_GUARD_SCRIPT_REFERENCE,
|
|
2697
|
+
// ROADMAP-2026-08-15-trackfw-validate-deve-detectar-scripts-de-hook-ausentes-ou-desatualizados,
|
|
2698
|
+
// ML-2A
|
|
2699
|
+
collectCommandsWithMarker,
|
|
2700
|
+
validateGuardHookResolvable,
|
|
2701
|
+
validateGitBranchGuardHookResolvable,
|
|
2702
|
+
validateGitBranchGuardScriptIntegrity,
|
|
2703
|
+
GIT_BRANCH_GUARD_SCRIPT_REFERENCE,
|
|
2704
|
+
CREDENTIAL_GUARD_GLOBAL_SCRIPT_REFERENCE,
|
|
2705
|
+
GLOBAL_GUARD_CONFIG_FILES,
|
|
2706
|
+
validateGuardGlobalHookResolvable,
|
|
2707
|
+
validateGuardGlobalScriptIntegrity,
|
|
2708
|
+
// ROADMAP-2026-08-15-instalacao-de-skills-de-terceiro-via-url-para-agentes-especialistas, ML-3A
|
|
2709
|
+
validateThirdPartyArtifactHasProvenance,
|
|
2710
|
+
validateCredentialGuardGlobalHookResolvable,
|
|
2711
|
+
validateCredentialGuardGlobalScriptIntegrity,
|
|
2712
|
+
validateGitBranchGuardGlobalHookResolvable,
|
|
2713
|
+
validateGitBranchGuardGlobalScriptIntegrity,
|
|
2015
2714
|
}
|