supply-chain-guard 5.23.0 → 5.23.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/README.md +36 -4
- package/action.yml +373 -59
- package/dist/agentic-workflow-scanner.d.ts.map +1 -1
- package/dist/agentic-workflow-scanner.js +11 -11
- package/dist/agentic-workflow-scanner.js.map +1 -1
- package/dist/archive-extractor.d.ts +11 -5
- package/dist/archive-extractor.d.ts.map +1 -1
- package/dist/archive-extractor.js +670 -8
- package/dist/archive-extractor.js.map +1 -1
- package/dist/broad-gap-pattern-matchers.d.ts +17 -0
- package/dist/broad-gap-pattern-matchers.d.ts.map +1 -0
- package/dist/broad-gap-pattern-matchers.js +921 -0
- package/dist/broad-gap-pattern-matchers.js.map +1 -0
- package/dist/cargo-scanner.d.ts.map +1 -1
- package/dist/cargo-scanner.js +171 -71
- package/dist/cargo-scanner.js.map +1 -1
- package/dist/cli.js +239 -77
- package/dist/cli.js.map +1 -1
- package/dist/composer-scanner.d.ts.map +1 -1
- package/dist/composer-scanner.js +9 -15
- package/dist/composer-scanner.js.map +1 -1
- package/dist/config-scanner.d.ts.map +1 -1
- package/dist/config-scanner.js +5 -4
- package/dist/config-scanner.js.map +1 -1
- package/dist/continuous-monitor.d.ts.map +1 -1
- package/dist/continuous-monitor.js +4 -0
- package/dist/continuous-monitor.js.map +1 -1
- package/dist/correlated-pattern-matchers.d.ts +13 -0
- package/dist/correlated-pattern-matchers.d.ts.map +1 -0
- package/dist/correlated-pattern-matchers.js +949 -0
- package/dist/correlated-pattern-matchers.js.map +1 -0
- package/dist/dependency-confusion.js +4 -4
- package/dist/dependency-confusion.js.map +1 -1
- package/dist/dockerfile-scanner.d.ts.map +1 -1
- package/dist/dockerfile-scanner.js +189 -4
- package/dist/dockerfile-scanner.js.map +1 -1
- package/dist/extracted-file-walker.d.ts +25 -0
- package/dist/extracted-file-walker.d.ts.map +1 -0
- package/dist/extracted-file-walker.js +218 -0
- package/dist/extracted-file-walker.js.map +1 -0
- package/dist/git-scanner.d.ts.map +1 -1
- package/dist/git-scanner.js +99 -78
- package/dist/git-scanner.js.map +1 -1
- package/dist/github-actions-scanner.d.ts +7 -1
- package/dist/github-actions-scanner.d.ts.map +1 -1
- package/dist/github-actions-scanner.js +36 -25
- package/dist/github-actions-scanner.js.map +1 -1
- package/dist/github-trust-scanner.d.ts.map +1 -1
- package/dist/github-trust-scanner.js +14 -20
- package/dist/github-trust-scanner.js.map +1 -1
- package/dist/go-scanner.d.ts.map +1 -1
- package/dist/go-scanner.js +149 -61
- package/dist/go-scanner.js.map +1 -1
- package/dist/internal-disclosure.d.ts +4 -0
- package/dist/internal-disclosure.d.ts.map +1 -1
- package/dist/internal-disclosure.js +2 -1
- package/dist/internal-disclosure.js.map +1 -1
- package/dist/ioc-blocklist.d.ts.map +1 -1
- package/dist/ioc-blocklist.js +112 -0
- package/dist/ioc-blocklist.js.map +1 -1
- package/dist/lockfile-checker.d.ts.map +1 -1
- package/dist/lockfile-checker.js +23 -39
- package/dist/lockfile-checker.js.map +1 -1
- package/dist/mcp-scanner.d.ts.map +1 -1
- package/dist/mcp-scanner.js +5 -3
- package/dist/mcp-scanner.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +3 -1
- package/dist/mcp-server.js.map +1 -1
- package/dist/npm-scanner.d.ts +10 -1
- package/dist/npm-scanner.d.ts.map +1 -1
- package/dist/npm-scanner.js +87 -57
- package/dist/npm-scanner.js.map +1 -1
- package/dist/nuget-scanner.d.ts.map +1 -1
- package/dist/nuget-scanner.js +24 -22
- package/dist/nuget-scanner.js.map +1 -1
- package/dist/pattern-applicability.d.ts +23 -0
- package/dist/pattern-applicability.d.ts.map +1 -0
- package/dist/pattern-applicability.js +102 -0
- package/dist/pattern-applicability.js.map +1 -0
- package/dist/pattern-scanner.d.ts +53 -0
- package/dist/pattern-scanner.d.ts.map +1 -0
- package/dist/pattern-scanner.js +336 -0
- package/dist/pattern-scanner.js.map +1 -0
- package/dist/patterns.d.ts +97 -28
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +1175 -103
- package/dist/patterns.js.map +1 -1
- package/dist/posture-engine.d.ts +4 -0
- package/dist/posture-engine.d.ts.map +1 -1
- package/dist/posture-engine.js +15 -3
- package/dist/posture-engine.js.map +1 -1
- package/dist/pypi-scanner.d.ts +34 -1
- package/dist/pypi-scanner.d.ts.map +1 -1
- package/dist/pypi-scanner.js +315 -98
- package/dist/pypi-scanner.js.map +1 -1
- package/dist/python-lockfile-scanner.d.ts.map +1 -1
- package/dist/python-lockfile-scanner.js +12 -19
- package/dist/python-lockfile-scanner.js.map +1 -1
- package/dist/regex-complexity.d.ts +12 -0
- package/dist/regex-complexity.d.ts.map +1 -0
- package/dist/regex-complexity.js +173 -0
- package/dist/regex-complexity.js.map +1 -0
- package/dist/reporter.d.ts +9 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +142 -30
- package/dist/reporter.js.map +1 -1
- package/dist/rubygems-scanner.d.ts.map +1 -1
- package/dist/rubygems-scanner.js +9 -15
- package/dist/rubygems-scanner.js.map +1 -1
- package/dist/sbom-generator.js +1 -1
- package/dist/sbom-generator.js.map +1 -1
- package/dist/scanner.d.ts +6 -0
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +261 -247
- package/dist/scanner.js.map +1 -1
- package/dist/skills-scanner.d.ts.map +1 -1
- package/dist/skills-scanner.js +198 -141
- package/dist/skills-scanner.js.map +1 -1
- package/dist/soc-exporter.d.ts.map +1 -1
- package/dist/soc-exporter.js +38 -5
- package/dist/soc-exporter.js.map +1 -1
- package/dist/threat-intel.d.ts.map +1 -1
- package/dist/threat-intel.js +340 -0
- package/dist/threat-intel.js.map +1 -1
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vscode-scanner.d.ts +16 -1
- package/dist/vscode-scanner.d.ts.map +1 -1
- package/dist/vscode-scanner.js +289 -119
- package/dist/vscode-scanner.js.map +1 -1
- package/dist/workflow-pattern-matchers.d.ts +15 -0
- package/dist/workflow-pattern-matchers.d.ts.map +1 -0
- package/dist/workflow-pattern-matchers.js +303 -0
- package/dist/workflow-pattern-matchers.js.map +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ Run the scanner as a [pre-commit](https://pre-commit.com) hook (Python-ecosystem
|
|
|
131
131
|
```yaml
|
|
132
132
|
repos:
|
|
133
133
|
- repo: https://github.com/homeofe/supply-chain-guard
|
|
134
|
-
rev: v5.23.
|
|
134
|
+
rev: v5.23.2
|
|
135
135
|
hooks:
|
|
136
136
|
- id: supply-chain-guard
|
|
137
137
|
```
|
|
@@ -201,6 +201,7 @@ supply-chain-guard scan ./project --format sbom # CycloneDX 1.6 SBOM with real
|
|
|
201
201
|
supply-chain-guard scan ./project --sbom-output sbom.json # Write SBOM to file separately
|
|
202
202
|
supply-chain-guard scan ./project --format badge # Shields.io endpoint JSON
|
|
203
203
|
supply-chain-guard scan ./project --format gitlab # GitLab Dependency Scanning report (security-report-schemas 15.2.4, see examples/gitlab-ci.yml)
|
|
204
|
+
supply-chain-guard scan ./project --format markdown --json-output canonical.json # Same scan, human report plus canonical JSON
|
|
204
205
|
```
|
|
205
206
|
|
|
206
207
|
### Badge
|
|
@@ -233,6 +234,12 @@ supply-chain-guard scan ./project --fail-on high # Fail on high or above
|
|
|
233
234
|
supply-chain-guard scan ./project --fail-on info # Fail on any finding
|
|
234
235
|
```
|
|
235
236
|
|
|
237
|
+
`--min-severity` may reduce report noise, but it cannot be stricter than the
|
|
238
|
+
active `--fail-on` gate because that would hide findings required for the exit
|
|
239
|
+
verdict. Invalid combinations fail before scanning. Incomplete coverage also
|
|
240
|
+
exits nonzero regardless of the severity threshold and is reported as
|
|
241
|
+
`partialScan: true` in JSON.
|
|
242
|
+
|
|
236
243
|
## Filtering
|
|
237
244
|
|
|
238
245
|
```bash
|
|
@@ -585,6 +592,10 @@ Honest caveats: Socket's registry-wide behavioral detection is deeper than anyth
|
|
|
585
592
|
name: Supply Chain Security
|
|
586
593
|
on: [push, pull_request]
|
|
587
594
|
|
|
595
|
+
permissions:
|
|
596
|
+
contents: read
|
|
597
|
+
pull-requests: write
|
|
598
|
+
|
|
588
599
|
jobs:
|
|
589
600
|
scan:
|
|
590
601
|
runs-on: ubuntu-latest
|
|
@@ -601,11 +612,32 @@ jobs:
|
|
|
601
612
|
| Input | Description | Default |
|
|
602
613
|
|-------|-------------|---------|
|
|
603
614
|
| `path` | Path to scan | `.` |
|
|
604
|
-
| `format` | Output format (text/json/markdown/html/
|
|
615
|
+
| `format` | Output format (text/json/markdown/sarif/sbom/html/badge/gitlab/junit) | `markdown` |
|
|
605
616
|
| `min-severity` | Minimum severity to report | `low` |
|
|
606
617
|
| `exclude-rules` | Comma-separated rule IDs to exclude | |
|
|
607
|
-
| `fail-on` | Fail check at this severity or above | `critical` |
|
|
608
|
-
| `comment-on-pr` | Post
|
|
618
|
+
| `fail-on` | Fail check at this severity or above, including `info` | `critical` |
|
|
619
|
+
| `comment-on-pr` | Post or update a PR comment | `true` |
|
|
620
|
+
|
|
621
|
+
Coverage failures are fail-closed regardless of `fail-on`: the Action exits
|
|
622
|
+
nonzero, sets `partial-scan` to `true` and `risk-level` to `partial`, and posts
|
|
623
|
+
a warning even when severity filters hide the informational coverage finding.
|
|
624
|
+
A critical threshold failure retains exit code 2; other partial results exit 1.
|
|
625
|
+
|
|
626
|
+
PR comments require `pull-requests: write` (shown above). GitHub restricts write
|
|
627
|
+
access for pull requests from forks, so the check verdict, job log, and outputs
|
|
628
|
+
remain authoritative when the platform refuses a comment.
|
|
629
|
+
|
|
630
|
+
### Action Outputs
|
|
631
|
+
|
|
632
|
+
| Output | Description |
|
|
633
|
+
|--------|-------------|
|
|
634
|
+
| `score` | Risk score from 0 to 100 |
|
|
635
|
+
| `risk-level` | `partial`, `clean`, `low`, `medium`, `high`, or `critical` |
|
|
636
|
+
| `findings-count` | Number of reportable findings after filters |
|
|
637
|
+
| `partial-scan` | `true` when coverage was incomplete |
|
|
638
|
+
| `report` | Requested report, or a size notice when it exceeds the safe output budget |
|
|
639
|
+
| `report-path` | Runner-local path to the complete report for later steps in the same job |
|
|
640
|
+
| `report-truncated` | `true` when `report` was replaced by the size notice |
|
|
609
641
|
|
|
610
642
|
## For AI Coding Agents (MCP)
|
|
611
643
|
|
package/action.yml
CHANGED
|
@@ -24,7 +24,7 @@ inputs:
|
|
|
24
24
|
required: false
|
|
25
25
|
default: ""
|
|
26
26
|
fail-on:
|
|
27
|
-
description: "Fail the check if findings at this severity or above are found: critical, high, medium, low"
|
|
27
|
+
description: "Fail the check if findings at this severity or above are found: critical, high, medium, low, info"
|
|
28
28
|
required: false
|
|
29
29
|
default: "critical"
|
|
30
30
|
comment-on-pr:
|
|
@@ -35,24 +35,38 @@ inputs:
|
|
|
35
35
|
outputs:
|
|
36
36
|
score:
|
|
37
37
|
description: "Risk score (0-100)"
|
|
38
|
+
value: ${{ steps.scan.outputs.score }}
|
|
38
39
|
risk-level:
|
|
39
|
-
description: "Risk level: clean, low, medium, high, critical"
|
|
40
|
+
description: "Risk level: partial, clean, low, medium, high, critical"
|
|
41
|
+
value: ${{ steps.scan.outputs['risk-level'] }}
|
|
40
42
|
findings-count:
|
|
41
|
-
description: "
|
|
43
|
+
description: "Number of reportable findings after filters"
|
|
44
|
+
value: ${{ steps.scan.outputs['findings-count'] }}
|
|
45
|
+
partial-scan:
|
|
46
|
+
description: "Whether coverage was incomplete and the check failed closed"
|
|
47
|
+
value: ${{ steps.scan.outputs['partial-scan'] }}
|
|
42
48
|
report:
|
|
43
|
-
description: "
|
|
49
|
+
description: "Scan report in the requested format (bounded for GitHub output limits)"
|
|
50
|
+
value: ${{ steps.scan.outputs.report }}
|
|
51
|
+
report-path:
|
|
52
|
+
description: "Runner-local path containing the complete scan report"
|
|
53
|
+
value: ${{ steps.scan.outputs['report-path'] }}
|
|
54
|
+
report-truncated:
|
|
55
|
+
description: "Whether the report output was replaced by a size-limit notice"
|
|
56
|
+
value: ${{ steps.scan.outputs['report-truncated'] }}
|
|
44
57
|
|
|
45
58
|
runs:
|
|
46
59
|
using: "composite"
|
|
47
60
|
steps:
|
|
48
61
|
- name: Setup Node.js
|
|
49
|
-
uses: actions/setup-node@v7
|
|
62
|
+
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
50
63
|
with:
|
|
51
64
|
node-version: "22"
|
|
65
|
+
package-manager-cache: false
|
|
52
66
|
|
|
53
67
|
- name: Install supply-chain-guard
|
|
54
68
|
shell: bash
|
|
55
|
-
run: npm install -g supply-chain-guard
|
|
69
|
+
run: npm install -g supply-chain-guard@5.23.2 --ignore-scripts --no-audit --no-fund
|
|
56
70
|
|
|
57
71
|
- name: Run scan
|
|
58
72
|
id: scan
|
|
@@ -60,18 +74,53 @@ runs:
|
|
|
60
74
|
env:
|
|
61
75
|
SCG_PATH: ${{ inputs.path }}
|
|
62
76
|
SCG_FORMAT: ${{ inputs.format }}
|
|
63
|
-
SCG_MIN_SEVERITY: ${{ inputs
|
|
64
|
-
SCG_EXCLUDE_RULES: ${{ inputs
|
|
65
|
-
SCG_FAIL_ON: ${{ inputs
|
|
77
|
+
SCG_MIN_SEVERITY: ${{ inputs['min-severity'] }}
|
|
78
|
+
SCG_EXCLUDE_RULES: ${{ inputs['exclude-rules'] }}
|
|
79
|
+
SCG_FAIL_ON: ${{ inputs['fail-on'] }}
|
|
80
|
+
SCG_COMMENT_ON_PR: ${{ inputs['comment-on-pr'] }}
|
|
66
81
|
run: |
|
|
67
82
|
set +e
|
|
68
83
|
|
|
69
84
|
# Inputs arrive via env (set by the runner), never interpolated into this
|
|
70
|
-
# script at template-render time.
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
# script at template-render time. Validate every enum here: Commander
|
|
86
|
+
# validates its own invocation, but the Action must not turn a typo into
|
|
87
|
+
# an ignored gate or a successful scan.
|
|
88
|
+
SCG_FORMAT="${SCG_FORMAT:-markdown}"
|
|
89
|
+
FAIL_SEVERITY="${SCG_FAIL_ON:-critical}"
|
|
90
|
+
COMMENT_ON_PR="${SCG_COMMENT_ON_PR:-true}"
|
|
91
|
+
case "$SCG_FORMAT" in
|
|
92
|
+
text|json|markdown|sarif|sbom|html|badge|gitlab|junit) ;;
|
|
93
|
+
*) echo "Invalid format input. Allowed: text, json, markdown, sarif, sbom, html, badge, gitlab, junit." >&2; exit 1 ;;
|
|
94
|
+
esac
|
|
95
|
+
case "$COMMENT_ON_PR" in
|
|
96
|
+
true|false) ;;
|
|
97
|
+
*) echo "Invalid comment-on-pr input. Allowed: true, false." >&2; exit 1 ;;
|
|
98
|
+
esac
|
|
99
|
+
case "$SCG_MIN_SEVERITY" in
|
|
100
|
+
""|info) MIN_SEVERITY_RANK=0 ;;
|
|
101
|
+
low) MIN_SEVERITY_RANK=1 ;;
|
|
102
|
+
medium) MIN_SEVERITY_RANK=2 ;;
|
|
103
|
+
high) MIN_SEVERITY_RANK=3 ;;
|
|
104
|
+
critical) MIN_SEVERITY_RANK=4 ;;
|
|
105
|
+
*) echo "Invalid min-severity input. Allowed: info, low, medium, high, critical." >&2; exit 1 ;;
|
|
106
|
+
esac
|
|
107
|
+
case "$FAIL_SEVERITY" in
|
|
108
|
+
info) FAIL_SEVERITY_RANK=0 ;;
|
|
109
|
+
low) FAIL_SEVERITY_RANK=1 ;;
|
|
110
|
+
medium) FAIL_SEVERITY_RANK=2 ;;
|
|
111
|
+
high) FAIL_SEVERITY_RANK=3 ;;
|
|
112
|
+
critical) FAIL_SEVERITY_RANK=4 ;;
|
|
113
|
+
*) echo "Invalid fail-on input. Allowed: info, low, medium, high, critical." >&2; exit 1 ;;
|
|
114
|
+
esac
|
|
115
|
+
if [ "$MIN_SEVERITY_RANK" -gt "$FAIL_SEVERITY_RANK" ]; then
|
|
116
|
+
echo "Invalid severity thresholds: min-severity '$SCG_MIN_SEVERITY' would hide findings required by fail-on '$FAIL_SEVERITY'." >&2
|
|
117
|
+
exit 1
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Build the filter flags as an array so values cannot word-split or be
|
|
121
|
+
# parsed by the shell. The scan is read-only, and --no-history prevents
|
|
122
|
+
# it from mutating the repository while producing both report formats.
|
|
123
|
+
FILTER_ARGS=(--no-history --fail-on "$FAIL_SEVERITY")
|
|
75
124
|
if [ -n "$SCG_MIN_SEVERITY" ]; then
|
|
76
125
|
FILTER_ARGS+=(--min-severity "$SCG_MIN_SEVERITY")
|
|
77
126
|
fi
|
|
@@ -79,88 +128,353 @@ runs:
|
|
|
79
128
|
FILTER_ARGS+=(--exclude "$SCG_EXCLUDE_RULES")
|
|
80
129
|
fi
|
|
81
130
|
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
131
|
+
# One scan produces both payloads from the same in-memory ScanReport.
|
|
132
|
+
# The canonical JSON drives policy; stdout carries the requested format.
|
|
133
|
+
SCG_RUN_DIR=$(mktemp -d "$RUNNER_TEMP/scg-action.XXXXXX") || {
|
|
134
|
+
echo "Could not create the isolated scan output directory." >&2
|
|
135
|
+
exit 1
|
|
136
|
+
}
|
|
137
|
+
JSON_FILE="$SCG_RUN_DIR/canonical-report.json"
|
|
138
|
+
FORMAT_FILE="$SCG_RUN_DIR/requested-report"
|
|
139
|
+
FORMAT_ERR="$SCG_RUN_DIR/format.stderr"
|
|
140
|
+
supply-chain-guard scan "$SCG_PATH" --format "$SCG_FORMAT" \
|
|
141
|
+
--json-output "$JSON_FILE" "${FILTER_ARGS[@]}" \
|
|
142
|
+
> "$FORMAT_FILE" 2>"$FORMAT_ERR"
|
|
143
|
+
SCAN_STATUS=$?
|
|
144
|
+
set -e
|
|
145
|
+
JSON_STATUS=$SCAN_STATUS
|
|
146
|
+
FORMAT_STATUS=$SCAN_STATUS
|
|
147
|
+
# Validate every field consumed by the gate or published through a
|
|
148
|
+
# single-line output. Exact per-severity counts prevent a malformed
|
|
149
|
+
# summary from under-reporting a finding and passing the threshold.
|
|
150
|
+
if ! jq -e '
|
|
151
|
+
def nonnegint: type == "number" and . >= 0 and floor == .;
|
|
152
|
+
def severity_score:
|
|
153
|
+
if . == "critical" then 25
|
|
154
|
+
elif . == "high" then 15
|
|
155
|
+
elif . == "medium" then 5
|
|
156
|
+
elif . == "low" then 2
|
|
157
|
+
else 1
|
|
158
|
+
end;
|
|
159
|
+
def minimum_visible_score:
|
|
160
|
+
[.findings[]
|
|
161
|
+
| select(.rule != "CRITICAL_FINDING_NO_OWNER"
|
|
162
|
+
and .rule != "RISK_STAGNATION_HIGH")
|
|
163
|
+
| { rule: .rule, score: (.severity | severity_score) }]
|
|
164
|
+
| group_by(.rule)
|
|
165
|
+
| map(map(.score) | max)
|
|
166
|
+
| (add // 0)
|
|
167
|
+
| if . > 100 then 100 else . end;
|
|
168
|
+
def coverage_rule:
|
|
169
|
+
. == "FILE_TOO_LARGE_SKIPPED"
|
|
170
|
+
or . == "PATTERN_SCAN_INCOMPLETE"
|
|
171
|
+
or . == "PATH_SCAN_INCOMPLETE"
|
|
172
|
+
or . == "INTERNAL_DISCLOSURE_TRUNCATED"
|
|
173
|
+
or . == "PYPI_NO_SOURCE"
|
|
174
|
+
or . == "NPM_NO_ARTIFACT"
|
|
175
|
+
or . == "INTERNAL_DENYLIST_UNAVAILABLE"
|
|
176
|
+
or . == "INTERNAL_DENYLIST_INVALID_ENTRY"
|
|
177
|
+
or . == "POLICY_INVALID_INTERNAL_TERM";
|
|
178
|
+
type == "object"
|
|
179
|
+
and (.summary | type == "object")
|
|
180
|
+
and (.findings | type == "array")
|
|
181
|
+
and ([.summary.totalFiles, .summary.filesScanned, .summary.critical,
|
|
182
|
+
.summary.high, .summary.medium, .summary.low, .summary.info]
|
|
183
|
+
| all(nonnegint))
|
|
184
|
+
and (.summary.filesScanned <= .summary.totalFiles)
|
|
185
|
+
and (.score | nonnegint and . <= 100)
|
|
186
|
+
and (.score >= (minimum_visible_score))
|
|
187
|
+
and (.riskLevel == "clean" or .riskLevel == "low" or .riskLevel == "medium"
|
|
188
|
+
or .riskLevel == "high" or .riskLevel == "critical")
|
|
189
|
+
and ((.score == 0 and .riskLevel == "clean")
|
|
190
|
+
or (.score >= 1 and .score <= 10 and .riskLevel == "low")
|
|
191
|
+
or (.score >= 11 and .score <= 30 and .riskLevel == "medium")
|
|
192
|
+
or (.score >= 31 and .score <= 60 and .riskLevel == "high")
|
|
193
|
+
or (.score >= 61 and .riskLevel == "critical"))
|
|
194
|
+
and ((has("partialScan") | not) or (.partialScan | type == "boolean"))
|
|
195
|
+
and all(.findings[];
|
|
196
|
+
type == "object"
|
|
197
|
+
and (.rule | type == "string" and length > 0)
|
|
198
|
+
and (.severity == "critical" or .severity == "high" or .severity == "medium"
|
|
199
|
+
or .severity == "low" or .severity == "info"))
|
|
200
|
+
and (([.findings[] | select(.rule | coverage_rule)] | length) == 0
|
|
201
|
+
or .partialScan == true)
|
|
202
|
+
and (.summary.critical == ([.findings[] | select(.severity == "critical")] | length))
|
|
203
|
+
and (.summary.high == ([.findings[] | select(.severity == "high")] | length))
|
|
204
|
+
and (.summary.medium == ([.findings[] | select(.severity == "medium")] | length))
|
|
205
|
+
and (.summary.low == ([.findings[] | select(.severity == "low")] | length))
|
|
206
|
+
and (.summary.info == ([.findings[] | select(.severity == "info")] | length))
|
|
207
|
+
' "$JSON_FILE" >/dev/null; then
|
|
208
|
+
echo "supply-chain-guard did not produce a valid JSON scan report; failing closed." >&2
|
|
209
|
+
exit 1
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# Extract only schema-validated scalar values.
|
|
213
|
+
SCORE=$(jq -r '.score' "$JSON_FILE")
|
|
214
|
+
PARTIAL_SCAN=$(jq -r '.partialScan // false' "$JSON_FILE")
|
|
215
|
+
RISK_LEVEL=$(jq -r '.riskLevel' "$JSON_FILE")
|
|
216
|
+
FINDINGS_COUNT=$(jq -r '.findings | length' "$JSON_FILE")
|
|
217
|
+
if [ "$PARTIAL_SCAN" = "true" ]; then
|
|
218
|
+
RISK_LEVEL="partial"
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
# Validate that the requested formatter produced its documented payload,
|
|
222
|
+
# not an error message whose exit code happens to equal a finding gate.
|
|
223
|
+
FORMAT_VALID=false
|
|
224
|
+
case "$SCG_FORMAT" in
|
|
225
|
+
json)
|
|
226
|
+
jq -s -e 'length == 2 and .[0] == .[1]' "$FORMAT_FILE" "$JSON_FILE" >/dev/null && FORMAT_VALID=true
|
|
227
|
+
;;
|
|
228
|
+
sarif)
|
|
229
|
+
jq -e '.version == "2.1.0" and (.runs | type == "array") and (.runs[0].tool.driver.name == "supply-chain-guard") and (.runs[0].results | type == "array")' "$FORMAT_FILE" >/dev/null && FORMAT_VALID=true
|
|
230
|
+
;;
|
|
231
|
+
sbom)
|
|
232
|
+
jq -e '.bomFormat == "CycloneDX" and .specVersion == "1.6" and (.vulnerabilities | type == "array")' "$FORMAT_FILE" >/dev/null && FORMAT_VALID=true
|
|
233
|
+
;;
|
|
234
|
+
badge)
|
|
235
|
+
jq -e '.schemaVersion == 1 and .label == "supply-chain-guard" and (.message | type == "string") and (.color | type == "string")' "$FORMAT_FILE" >/dev/null && FORMAT_VALID=true
|
|
236
|
+
;;
|
|
237
|
+
gitlab)
|
|
238
|
+
jq -e '.version == "15.2.4" and .scan.scanner.name == "supply-chain-guard" and (.vulnerabilities | type == "array")' "$FORMAT_FILE" >/dev/null && FORMAT_VALID=true
|
|
239
|
+
;;
|
|
240
|
+
text)
|
|
241
|
+
grep -Fq 'supply-chain-guard' "$FORMAT_FILE" && grep -Fq 'FINDINGS SUMMARY' "$FORMAT_FILE" && FORMAT_VALID=true
|
|
242
|
+
;;
|
|
243
|
+
markdown)
|
|
244
|
+
grep -Fq 'supply-chain-guard Scan Report' "$FORMAT_FILE" && grep -Fq '| Property | Value |' "$FORMAT_FILE" && FORMAT_VALID=true
|
|
245
|
+
;;
|
|
246
|
+
html)
|
|
247
|
+
grep -Fq '<!DOCTYPE html>' "$FORMAT_FILE" && grep -Fq '<h1>supply-chain-guard Scan Report</h1>' "$FORMAT_FILE" && grep -Fq '</html>' "$FORMAT_FILE" && FORMAT_VALID=true
|
|
248
|
+
;;
|
|
249
|
+
junit)
|
|
250
|
+
grep -Fq '<?xml version="1.0" encoding="UTF-8"?>' "$FORMAT_FILE" && grep -Fq '<testsuite name="supply-chain-guard"' "$FORMAT_FILE" && grep -Fq '</testsuite>' "$FORMAT_FILE" && FORMAT_VALID=true
|
|
251
|
+
;;
|
|
252
|
+
esac
|
|
253
|
+
|
|
254
|
+
# The CLI receives the same explicit gate as this Action. Its explicit
|
|
255
|
+
# --fail-on contract exits 1 for a threshold hit (including critical)
|
|
256
|
+
# or partial coverage, and 0 otherwise. The Action maps a critical gate
|
|
257
|
+
# hit to its documented public exit 2 after this reconciliation.
|
|
258
|
+
EXPECTED_STATUS=0
|
|
259
|
+
if [ "$PARTIAL_SCAN" = "true" ]; then
|
|
260
|
+
EXPECTED_STATUS=1
|
|
91
261
|
else
|
|
92
|
-
|
|
93
|
-
|
|
262
|
+
case "$FAIL_SEVERITY" in
|
|
263
|
+
critical) GATE_QUERY='.summary.critical > 0' ;;
|
|
264
|
+
high) GATE_QUERY='.summary.critical + .summary.high > 0' ;;
|
|
265
|
+
medium) GATE_QUERY='.summary.critical + .summary.high + .summary.medium > 0' ;;
|
|
266
|
+
low) GATE_QUERY='.summary.critical + .summary.high + .summary.medium + .summary.low > 0' ;;
|
|
267
|
+
info) GATE_QUERY='.summary.critical + .summary.high + .summary.medium + .summary.low + .summary.info > 0' ;;
|
|
268
|
+
esac
|
|
269
|
+
if jq -e "$GATE_QUERY" "$JSON_FILE" >/dev/null; then
|
|
270
|
+
EXPECTED_STATUS=1
|
|
271
|
+
fi
|
|
94
272
|
fi
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
273
|
+
PASSES_CONSISTENT=false
|
|
274
|
+
if [ "$FORMAT_VALID" = "true" ] \
|
|
275
|
+
&& [ "$JSON_STATUS" -eq "$EXPECTED_STATUS" ] \
|
|
276
|
+
&& [ "$FORMAT_STATUS" -eq "$EXPECTED_STATUS" ]; then
|
|
277
|
+
PASSES_CONSISTENT=true
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# Keep the complete report on disk. GitHub limits all step outputs in a
|
|
281
|
+
# job to roughly 1 MB (UTF-16 accounting), so publish at most 400 KB and
|
|
282
|
+
# expose the runner-local path for callers that need the complete file.
|
|
283
|
+
REPORT_PATH="$SCG_RUN_DIR/report.txt"
|
|
284
|
+
cp "$FORMAT_FILE" "$REPORT_PATH"
|
|
285
|
+
REPORT_BYTES=$(wc -c < "$REPORT_PATH")
|
|
286
|
+
REPORT_TRUNCATED=false
|
|
287
|
+
if [ "$REPORT_BYTES" -gt 400000 ]; then
|
|
288
|
+
REPORT_TRUNCATED=true
|
|
289
|
+
fi
|
|
290
|
+
|
|
291
|
+
REPORT_DELIM="SCG_REPORT_$(node -p "require('node:crypto').randomUUID()")"
|
|
111
292
|
{
|
|
293
|
+
echo "score=$SCORE"
|
|
294
|
+
echo "risk-level=$RISK_LEVEL"
|
|
295
|
+
echo "findings-count=$FINDINGS_COUNT"
|
|
296
|
+
echo "partial-scan=$PARTIAL_SCAN"
|
|
297
|
+
echo "report-valid=true"
|
|
298
|
+
echo "passes-consistent=$PASSES_CONSISTENT"
|
|
299
|
+
echo "report-path=$REPORT_PATH"
|
|
300
|
+
echo "report-truncated=$REPORT_TRUNCATED"
|
|
112
301
|
echo "report<<$REPORT_DELIM"
|
|
113
|
-
|
|
302
|
+
if [ "$REPORT_TRUNCATED" = "true" ]; then
|
|
303
|
+
echo "Report exceeds the safe GitHub output limit ($REPORT_BYTES bytes). The complete report is retained at the report-path output."
|
|
304
|
+
else
|
|
305
|
+
cat "$REPORT_PATH"
|
|
306
|
+
fi
|
|
114
307
|
echo "$REPORT_DELIM"
|
|
115
|
-
} >> $GITHUB_OUTPUT
|
|
308
|
+
} >> "$GITHUB_OUTPUT"
|
|
116
309
|
|
|
117
|
-
#
|
|
118
|
-
|
|
310
|
+
# Keep logs bounded as well as step outputs. The complete report remains
|
|
311
|
+
# available through report-path for later steps in this job.
|
|
312
|
+
LOG_BYTE_LIMIT=200000
|
|
313
|
+
LOG_COMMAND_TOKEN="SCG_LOG_$(node -p "require('node:crypto').randomUUID()")"
|
|
314
|
+
echo "::stop-commands::$LOG_COMMAND_TOKEN"
|
|
315
|
+
if [ "$REPORT_BYTES" -gt "$LOG_BYTE_LIMIT" ]; then
|
|
316
|
+
head -c "$LOG_BYTE_LIMIT" "$REPORT_PATH"
|
|
317
|
+
echo
|
|
318
|
+
echo "[supply-chain-guard report log truncated at $LOG_BYTE_LIMIT bytes]"
|
|
319
|
+
else
|
|
320
|
+
cat "$REPORT_PATH"
|
|
321
|
+
fi
|
|
322
|
+
echo
|
|
323
|
+
echo "::$LOG_COMMAND_TOKEN::"
|
|
119
324
|
|
|
120
|
-
#
|
|
121
|
-
|
|
325
|
+
# A malformed formatter payload or command-status disagreement is an
|
|
326
|
+
# operational failure, even when exit 1 could also mean findings.
|
|
327
|
+
if [ "$PASSES_CONSISTENT" != "true" ]; then
|
|
328
|
+
if [ -s "$FORMAT_ERR" ]; then
|
|
329
|
+
ERROR_COMMAND_TOKEN="SCG_ERR_$(node -p "require('node:crypto').randomUUID()")"
|
|
330
|
+
echo "::stop-commands::$ERROR_COMMAND_TOKEN"
|
|
331
|
+
head -c "$LOG_BYTE_LIMIT" "$FORMAT_ERR"
|
|
332
|
+
echo
|
|
333
|
+
echo "::$ERROR_COMMAND_TOKEN::"
|
|
334
|
+
fi
|
|
335
|
+
echo "Scan status or requested-format payload was invalid; failing closed (scan=$SCAN_STATUS, expected=$EXPECTED_STATUS)." >&2
|
|
336
|
+
exit 1
|
|
337
|
+
fi
|
|
338
|
+
|
|
339
|
+
# Determine if we should fail from validated counts.
|
|
122
340
|
case "$FAIL_SEVERITY" in
|
|
123
341
|
critical)
|
|
124
|
-
|
|
342
|
+
jq -e '.summary.critical > 0' "$JSON_FILE" >/dev/null && exit 2
|
|
125
343
|
;;
|
|
126
344
|
high)
|
|
127
|
-
|
|
345
|
+
jq -e '.summary.critical + .summary.high > 0' "$JSON_FILE" >/dev/null && exit 1
|
|
128
346
|
;;
|
|
129
347
|
medium)
|
|
130
|
-
|
|
348
|
+
jq -e '.summary.critical + .summary.high + .summary.medium > 0' "$JSON_FILE" >/dev/null && exit 1
|
|
131
349
|
;;
|
|
132
350
|
low)
|
|
133
|
-
|
|
351
|
+
jq -e '.summary.critical + .summary.high + .summary.medium + .summary.low > 0' "$JSON_FILE" >/dev/null && exit 1
|
|
352
|
+
;;
|
|
353
|
+
info)
|
|
354
|
+
jq -e '.summary.critical + .summary.high + .summary.medium + .summary.low + .summary.info > 0' "$JSON_FILE" >/dev/null && exit 1
|
|
134
355
|
;;
|
|
135
356
|
esac
|
|
136
357
|
|
|
358
|
+
# Coverage failures are independent of the finding-severity threshold.
|
|
359
|
+
# A partial scan can never prove that no threshold finding exists.
|
|
360
|
+
if [ "$PARTIAL_SCAN" = "true" ]; then
|
|
361
|
+
exit 1
|
|
362
|
+
fi
|
|
363
|
+
|
|
137
364
|
exit 0
|
|
138
365
|
|
|
139
366
|
- name: Comment on PR
|
|
140
|
-
if: inputs
|
|
141
|
-
uses: actions/github-script@v9
|
|
367
|
+
if: always() && inputs['comment-on-pr'] == 'true' && github.event_name == 'pull_request'
|
|
368
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
142
369
|
continue-on-error: true
|
|
370
|
+
env:
|
|
371
|
+
SCG_PARTIAL_SCAN: ${{ steps.scan.outputs['partial-scan'] }}
|
|
372
|
+
SCG_REPORT_VALID: ${{ steps.scan.outputs['report-valid'] }}
|
|
373
|
+
SCG_PASSES_CONSISTENT: ${{ steps.scan.outputs['passes-consistent'] }}
|
|
374
|
+
SCG_FINDINGS_COUNT: ${{ steps.scan.outputs['findings-count'] }}
|
|
375
|
+
SCG_SCAN_OUTCOME: ${{ steps.scan.outcome }}
|
|
376
|
+
SCG_REPORT_PATH: ${{ steps.scan.outputs['report-path'] }}
|
|
143
377
|
with:
|
|
144
378
|
script: |
|
|
145
379
|
const fs = require('fs');
|
|
380
|
+
const marker = '## supply-chain-guard Scan Report';
|
|
381
|
+
const reportValid = process.env.SCG_REPORT_VALID === 'true';
|
|
382
|
+
const passesConsistent = process.env.SCG_PASSES_CONSISTENT === 'true';
|
|
383
|
+
const partial = process.env.SCG_PARTIAL_SCAN === 'true';
|
|
384
|
+
const parsedCount = Number.parseInt(process.env.SCG_FINDINGS_COUNT ?? '', 10);
|
|
385
|
+
const findingsCount = Number.isSafeInteger(parsedCount) && parsedCount >= 0
|
|
386
|
+
? parsedCount
|
|
387
|
+
: null;
|
|
388
|
+
const scanOutcome = process.env.SCG_SCAN_OUTCOME || 'unavailable';
|
|
389
|
+
|
|
390
|
+
// Read only the bytes that can be useful in a GitHub comment. This
|
|
391
|
+
// keeps a maliciously large formatter payload from exhausting memory.
|
|
392
|
+
const reportByteLimit = 60000;
|
|
146
393
|
let report = '';
|
|
394
|
+
let reportReadTruncated = false;
|
|
147
395
|
try {
|
|
148
|
-
|
|
396
|
+
const descriptor = fs.openSync(process.env.SCG_REPORT_PATH, 'r');
|
|
397
|
+
try {
|
|
398
|
+
const buffer = Buffer.alloc(reportByteLimit + 1);
|
|
399
|
+
const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
|
|
400
|
+
reportReadTruncated = bytesRead > reportByteLimit;
|
|
401
|
+
report = buffer.subarray(0, Math.min(bytesRead, reportByteLimit)).toString('utf8').trim();
|
|
402
|
+
} finally {
|
|
403
|
+
fs.closeSync(descriptor);
|
|
404
|
+
}
|
|
149
405
|
} catch (err) {
|
|
150
|
-
console.error('Failed to read report
|
|
406
|
+
console.error('Failed to read the bounded report excerpt:', err);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Escape HTML and neutralize mentions before rendering the report as
|
|
410
|
+
// inert code in the PR comment.
|
|
411
|
+
const escapeReport = (value) => value
|
|
412
|
+
.replace(/&/g, '&')
|
|
413
|
+
.replace(/</g, '<')
|
|
414
|
+
.replace(/>/g, '>')
|
|
415
|
+
.replace(/@/g, '@');
|
|
416
|
+
const escapedReport = escapeReport(report);
|
|
417
|
+
// Treat every scanner-rendered line as inert code. Even the Markdown
|
|
418
|
+
// formatter contains package-controlled text, which must not create
|
|
419
|
+
// links, images, headings, mentions, or other active PR content.
|
|
420
|
+
let reportForComment = escapedReport
|
|
421
|
+
.split(/\r?\n/)
|
|
422
|
+
.map((line) => ` ${line}`)
|
|
423
|
+
.join('\n');
|
|
424
|
+
if (reportReadTruncated && reportForComment) {
|
|
425
|
+
reportForComment += '\n\n> Report excerpt truncated before comment construction.';
|
|
151
426
|
}
|
|
152
|
-
|
|
427
|
+
|
|
428
|
+
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
153
429
|
owner: context.repo.owner,
|
|
154
430
|
repo: context.repo.repo,
|
|
155
431
|
issue_number: context.issue.number,
|
|
432
|
+
per_page: 100,
|
|
156
433
|
});
|
|
157
434
|
|
|
158
|
-
// Update existing comment or create new one
|
|
159
435
|
const botComment = comments.find(
|
|
160
|
-
(
|
|
436
|
+
(comment) => comment.user?.login === 'github-actions[bot]'
|
|
437
|
+
&& typeof comment.body === 'string'
|
|
438
|
+
&& comment.body.startsWith(`${marker}\n`)
|
|
161
439
|
);
|
|
440
|
+
const clean = reportValid && passesConsistent && !partial && findingsCount === 0 && scanOutcome === 'success';
|
|
162
441
|
|
|
163
|
-
|
|
442
|
+
// Do not add noise for a first clean scan, but replace a stale finding
|
|
443
|
+
// or partial comment when the repository returns to a complete clean state.
|
|
444
|
+
if (clean && !botComment) {
|
|
445
|
+
core.info('Complete clean scan; no existing scan comment to update.');
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const canonicalLine = findingsCount === null
|
|
450
|
+
? ''
|
|
451
|
+
: `> Canonical JSON report: **${findingsCount} reportable finding(s)**.`;
|
|
452
|
+
let detail;
|
|
453
|
+
if (!reportValid) {
|
|
454
|
+
detail = `> WARNING: The scanner did not produce a validated report (step outcome: ${scanOutcome}). No clean verdict is available.`;
|
|
455
|
+
} else if (findingsCount === null) {
|
|
456
|
+
detail = '> WARNING: Scan metadata is unavailable. No clean verdict is available.';
|
|
457
|
+
} else if (!passesConsistent) {
|
|
458
|
+
detail = `> WARNING: The requested formatter or canonical JSON reconciliation failed. No clean verdict is available.\n\n${canonicalLine}`;
|
|
459
|
+
if (reportForComment) detail += `\n\n${reportForComment}`;
|
|
460
|
+
} else if (partial) {
|
|
461
|
+
detail = `> WARNING: Scan incomplete. Coverage is partial; no clean verdict is available.\n\n${canonicalLine}`;
|
|
462
|
+
if (reportForComment) detail += `\n\n${reportForComment}`;
|
|
463
|
+
} else if (findingsCount > 0) {
|
|
464
|
+
detail = `> WARNING: Findings were detected.\n\n${canonicalLine}`;
|
|
465
|
+
if (reportForComment) detail += `\n\n${reportForComment}`;
|
|
466
|
+
} else if (scanOutcome !== 'success') {
|
|
467
|
+
detail = `> WARNING: The scan step ended with ${scanOutcome} after producing a report. No clean verdict is available.\n\n${canonicalLine}`;
|
|
468
|
+
} else {
|
|
469
|
+
detail = `${canonicalLine}\n\n${reportForComment || '> Complete scan: no reportable malicious indicators detected.'}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const maxDetailChars = 55000;
|
|
473
|
+
if (detail.length > maxDetailChars) {
|
|
474
|
+
detail = detail.slice(0, maxDetailChars)
|
|
475
|
+
+ '\n\n> Comment truncated. See the bounded job-log excerpt or consume the report-path output in a later step.';
|
|
476
|
+
}
|
|
477
|
+
const body = `${marker}\n\n${detail}`;
|
|
164
478
|
|
|
165
479
|
if (botComment) {
|
|
166
480
|
await github.rest.issues.updateComment({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentic-workflow-scanner.d.ts","sourceRoot":"","sources":["../src/agentic-workflow-scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAY,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"agentic-workflow-scanner.d.ts","sourceRoot":"","sources":["../src/agentic-workflow-scanner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAY,MAAM,YAAY,CAAC;AAyDpD,6DAA6D;AAC7D,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,EAAE,CA8B3D"}
|
|
@@ -49,6 +49,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
49
49
|
const path = __importStar(require("node:path"));
|
|
50
50
|
const workflow_ast_js_1 = require("./workflow-ast.js");
|
|
51
51
|
const patterns_js_1 = require("./patterns.js");
|
|
52
|
+
const pattern_scanner_js_1 = require("./pattern-scanner.js");
|
|
52
53
|
/** Triggers that feed attacker-controllable text into the agent. */
|
|
53
54
|
const UNTRUSTED_TRIGGERS = [
|
|
54
55
|
"issues", "issue_comment", "pull_request", "pull_request_target",
|
|
@@ -152,17 +153,16 @@ function scanOne(frontmatter, body, rel, findings) {
|
|
|
152
153
|
`(custom token secret or explicit repo list). Combined with a public-post tool this is the ` +
|
|
153
154
|
`full GitLost lethal trifecta.`, rel, 1);
|
|
154
155
|
}
|
|
155
|
-
// LLM control tokens hidden in the instruction body.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
}
|
|
156
|
+
// LLM control tokens hidden in the instruction body. File/path gates target
|
|
157
|
+
// ordinary repository files and are intentionally irrelevant after this
|
|
158
|
+
// scanner has selected the agent-facing instruction body.
|
|
159
|
+
for (const pattern of patterns_js_1.PROMPT_INJECTION_PATTERNS) {
|
|
160
|
+
const hits = (0, pattern_scanner_js_1.matchPatternInSemanticText)(pattern, body, rel, findings, "i");
|
|
161
|
+
const hit = hits?.[0];
|
|
162
|
+
if (!hit)
|
|
163
|
+
continue;
|
|
164
|
+
push(findings, "AGENTIC_WF_PROMPT_INJECTION", "high", `LLM control token / override phrase in an Agentic Workflow instruction body ` +
|
|
165
|
+
`(matched ${pattern.rule}). Agents read the body verbatim.`, rel, hit.line);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
//# sourceMappingURL=agentic-workflow-scanner.js.map
|