awguard 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/Dockerfile +8 -0
- package/README.md +75 -1
- package/action.yml +2 -2
- package/docs/assets/terminal-demo.svg +19 -0
- package/docs/comparison.md +23 -0
- package/docs/launch-plan.md +28 -13
- package/docs/market-analysis.md +20 -0
- package/docs/marketplace-listing.md +40 -0
- package/docs/roadmap.md +75 -0
- package/docs/site/index.html +251 -0
- package/examples/.gitlab-ci.yml +6 -0
- package/examples/.vscode/tasks.json +17 -0
- package/examples/README.md +5 -0
- package/examples/awguard.config.example.json +6 -0
- package/examples/lab/README.md +27 -0
- package/examples/lab/fixed/.github/workflows/ai-triage.yml +20 -0
- package/examples/lab/fixed/.mcp.json +12 -0
- package/examples/lab/fixed/AGENTS.md +5 -0
- package/examples/lab/unsafe/.github/workflows/ai-triage.yml +16 -0
- package/examples/lab/unsafe/.mcp.json +11 -0
- package/examples/lab/unsafe/AGENTS.md +4 -0
- package/examples/pre-commit-config.yaml +8 -0
- package/package.json +2 -1
- package/src/cli.js +63 -3
- package/src/compare.js +110 -0
- package/src/config.js +29 -2
- package/src/graph.js +6 -1
- package/src/init.js +81 -0
- package/src/inventory.js +159 -0
- package/src/migration.js +10 -0
- package/src/presets.js +2 -1
- package/src/remediation.js +19 -0
- package/src/reporters.js +10 -1
- package/src/scanner.js +119 -5
- package/src/score.js +3 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta
|
|
7
|
+
name="description"
|
|
8
|
+
content="Agentic Workflow Guard maps AI-agent workflow, instruction, and MCP trust boundaries in repositories."
|
|
9
|
+
>
|
|
10
|
+
<title>Agentic Workflow Guard</title>
|
|
11
|
+
<style>
|
|
12
|
+
:root {
|
|
13
|
+
color-scheme: light;
|
|
14
|
+
--ink: #17212b;
|
|
15
|
+
--muted: #5f6e7b;
|
|
16
|
+
--line: #d9e1e8;
|
|
17
|
+
--paper: #f7fafc;
|
|
18
|
+
--panel: #ffffff;
|
|
19
|
+
--accent: #0f766e;
|
|
20
|
+
--accent-strong: #134e4a;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
* {
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
margin: 0;
|
|
29
|
+
background: var(--paper);
|
|
30
|
+
color: var(--ink);
|
|
31
|
+
font-family:
|
|
32
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
33
|
+
line-height: 1.55;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
a {
|
|
37
|
+
color: var(--accent-strong);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.hero {
|
|
41
|
+
border-bottom: 1px solid var(--line);
|
|
42
|
+
background: var(--panel);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.wrap {
|
|
46
|
+
width: min(1120px, calc(100% - 40px));
|
|
47
|
+
margin: 0 auto;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.hero .wrap {
|
|
51
|
+
display: grid;
|
|
52
|
+
grid-template-columns: minmax(0, 1.05fr) minmax(320px, 0.95fr);
|
|
53
|
+
gap: 44px;
|
|
54
|
+
align-items: center;
|
|
55
|
+
min-height: 86vh;
|
|
56
|
+
padding: 64px 0 40px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.eyebrow {
|
|
60
|
+
margin: 0 0 14px;
|
|
61
|
+
color: var(--accent);
|
|
62
|
+
font-size: 0.78rem;
|
|
63
|
+
font-weight: 750;
|
|
64
|
+
letter-spacing: 0;
|
|
65
|
+
text-transform: uppercase;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
h1 {
|
|
69
|
+
margin: 0;
|
|
70
|
+
max-width: 820px;
|
|
71
|
+
font-size: clamp(2.45rem, 7vw, 5.6rem);
|
|
72
|
+
line-height: 0.96;
|
|
73
|
+
letter-spacing: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.lead {
|
|
77
|
+
max-width: 680px;
|
|
78
|
+
margin: 24px 0 0;
|
|
79
|
+
color: var(--muted);
|
|
80
|
+
font-size: 1.18rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.actions {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-wrap: wrap;
|
|
86
|
+
gap: 12px;
|
|
87
|
+
margin-top: 30px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.button {
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
min-height: 44px;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
border: 1px solid var(--line);
|
|
96
|
+
border-radius: 8px;
|
|
97
|
+
padding: 10px 15px;
|
|
98
|
+
background: var(--panel);
|
|
99
|
+
color: var(--ink);
|
|
100
|
+
font-weight: 720;
|
|
101
|
+
text-decoration: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.button.primary {
|
|
105
|
+
border-color: var(--accent);
|
|
106
|
+
background: var(--accent);
|
|
107
|
+
color: #ffffff;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.terminal {
|
|
111
|
+
width: 100%;
|
|
112
|
+
border: 1px solid var(--line);
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
background: #0c1117;
|
|
115
|
+
box-shadow: 0 18px 45px rgb(23 33 43 / 12%);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
section {
|
|
119
|
+
padding: 46px 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
h2 {
|
|
123
|
+
margin: 0 0 18px;
|
|
124
|
+
font-size: 1.35rem;
|
|
125
|
+
letter-spacing: 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.grid {
|
|
129
|
+
display: grid;
|
|
130
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
131
|
+
gap: 16px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.item {
|
|
135
|
+
min-height: 144px;
|
|
136
|
+
border: 1px solid var(--line);
|
|
137
|
+
border-radius: 8px;
|
|
138
|
+
padding: 18px;
|
|
139
|
+
background: var(--panel);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.item h3 {
|
|
143
|
+
margin: 0 0 8px;
|
|
144
|
+
font-size: 1rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.item p {
|
|
148
|
+
margin: 0;
|
|
149
|
+
color: var(--muted);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
code {
|
|
153
|
+
border: 1px solid var(--line);
|
|
154
|
+
border-radius: 6px;
|
|
155
|
+
padding: 2px 5px;
|
|
156
|
+
background: #eef4f8;
|
|
157
|
+
font-size: 0.9em;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
footer {
|
|
161
|
+
border-top: 1px solid var(--line);
|
|
162
|
+
padding: 24px 0 34px;
|
|
163
|
+
color: var(--muted);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@media (max-width: 850px) {
|
|
167
|
+
.hero .wrap {
|
|
168
|
+
grid-template-columns: 1fr;
|
|
169
|
+
gap: 30px;
|
|
170
|
+
min-height: auto;
|
|
171
|
+
padding-top: 46px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.grid {
|
|
175
|
+
grid-template-columns: 1fr;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
</style>
|
|
179
|
+
</head>
|
|
180
|
+
<body>
|
|
181
|
+
<main>
|
|
182
|
+
<header class="hero">
|
|
183
|
+
<div class="wrap">
|
|
184
|
+
<div>
|
|
185
|
+
<p class="eyebrow">AI workflow security scanner</p>
|
|
186
|
+
<h1>Agentic Workflow Guard</h1>
|
|
187
|
+
<p class="lead">
|
|
188
|
+
Map every place a repository gives AI agents instructions, tools, secrets, or write power,
|
|
189
|
+
then turn that map into findings, reports, and safer pull request checks.
|
|
190
|
+
</p>
|
|
191
|
+
<div class="actions">
|
|
192
|
+
<a class="button primary" href="https://github.com/Mughal-Baig/agentic-workflow-guard">GitHub</a>
|
|
193
|
+
<a class="button" href="https://www.npmjs.com/package/awguard">npm</a>
|
|
194
|
+
<a class="button" href="https://github.com/Mughal-Baig/agentic-workflow-guard/blob/main/docs/comparison.md">Comparison</a>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
<img
|
|
198
|
+
class="terminal"
|
|
199
|
+
src="assets/terminal-demo.svg"
|
|
200
|
+
alt="AWGuard terminal demo showing inventory, score, migration, and graph reports"
|
|
201
|
+
>
|
|
202
|
+
</div>
|
|
203
|
+
</header>
|
|
204
|
+
|
|
205
|
+
<section>
|
|
206
|
+
<div class="wrap">
|
|
207
|
+
<h2>What It Scans</h2>
|
|
208
|
+
<div class="grid">
|
|
209
|
+
<article class="item">
|
|
210
|
+
<h3>Agent Instructions</h3>
|
|
211
|
+
<p>Finds AGENTS.md, Copilot instructions, custom agents, prompts, and reusable skills.</p>
|
|
212
|
+
</article>
|
|
213
|
+
<article class="item">
|
|
214
|
+
<h3>Automation Paths</h3>
|
|
215
|
+
<p>Reviews GitHub Actions and other workflow files for unsafe agent execution boundaries.</p>
|
|
216
|
+
</article>
|
|
217
|
+
<article class="item">
|
|
218
|
+
<h3>MCP Trust</h3>
|
|
219
|
+
<p>Flags unapproved MCP servers, package launches, command tools, and environment exposure.</p>
|
|
220
|
+
</article>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</section>
|
|
224
|
+
|
|
225
|
+
<section>
|
|
226
|
+
<div class="wrap">
|
|
227
|
+
<h2>Reports Built For Adoption</h2>
|
|
228
|
+
<div class="grid">
|
|
229
|
+
<article class="item">
|
|
230
|
+
<h3>Inventory</h3>
|
|
231
|
+
<p><code>--format inventory</code> and <code>inventory-json</code> explain the agentic surface.</p>
|
|
232
|
+
</article>
|
|
233
|
+
<article class="item">
|
|
234
|
+
<h3>Risk Score</h3>
|
|
235
|
+
<p><code>--format score</code> gives teams a compact AWI score they can track over time.</p>
|
|
236
|
+
</article>
|
|
237
|
+
<article class="item">
|
|
238
|
+
<h3>Compare</h3>
|
|
239
|
+
<p><code>--compare old.json new.json</code> shows introduced and resolved findings between scans.</p>
|
|
240
|
+
</article>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</section>
|
|
244
|
+
</main>
|
|
245
|
+
<footer>
|
|
246
|
+
<div class="wrap">
|
|
247
|
+
Released as open source. Start with <code>npx awguard@latest init</code>.
|
|
248
|
+
</div>
|
|
249
|
+
</footer>
|
|
250
|
+
</body>
|
|
251
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.0",
|
|
3
|
+
"tasks": [
|
|
4
|
+
{
|
|
5
|
+
"label": "awguard inventory",
|
|
6
|
+
"type": "shell",
|
|
7
|
+
"command": "npx awguard@latest . --format inventory",
|
|
8
|
+
"problemMatcher": []
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"label": "awguard scan",
|
|
12
|
+
"type": "shell",
|
|
13
|
+
"command": "npx awguard@latest . --fail-on high",
|
|
14
|
+
"problemMatcher": []
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
package/examples/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
- `.github/copilot-instructions.md`: demonstrates risky persistent agent instruction guidance.
|
|
8
8
|
- `.mcp.json`: demonstrates mutable MCP server packages and committed MCP credentials.
|
|
9
9
|
- `awguard.config.example.json`: sample config with a strict preset and overrides.
|
|
10
|
+
- `lab/`: vulnerable and fixed mini-repositories for demos.
|
|
11
|
+
- `.gitlab-ci.yml`, `pre-commit-config.yaml`, `.vscode/tasks.json`: adoption examples for other workflows.
|
|
10
12
|
|
|
11
13
|
Try:
|
|
12
14
|
|
|
@@ -14,9 +16,12 @@ Try:
|
|
|
14
16
|
node ../bin/awguard.js unsafe-agent.yml --format graph
|
|
15
17
|
node ../bin/awguard.js unsafe-agent.yml --format html --output awguard-report.html
|
|
16
18
|
node ../bin/awguard.js unsafe-agent.yml --format migration
|
|
19
|
+
node ../bin/awguard.js . --format inventory
|
|
20
|
+
node ../bin/awguard.js . --format inventory-json
|
|
17
21
|
node ../bin/awguard.js unsafe-agent.yml --format score
|
|
18
22
|
node ../bin/awguard.js safe-agent.yml --format badge
|
|
19
23
|
node ../bin/awguard.js .mcp.json --format text
|
|
20
24
|
node ../bin/awguard.js . --format text
|
|
25
|
+
node ../bin/awguard.js init
|
|
21
26
|
node ../bin/awguard.js unsafe-agent.yml --fix-dry-run
|
|
22
27
|
```
|
|
@@ -10,5 +10,11 @@
|
|
|
10
10
|
"suppressions": {
|
|
11
11
|
"allowedRules": ["AWG001", "AWG002"],
|
|
12
12
|
"minimumReasonLength": 20
|
|
13
|
+
},
|
|
14
|
+
"policy": {
|
|
15
|
+
"approvedFiles": ["AGENTS.md", ".github/workflows/*"],
|
|
16
|
+
"approvedMcpServers": ["github"],
|
|
17
|
+
"approvedMcpPackages": ["@modelcontextprotocol/server-github@1.2.3"],
|
|
18
|
+
"approvedMcpCommands": ["npx", "node"]
|
|
13
19
|
}
|
|
14
20
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Vulnerable Lab
|
|
2
|
+
|
|
3
|
+
This lab gives maintainers a tiny before/after set for demos, screenshots, and testing.
|
|
4
|
+
|
|
5
|
+
## Unsafe
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx awguard@latest examples/lab/unsafe --format inventory
|
|
9
|
+
npx awguard@latest examples/lab/unsafe --format graph
|
|
10
|
+
npx awguard@latest examples/lab/unsafe --fix-dry-run
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The unsafe version includes:
|
|
14
|
+
|
|
15
|
+
- an AI triage workflow that reads issue comments;
|
|
16
|
+
- broad token permissions;
|
|
17
|
+
- an unsafe persistent agent instruction;
|
|
18
|
+
- a mutable MCP server with a committed token-shaped value.
|
|
19
|
+
|
|
20
|
+
## Fixed
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx awguard@latest examples/lab/fixed --format inventory
|
|
24
|
+
npx awguard@latest examples/lab/fixed --fail-on high
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The fixed version uses read-only workflow permissions, conservative agent instructions, and pinned MCP package startup with prompted credentials.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Safer AI triage
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issue_comment:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
triage:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- name: Capture comment as untrusted data
|
|
14
|
+
env:
|
|
15
|
+
USER_TEXT: ${{ github.event.comment.body }} # awguard-disable-line AWG001 -- Reviewed: captured as data and used only in read-only suggestion mode.
|
|
16
|
+
run: |
|
|
17
|
+
printf '%s\n' "$USER_TEXT" > untrusted-input.txt
|
|
18
|
+
- name: Ask agent for suggestion only
|
|
19
|
+
run: |
|
|
20
|
+
codex --approval-mode suggest --prompt-file untrusted-input.txt
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"inputs": [{ "type": "promptString", "id": "github-token", "password": true }],
|
|
3
|
+
"mcpServers": {
|
|
4
|
+
"github": {
|
|
5
|
+
"command": "npx",
|
|
6
|
+
"args": ["-y", "@modelcontextprotocol/server-github@1.2.3"],
|
|
7
|
+
"env": {
|
|
8
|
+
"GITHUB_TOKEN": "${input:github-token}"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Unsafe AI triage
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issue_comment:
|
|
5
|
+
|
|
6
|
+
permissions: write-all
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
triage:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- env:
|
|
14
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
15
|
+
run: |
|
|
16
|
+
codex --dangerously-skip-permissions --prompt "${{ github.event.comment.body }}"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "awguard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Scan GitHub Actions workflows, agent instructions, and MCP configs for AI-agent injection and unsafe tool boundaries.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Mughal-Baig/agentic-workflow-guard#readme",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"action.yml",
|
|
40
|
+
"Dockerfile",
|
|
40
41
|
"CHANGELOG.md",
|
|
41
42
|
"bin",
|
|
42
43
|
"src",
|
package/src/cli.js
CHANGED
|
@@ -2,17 +2,35 @@ import process from 'node:process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { applyBaseline, createBaseline, loadBaseline, writeBaseline } from './baseline.js';
|
|
5
|
+
import { loadReport, renderComparison } from './compare.js';
|
|
5
6
|
import { loadConfig } from './config.js';
|
|
7
|
+
import { renderInitGuide } from './init.js';
|
|
6
8
|
import { renderFixDryRun } from './remediation.js';
|
|
7
9
|
import { scanWorkflows, severityRank } from './scanner.js';
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
renderBadge,
|
|
12
|
+
renderGithubAnnotations,
|
|
13
|
+
renderGraph,
|
|
14
|
+
renderHtml,
|
|
15
|
+
renderJson,
|
|
16
|
+
renderMarkdown,
|
|
17
|
+
renderMigration,
|
|
18
|
+
renderSarif,
|
|
19
|
+
renderScore,
|
|
20
|
+
renderSurfaceInventory,
|
|
21
|
+
renderSurfaceInventoryJson,
|
|
22
|
+
renderText
|
|
23
|
+
} from './reporters.js';
|
|
9
24
|
|
|
10
25
|
const HELP = `Agentic Workflow Guard
|
|
11
26
|
|
|
12
27
|
Usage:
|
|
13
|
-
awguard [path] [--config file] [--preset name] [--format text|json|markdown|github|sarif|graph|html|migration|score|badge] [--output file] [--baseline file] [--write-baseline file] [--fix-dry-run] [--fail-on none|low|medium|high|critical]
|
|
28
|
+
awguard [path] [--config file] [--preset name] [--format text|json|markdown|github|sarif|graph|html|migration|score|badge|inventory|inventory-json] [--output file] [--baseline file] [--write-baseline file] [--fix-dry-run] [--fail-on none|low|medium|high|critical]
|
|
29
|
+
awguard init
|
|
30
|
+
awguard --compare previous.json current.json
|
|
14
31
|
|
|
15
32
|
Examples:
|
|
33
|
+
awguard init
|
|
16
34
|
awguard .
|
|
17
35
|
awguard .mcp.json
|
|
18
36
|
awguard . --config awguard.config.json
|
|
@@ -20,16 +38,24 @@ Examples:
|
|
|
20
38
|
awguard .github/workflows/agent.yml --format markdown --fail-on high
|
|
21
39
|
awguard . --format html --output awguard-report.html
|
|
22
40
|
awguard . --format migration --output awguard-migration.md
|
|
41
|
+
awguard . --format inventory
|
|
42
|
+
awguard . --format inventory-json --output awguard-inventory.json
|
|
23
43
|
awguard . --format score
|
|
24
44
|
awguard . --format badge --output awguard-badge.json
|
|
25
45
|
awguard . --fix-dry-run
|
|
26
46
|
awguard . --format sarif --output awguard.sarif --fail-on none
|
|
27
47
|
awguard . --write-baseline awguard.baseline.json
|
|
28
48
|
awguard . --baseline awguard.baseline.json --fail-on high
|
|
49
|
+
awguard --compare old-awguard.json new-awguard.json
|
|
29
50
|
awguard . --format github --fail-on medium
|
|
30
51
|
`;
|
|
31
52
|
|
|
32
53
|
export async function runCli(args, env = process.env) {
|
|
54
|
+
if (args[0] === 'init') {
|
|
55
|
+
console.log(renderInitGuide());
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
const options = parseArgs(args, env);
|
|
34
60
|
|
|
35
61
|
if (options.help) {
|
|
@@ -37,6 +63,17 @@ export async function runCli(args, env = process.env) {
|
|
|
37
63
|
return;
|
|
38
64
|
}
|
|
39
65
|
|
|
66
|
+
if (options.compare.length > 0) {
|
|
67
|
+
const output = renderComparison(loadReport(options.compare[0]), loadReport(options.compare[1]));
|
|
68
|
+
if (options.output) {
|
|
69
|
+
const outputFile = writeOutput(options.output, output);
|
|
70
|
+
console.error(`Wrote ${outputFile}`);
|
|
71
|
+
} else {
|
|
72
|
+
console.log(output);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
40
77
|
const { config } = loadConfig({ configPath: options.config, root: options.path, presets: options.presets });
|
|
41
78
|
let result = scanWorkflows({ root: options.path, config });
|
|
42
79
|
|
|
@@ -74,6 +111,7 @@ export function parseArgs(args, env = {}) {
|
|
|
74
111
|
baseline: readInput(env, 'baseline') || '',
|
|
75
112
|
writeBaseline: readInput(env, 'write_baseline') || readInput(env, 'write-baseline') || '',
|
|
76
113
|
config: readInput(env, 'config') || '',
|
|
114
|
+
compare: [],
|
|
77
115
|
presets: splitList(readInput(env, 'preset') || readInput(env, 'presets') || ''),
|
|
78
116
|
fixDryRun: readBoolInput(env, 'fix_dry_run') || readBoolInput(env, 'fix-dry-run'),
|
|
79
117
|
help: false
|
|
@@ -108,6 +146,10 @@ export function parseArgs(args, env = {}) {
|
|
|
108
146
|
options.config = args[++index];
|
|
109
147
|
} else if (arg.startsWith('--config=')) {
|
|
110
148
|
options.config = arg.slice('--config='.length);
|
|
149
|
+
} else if (arg === '--compare') {
|
|
150
|
+
options.compare = [args[++index], args[++index]].filter(Boolean);
|
|
151
|
+
} else if (arg.startsWith('--compare=')) {
|
|
152
|
+
options.compare = arg.slice('--compare='.length).split(',').map((item) => item.trim()).filter(Boolean);
|
|
111
153
|
} else if (arg === '--preset') {
|
|
112
154
|
options.presets.push(...splitList(args[++index]));
|
|
113
155
|
} else if (arg.startsWith('--preset=')) {
|
|
@@ -121,8 +163,24 @@ export function parseArgs(args, env = {}) {
|
|
|
121
163
|
}
|
|
122
164
|
}
|
|
123
165
|
|
|
124
|
-
validateEnum('format', options.format, [
|
|
166
|
+
validateEnum('format', options.format, [
|
|
167
|
+
'text',
|
|
168
|
+
'json',
|
|
169
|
+
'markdown',
|
|
170
|
+
'github',
|
|
171
|
+
'sarif',
|
|
172
|
+
'graph',
|
|
173
|
+
'html',
|
|
174
|
+
'migration',
|
|
175
|
+
'score',
|
|
176
|
+
'badge',
|
|
177
|
+
'inventory',
|
|
178
|
+
'inventory-json'
|
|
179
|
+
]);
|
|
125
180
|
validateEnum('fail-on', options.failOn, ['none', 'low', 'medium', 'high', 'critical']);
|
|
181
|
+
if (options.compare.length !== 0 && options.compare.length !== 2) {
|
|
182
|
+
throw new Error('--compare requires two awguard --format json report files');
|
|
183
|
+
}
|
|
126
184
|
|
|
127
185
|
return options;
|
|
128
186
|
}
|
|
@@ -141,6 +199,8 @@ function render(result, format) {
|
|
|
141
199
|
if (format === 'migration') return renderMigration(result);
|
|
142
200
|
if (format === 'score') return renderScore(result);
|
|
143
201
|
if (format === 'badge') return renderBadge(result);
|
|
202
|
+
if (format === 'inventory') return renderSurfaceInventory(result);
|
|
203
|
+
if (format === 'inventory-json') return renderSurfaceInventoryJson(result);
|
|
144
204
|
if (format === 'github') return renderGithubAnnotations(result);
|
|
145
205
|
return renderText(result);
|
|
146
206
|
}
|
package/src/compare.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function loadReport(file) {
|
|
5
|
+
const absoluteFile = path.resolve(file);
|
|
6
|
+
if (!fs.existsSync(absoluteFile)) {
|
|
7
|
+
throw new Error(`report file does not exist: ${absoluteFile}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const report = JSON.parse(fs.readFileSync(absoluteFile, 'utf8'));
|
|
11
|
+
if (!Array.isArray(report.findings) || !Array.isArray(report.scannedFiles)) {
|
|
12
|
+
throw new Error(`report file must be awguard --format json output: ${absoluteFile}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return report;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildComparison(previous, current) {
|
|
19
|
+
const previousFindings = mapByFingerprint(previous.findings);
|
|
20
|
+
const currentFindings = mapByFingerprint(current.findings);
|
|
21
|
+
const previousFiles = new Set(previous.scannedFiles || []);
|
|
22
|
+
const currentFiles = new Set(current.scannedFiles || []);
|
|
23
|
+
|
|
24
|
+
const introducedFindings = [...currentFindings.entries()]
|
|
25
|
+
.filter(([fingerprint]) => !previousFindings.has(fingerprint))
|
|
26
|
+
.map(([, finding]) => finding);
|
|
27
|
+
const resolvedFindings = [...previousFindings.entries()]
|
|
28
|
+
.filter(([fingerprint]) => !currentFindings.has(fingerprint))
|
|
29
|
+
.map(([, finding]) => finding);
|
|
30
|
+
const unchangedFindings = [...currentFindings.keys()].filter((fingerprint) => previousFindings.has(fingerprint));
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
summary: {
|
|
34
|
+
previousFindings: previous.findings.length,
|
|
35
|
+
currentFindings: current.findings.length,
|
|
36
|
+
introducedFindings: introducedFindings.length,
|
|
37
|
+
resolvedFindings: resolvedFindings.length,
|
|
38
|
+
unchangedFindings: unchangedFindings.length,
|
|
39
|
+
addedFiles: [...currentFiles].filter((file) => !previousFiles.has(file)).length,
|
|
40
|
+
removedFiles: [...previousFiles].filter((file) => !currentFiles.has(file)).length
|
|
41
|
+
},
|
|
42
|
+
introducedFindings,
|
|
43
|
+
resolvedFindings,
|
|
44
|
+
addedFiles: [...currentFiles].filter((file) => !previousFiles.has(file)).sort(),
|
|
45
|
+
removedFiles: [...previousFiles].filter((file) => !currentFiles.has(file)).sort()
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function renderComparison(previous, current) {
|
|
50
|
+
const comparison = buildComparison(previous, current);
|
|
51
|
+
const lines = [
|
|
52
|
+
'# Agentic Workflow Guard Comparison',
|
|
53
|
+
'',
|
|
54
|
+
`Previous findings: **${comparison.summary.previousFindings}**`,
|
|
55
|
+
`Current findings: **${comparison.summary.currentFindings}**`,
|
|
56
|
+
`Introduced findings: **${comparison.summary.introducedFindings}**`,
|
|
57
|
+
`Resolved findings: **${comparison.summary.resolvedFindings}**`,
|
|
58
|
+
`Unchanged findings: **${comparison.summary.unchangedFindings}**`,
|
|
59
|
+
`Added scanned files: **${comparison.summary.addedFiles}**`,
|
|
60
|
+
`Removed scanned files: **${comparison.summary.removedFiles}**`,
|
|
61
|
+
''
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
lines.push('## Introduced Findings', '');
|
|
65
|
+
appendFindings(lines, comparison.introducedFindings);
|
|
66
|
+
lines.push('', '## Resolved Findings', '');
|
|
67
|
+
appendFindings(lines, comparison.resolvedFindings);
|
|
68
|
+
lines.push('', '## Added Files', '');
|
|
69
|
+
appendFiles(lines, comparison.addedFiles);
|
|
70
|
+
lines.push('', '## Removed Files', '');
|
|
71
|
+
appendFiles(lines, comparison.removedFiles);
|
|
72
|
+
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function appendFindings(lines, findings) {
|
|
77
|
+
if (findings.length === 0) {
|
|
78
|
+
lines.push('None.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lines.push('| Severity | Rule | Location | Finding |');
|
|
83
|
+
lines.push('| --- | --- | --- | --- |');
|
|
84
|
+
for (const finding of findings) {
|
|
85
|
+
lines.push(
|
|
86
|
+
`| ${escapeMarkdown(finding.severity)} | ${escapeMarkdown(finding.ruleId)} | ${escapeMarkdown(
|
|
87
|
+
`${finding.file}:${finding.line}`
|
|
88
|
+
)} | ${escapeMarkdown(finding.title)} |`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function appendFiles(lines, files) {
|
|
94
|
+
if (files.length === 0) {
|
|
95
|
+
lines.push('None.');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
lines.push(`- \`${file.replaceAll('`', '\\`')}\``);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function mapByFingerprint(findings) {
|
|
105
|
+
return new Map(findings.map((finding) => [finding.fingerprint || `${finding.ruleId}:${finding.file}:${finding.line}`, finding]));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function escapeMarkdown(value) {
|
|
109
|
+
return String(value).replaceAll('|', '\\|');
|
|
110
|
+
}
|