@intentsolutions/audit-harness 0.1.0 → 1.1.5

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.
@@ -19,14 +19,32 @@
19
19
  # bash escape-scan.sh path/to/change.patch
20
20
  # bash escape-scan.sh --staged # git diff --cached
21
21
  # bash escape-scan.sh --range HEAD~1..HEAD
22
+ # bash escape-scan.sh --staged --json # machine-readable JSON to stdout
23
+ #
24
+ # JSON mode:
25
+ # stdout = single JSON object suitable for piping to `audit-harness emit-evidence`
26
+ # stderr = unchanged human-readable [SEVERITY] notes (preserves backward-compat)
27
+ # exit codes unchanged
22
28
 
23
29
  set -euo pipefail
24
30
 
25
31
  DIFF_SRC=""
26
32
  VERIFY_HASH=1
33
+ JSON_OUT=0
27
34
  ROOT="${ROOT:-$(pwd)}"
28
35
  HASH_SCRIPT="$(dirname "$0")/harness-hash.sh"
29
36
 
37
+ # First-pass arg parse: peel --json off the tail (any position) so primary
38
+ # arg parsing below is unchanged.
39
+ _filtered_args=()
40
+ for arg in "$@"; do
41
+ case "$arg" in
42
+ --json) JSON_OUT=1 ;;
43
+ *) _filtered_args+=("$arg") ;;
44
+ esac
45
+ done
46
+ set -- "${_filtered_args[@]+"${_filtered_args[@]}"}"
47
+
30
48
  if [[ "$#" -eq 0 ]]; then
31
49
  echo "escape-scan: pass a diff source (- for stdin, --staged, --range, or a patch file)" >&2
32
50
  exit 2
@@ -34,11 +52,20 @@ fi
34
52
 
35
53
  case "$1" in
36
54
  -) DIFF_SRC="/dev/stdin" ;;
37
- --staged) DIFF_SRC=$(mktemp); git diff --cached > "$DIFF_SRC" ;;
38
- --range) DIFF_SRC=$(mktemp); git diff "$2" > "$DIFF_SRC"; shift ;;
55
+ --staged)
56
+ DIFF_SRC=$(mktemp)
57
+ trap 'rm -f "$DIFF_SRC"' EXIT
58
+ git diff --cached > "$DIFF_SRC"
59
+ ;;
60
+ --range)
61
+ DIFF_SRC=$(mktemp)
62
+ trap 'rm -f "$DIFF_SRC"' EXIT
63
+ git diff "$2" > "$DIFF_SRC"
64
+ shift
65
+ ;;
39
66
  --no-hash) VERIFY_HASH=0; shift; DIFF_SRC="$1" ;;
40
67
  --help|-h)
41
- sed -n '2,22p' "$0"; exit 0 ;;
68
+ sed -n '2,26p' "$0"; exit 0 ;;
42
69
  *) DIFF_SRC="$1" ;;
43
70
  esac
44
71
 
@@ -159,7 +186,34 @@ if echo "$added_lines" | grep -Eq 'toBeDefined\(\)|\.is not None'; then
159
186
  fi
160
187
 
161
188
  # --- Summary & exit ---
162
- echo "escape-scan: REFUSE=$REFUSE CHALLENGE=$CHALLENGE FLAG=$FLAG"
189
+ if [[ "$JSON_OUT" -eq 1 ]]; then
190
+ # Result mapping (per intent-eval-lab evidence-bundle SPEC § 5 R6):
191
+ # any REFUSE → FAIL
192
+ # any CHALLENGE (no REFUSE) → FAIL (exit 1 = blocking, requires human)
193
+ # only FLAG → ADVISORY (exit 0 — informational)
194
+ # none → PASS
195
+ result="PASS"
196
+ if [[ "$REFUSE" -gt 0 || "$CHALLENGE" -gt 0 ]]; then
197
+ result="FAIL"
198
+ elif [[ "$FLAG" -gt 0 ]]; then
199
+ result="ADVISORY"
200
+ fi
201
+ input_hash=$(sha256sum "$DIFF_SRC" | awk '{print "sha256:"$1}')
202
+ policy_hash="sha256:0000000000000000000000000000000000000000000000000000000000000000"
203
+ if [[ -f "$TESTING_MD" ]]; then
204
+ policy_hash=$(sha256sum "$TESTING_MD" | awk '{print "sha256:"$1}')
205
+ fi
206
+ printf '{"gate_id":"audit-harness:%s:escape-scan","result":"%s","input_hash":"%s","policy_hash":"%s","metadata":{"refuse":%d,"challenge":%d,"flag":%d,"coverage_line_floor":%d,"coverage_branch_floor":%d,"mutation_floor":%d}' \
207
+ "${AUDIT_HARNESS_SIDE:-ci}" "$result" "$input_hash" "$policy_hash" "$REFUSE" "$CHALLENGE" "$FLAG" \
208
+ "$COVERAGE_LINE_FLOOR" "$COVERAGE_BRANCH_FLOOR" "$MUTATION_FLOOR"
209
+ if [[ "$result" == "ADVISORY" ]]; then
210
+ printf ',"advisory_severity":"info"'
211
+ fi
212
+ printf '}\n'
213
+ echo "escape-scan: REFUSE=$REFUSE CHALLENGE=$CHALLENGE FLAG=$FLAG" >&2
214
+ else
215
+ echo "escape-scan: REFUSE=$REFUSE CHALLENGE=$CHALLENGE FLAG=$FLAG"
216
+ fi
163
217
  if [[ "$REFUSE" -gt 0 ]]; then
164
218
  echo "escape-scan: pipeline halted (REFUSE)" >&2
165
219
  exit 2
@@ -15,11 +15,13 @@ set -euo pipefail
15
15
 
16
16
  PATH_ARG="features/"
17
17
  STRICT=0
18
+ JSON_OUT=0
18
19
 
19
20
  while [[ $# -gt 0 ]]; do
20
21
  case "$1" in
21
22
  --path) PATH_ARG="$2"; shift 2 ;;
22
23
  --strict) STRICT=1; shift ;;
24
+ --json) JSON_OUT=1; shift ;;
23
25
  --help|-h)
24
26
  sed -n '2,15p' "$0"; exit 0 ;;
25
27
  *) echo "gherkin-lint: unknown flag $1" >&2; exit 2 ;;
@@ -27,15 +29,40 @@ while [[ $# -gt 0 ]]; do
27
29
  done
28
30
 
29
31
  if [[ ! -d "$PATH_ARG" ]]; then
32
+ if [[ "$JSON_OUT" -eq 1 ]]; then
33
+ printf '{"gate_id":"audit-harness:%s:gherkin-lint","result":"NOT_APPLICABLE","input_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","policy_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","metadata":{"reason":"path not found","path":"%s"}}\n' \
34
+ "${AUDIT_HARNESS_SIDE:-ci}" "$PATH_ARG"
35
+ fi
30
36
  echo "gherkin-lint: path not found: $PATH_ARG" >&2
31
37
  exit 2
32
38
  fi
33
39
 
40
+ INPUT_HASH=$(find "$PATH_ARG" -name "*.feature" -type f -exec sha256sum {} \; 2>/dev/null | sort | sha256sum | awk '{print "sha256:"$1}')
41
+
42
+ if [[ "$JSON_OUT" -eq 1 ]]; then
43
+ exec 3>&1
44
+ exec 1>&2
45
+ fi
46
+
34
47
  WARN_COUNT=0
35
48
  ERROR_COUNT=0
36
49
 
37
50
  warn() { echo "WARN $1:$2 $3"; WARN_COUNT=$((WARN_COUNT + 1)); }
38
- err() { echo "ERROR $1:$2 $3"; ERROR_COUNT=$((ERROR_COUNT + 1)); }
51
+
52
+ # process_awk_output — funnel awk-printed WARN/ERROR lines through the bash
53
+ # counters so the summary + exit code reflect awk-fallback findings (the
54
+ # subprocesses below can't otherwise touch the parent-shell counters).
55
+ # Single-pass awk counts both at once; no-match handled cleanly under
56
+ # set -euo pipefail via the `+0` numeric coercions.
57
+ process_awk_output() {
58
+ local out="$1"
59
+ [ -z "$out" ] && return 0
60
+ local w=0 e=0
61
+ read -r w e < <(awk '/^WARN /{w++} /^ERROR /{e++} END {print w+0, e+0}' <<< "$out")
62
+ WARN_COUNT=$((WARN_COUNT + w))
63
+ ERROR_COUNT=$((ERROR_COUNT + e))
64
+ printf '%s\n' "$out"
65
+ }
39
66
 
40
67
  # 1. Prefer official gherkin-lint if available
41
68
  if command -v gherkin-lint >/dev/null 2>&1; then
@@ -48,7 +75,7 @@ else
48
75
 
49
76
  while IFS= read -r -d '' feature; do
50
77
  # Imperative verbs / CSS selectors in steps (declarative warning)
51
- awk -v file="$feature" '
78
+ process_awk_output "$(awk -v file="$feature" '
52
79
  /^[[:space:]]*(Given|When|Then|And|But)/ {
53
80
  line = $0
54
81
  if (line ~ /click|type|fill[ _]in|press|select.*from[ _]dropdown/) {
@@ -58,10 +85,10 @@ else
58
85
  printf "WARN %s:%d CSS selector / xpath in step (prefer business language)\n", file, NR
59
86
  }
60
87
  }
61
- ' "$feature"
88
+ ' "$feature")"
62
89
 
63
90
  # Scenario length (> 10 steps)
64
- awk -v file="$feature" '
91
+ process_awk_output "$(awk -v file="$feature" '
65
92
  /^[[:space:]]*Scenario/ { sc = NR; steps = 0; sn = $0; next }
66
93
  /^[[:space:]]*(Given|When|Then|And|But)/ { if (sc) steps++ }
67
94
  /^[[:space:]]*Scenario|^[[:space:]]*Feature|^$/ {
@@ -75,7 +102,7 @@ else
75
102
  printf "WARN %s:%d scenario has %d steps (>10 is too long)\n", file, sc, steps
76
103
  }
77
104
  }
78
- ' "$feature"
105
+ ' "$feature")"
79
106
 
80
107
  # Repeated Givens without Background (3+ identical Given lines)
81
108
  dupe=$(awk '/^[[:space:]]*Given/ { print }' "$feature" | sort | uniq -c | awk '$1 >= 3 { print }')
@@ -84,9 +111,7 @@ else
84
111
  fi
85
112
 
86
113
  # "And" at scenario start (grammar error)
87
- awk -v file="$feature" '
88
- prev_blank = 1
89
- /^[[:space:]]*$/ { prev_blank = 1; next }
114
+ process_awk_output "$(awk -v file="$feature" '
90
115
  /^[[:space:]]*Scenario/ { in_scenario = 1; step_count = 0; next }
91
116
  /^[[:space:]]*(Given|When|Then|And|But)/ {
92
117
  if (in_scenario && step_count == 0 && /^[[:space:]]*And/) {
@@ -94,7 +119,7 @@ else
94
119
  }
95
120
  step_count++
96
121
  }
97
- ' "$feature"
122
+ ' "$feature")"
98
123
 
99
124
  done < <(find "$PATH_ARG" -name "*.feature" -print0)
100
125
  fi
@@ -102,6 +127,25 @@ fi
102
127
  echo ""
103
128
  echo "gherkin-lint summary: $WARN_COUNT warning(s), $ERROR_COUNT error(s)"
104
129
 
130
+ if [[ "$JSON_OUT" -eq 1 ]]; then
131
+ exec 1>&3 3>&-
132
+ result="PASS"
133
+ sev_block=""
134
+ if [[ "$ERROR_COUNT" -gt 0 ]]; then
135
+ result="FAIL"
136
+ elif [[ "$WARN_COUNT" -gt 0 ]]; then
137
+ if [[ "$STRICT" -eq 1 ]]; then
138
+ result="FAIL"
139
+ else
140
+ result="ADVISORY"
141
+ sev_block=',"advisory_severity":"warn"'
142
+ fi
143
+ fi
144
+ printf '{"gate_id":"audit-harness:%s:gherkin-lint","result":"%s"%s,"input_hash":"%s","policy_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","metadata":{"warnings":%d,"errors":%d,"strict":%s,"path":"%s"}}\n' \
145
+ "${AUDIT_HARNESS_SIDE:-ci}" "$result" "$sev_block" "$INPUT_HASH" "$WARN_COUNT" "$ERROR_COUNT" \
146
+ "$([[ "$STRICT" -eq 1 ]] && echo true || echo false)" "$PATH_ARG"
147
+ fi
148
+
105
149
  if [[ "$ERROR_COUNT" -gt 0 ]]; then
106
150
  exit 1
107
151
  fi
@@ -6,19 +6,48 @@
6
6
  # causes escape-scan.sh to REFUSE the AI diff.
7
7
  #
8
8
  # Usage:
9
- # bash harness-hash.sh --init # write manifest (engineer-initiated)
10
- # bash harness-hash.sh --verify # compare current hashes to manifest
11
- # bash harness-hash.sh --list # show which files are pinned
9
+ # bash harness-hash.sh --init # write manifest (engineer-initiated)
10
+ # bash harness-hash.sh --verify # compare current hashes to manifest
11
+ # bash harness-hash.sh --verify --json # machine-readable JSON to stdout (verify only)
12
+ # bash harness-hash.sh --list # show which files are pinned
12
13
  #
13
14
  # Exit codes:
14
15
  # 0 — OK (pin matches, or init succeeded)
15
16
  # 2 — HARNESS_TAMPERED (hash mismatch)
16
17
  # 3 — no manifest found (--verify without --init)
18
+ #
19
+ # JSON mode:
20
+ # stdout = single JSON object suitable for piping to `audit-harness emit-evidence`
21
+ # stderr = unchanged human-readable summary (preserves backward-compat)
22
+ # exit codes unchanged
17
23
 
18
24
  set -euo pipefail
19
25
 
26
+ # Cross-platform SHA-256: `sha256sum` ships with GNU coreutils (Linux);
27
+ # macOS only has `shasum -a 256`. Both produce identical `<hash> <file>`
28
+ # output, so downstream awk parsing is unchanged.
29
+ if command -v sha256sum >/dev/null 2>&1; then
30
+ SHA256_CMD=(sha256sum)
31
+ elif command -v shasum >/dev/null 2>&1; then
32
+ SHA256_CMD=(shasum -a 256)
33
+ else
34
+ echo "harness-hash: neither sha256sum nor shasum found in PATH" >&2
35
+ exit 2
36
+ fi
37
+
20
38
  ROOT="${ROOT:-$(pwd)}"
21
39
  MANIFEST="${ROOT}/.harness-hash"
40
+ JSON_OUT=0
41
+
42
+ # Peel --json from anywhere in args (additive, doesn't disturb existing arg shape)
43
+ _filtered_args=()
44
+ for arg in "$@"; do
45
+ case "$arg" in
46
+ --json) JSON_OUT=1 ;;
47
+ *) _filtered_args+=("$arg") ;;
48
+ esac
49
+ done
50
+ set -- "${_filtered_args[@]+"${_filtered_args[@]}"}"
22
51
 
23
52
  PATTERNS=(
24
53
  # Wall 1: acceptance
@@ -42,6 +71,27 @@ PATTERNS=(
42
71
  "stryker.config.js"
43
72
  )
44
73
 
74
+ # Optional per-repo extra patterns appended from .harness-hash-extra-patterns
75
+ # at the repo root. Used by repos whose policy files don't match the default
76
+ # canonical patterns above — e.g., the audit-harness repo itself pins its own
77
+ # scripts (scripts/*.sh + scripts/*.py + bin/audit-harness.js), which are the
78
+ # policy enforcement surface but aren't covered by the consumer-facing
79
+ # defaults. Lines beginning with `#` are comments; blank lines are ignored.
80
+ # This mechanism is additive — repos without the file get exactly the
81
+ # default behavior, so consumer repos are not affected.
82
+ EXTRA_PATTERNS_FILE="${ROOT}/.harness-hash-extra-patterns"
83
+ if [[ -f "${EXTRA_PATTERNS_FILE}" ]]; then
84
+ while IFS= read -r line || [[ -n "${line}" ]]; do
85
+ # strip inline comments
86
+ line="${line%%#*}"
87
+ # trim leading + trailing whitespace
88
+ line="${line#"${line%%[![:space:]]*}"}"
89
+ line="${line%"${line##*[![:space:]]}"}"
90
+ [[ -z "${line}" ]] && continue
91
+ PATTERNS+=("${line}")
92
+ done < "${EXTRA_PATTERNS_FILE}"
93
+ fi
94
+
45
95
  collect_files() {
46
96
  local out=()
47
97
  shopt -s nullglob globstar
@@ -61,7 +111,7 @@ hash_files() {
61
111
  return 0
62
112
  fi
63
113
  while IFS= read -r f; do
64
- printf '%s %s\n' "$(sha256sum "$f" | awk '{print $1}')" "$f"
114
+ printf '%s %s\n' "$("${SHA256_CMD[@]}" "$f" | awk '{print $1}')" "$f"
65
115
  done <<< "$files"
66
116
  }
67
117
 
@@ -76,6 +126,10 @@ cmd_init() {
76
126
  cmd_verify() {
77
127
  cd "$ROOT"
78
128
  if [[ ! -f "$MANIFEST" ]]; then
129
+ if [[ "$JSON_OUT" -eq 1 ]]; then
130
+ printf '{"gate_id":"audit-harness:%s:harness-hash","result":"NOT_APPLICABLE","input_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","policy_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","metadata":{"reason":"no manifest at %s (run --init)"}}\n' \
131
+ "${AUDIT_HARNESS_SIDE:-ci}" "$MANIFEST"
132
+ fi
79
133
  echo "harness-hash: no manifest at $MANIFEST (run --init)" >&2
80
134
  exit 3
81
135
  fi
@@ -84,13 +138,32 @@ cmd_verify() {
84
138
  local expected
85
139
  expected=$(cat "$MANIFEST")
86
140
 
141
+ local manifest_hash
142
+ manifest_hash=$("${SHA256_CMD[@]}" "$MANIFEST" | awk '{print "sha256:"$1}')
143
+
144
+ local pinned_count
145
+ pinned_count=$(echo "$expected" | grep -c '^' || true)
146
+
87
147
  # Compare sorted manifests so order doesn't matter
88
148
  local diff_out
89
149
  diff_out=$(diff <(echo "$expected" | sort) <(echo "$current" | sort) || true)
90
150
  if [[ -z "$diff_out" ]]; then
91
- echo "harness-hash: OK"
151
+ if [[ "$JSON_OUT" -eq 1 ]]; then
152
+ printf '{"gate_id":"audit-harness:%s:harness-hash","result":"PASS","input_hash":"%s","policy_hash":"%s","metadata":{"pinned_count":%d}}\n' \
153
+ "${AUDIT_HARNESS_SIDE:-ci}" "$manifest_hash" "$manifest_hash" "$pinned_count"
154
+ echo "harness-hash: OK" >&2
155
+ else
156
+ echo "harness-hash: OK"
157
+ fi
92
158
  exit 0
93
159
  fi
160
+ if [[ "$JSON_OUT" -eq 1 ]]; then
161
+ # diff output may contain quotes/newlines; encode as a single-line escaped string
162
+ local diff_escaped
163
+ diff_escaped=$(printf '%s' "$diff_out" | python3 -c 'import sys, json; print(json.dumps(sys.stdin.read()))')
164
+ printf '{"gate_id":"audit-harness:%s:harness-hash","result":"FAIL","failure_mode":"HARNESS_TAMPERED","input_hash":"%s","policy_hash":"%s","metadata":{"pinned_count":%d,"diff":%s}}\n' \
165
+ "${AUDIT_HARNESS_SIDE:-ci}" "$manifest_hash" "$manifest_hash" "$pinned_count" "$diff_escaped"
166
+ fi
94
167
  echo "HARNESS_TAMPERED: pinned artifact changed" >&2
95
168
  echo "$diff_out" >&2
96
169
  exit 2