awguard 1.6.0 → 1.7.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/Dockerfile +8 -1
  3. package/README.md +176 -12
  4. package/action.yml +5 -1
  5. package/docs/comparison.md +161 -16
  6. package/docs/launch-plan.md +12 -2
  7. package/docs/marketplace-listing.md +19 -0
  8. package/docs/npm-publishing.md +68 -0
  9. package/docs/release-checklist.md +71 -0
  10. package/docs/report-gallery.md +166 -0
  11. package/docs/roadmap.md +32 -2
  12. package/docs/rule-authoring.md +99 -0
  13. package/docs/schemas.md +16 -0
  14. package/docs/setup-recipes.md +199 -0
  15. package/docs/site/index.html +29 -0
  16. package/examples/.vscode/tasks.json +17 -1
  17. package/examples/README.md +7 -0
  18. package/examples/awguard.config.example.json +8 -0
  19. package/examples/corpus/.cursor/rules/autonomy.mdc +3 -0
  20. package/examples/corpus/.github/prompts/auto-fix.prompt.md +3 -0
  21. package/examples/corpus/.github/workflows/agentic-pr-review.yml +20 -0
  22. package/examples/corpus/.github/workflows/pull-request-target-head.yml +13 -0
  23. package/examples/corpus/.mcp.json +15 -0
  24. package/examples/corpus/AGENTS.md +5 -0
  25. package/examples/corpus/README.md +23 -0
  26. package/examples/dashboard/README.md +55 -0
  27. package/examples/dashboard/index.html +313 -0
  28. package/examples/dashboard/sample-history.json +53 -0
  29. package/examples/lab/README.md +6 -0
  30. package/examples/pr-comment-bot.yml +43 -0
  31. package/examples/pull-request-target.yml +1 -1
  32. package/examples/safe-agent.yml +1 -1
  33. package/examples/unsafe-agent.yml +1 -1
  34. package/examples/vscode-extension/README.md +49 -0
  35. package/examples/vscode-extension/assets/problems-panel.svg +23 -0
  36. package/examples/vscode-extension/package.json +68 -0
  37. package/examples/vscode-extension/src/extension.js +116 -0
  38. package/package.json +2 -1
  39. package/schemas/awguard.badge.schema.json +25 -0
  40. package/schemas/awguard.baseline.schema.json +40 -0
  41. package/schemas/awguard.comparison.schema.json +146 -0
  42. package/schemas/awguard.config.schema.json +167 -0
  43. package/schemas/awguard.inventory.schema.json +124 -0
  44. package/schemas/awguard.report.schema.json +121 -0
  45. package/src/autofix.js +201 -0
  46. package/src/badges.js +63 -0
  47. package/src/baseline.js +77 -0
  48. package/src/cli.js +248 -6
  49. package/src/compare.js +60 -4
  50. package/src/config.js +31 -2
  51. package/src/demo.js +90 -0
  52. package/src/doctor.js +189 -0
  53. package/src/explain.js +147 -0
  54. package/src/init.js +4 -1
  55. package/src/policy-packs.js +99 -0
  56. package/src/policy-wizard.js +165 -0
  57. package/src/remediation.js +73 -1
  58. package/src/reporters.js +86 -3
  59. package/src/scanner.js +204 -5
  60. package/src/templates.js +132 -0
@@ -0,0 +1,167 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/Mughal-Baig/agentic-workflow-guard/main/schemas/awguard.config.schema.json",
4
+ "title": "Agentic Workflow Guard configuration",
5
+ "description": "Configuration for AWGuard rule severities, presets, suppression policy, and reviewed agentic surface allowlists.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string"
11
+ },
12
+ "extends": {
13
+ "description": "Built-in preset name or list of preset names to merge before local settings.",
14
+ "oneOf": [
15
+ {
16
+ "$ref": "#/$defs/preset"
17
+ },
18
+ {
19
+ "type": "array",
20
+ "items": {
21
+ "$ref": "#/$defs/preset"
22
+ },
23
+ "uniqueItems": true
24
+ }
25
+ ]
26
+ },
27
+ "rules": {
28
+ "type": "object",
29
+ "description": "Rule severity overrides. Use \"off\" to disable a rule.",
30
+ "additionalProperties": false,
31
+ "patternProperties": {
32
+ "^AWG(00[1-9]|01[0-9])$": {
33
+ "oneOf": [
34
+ {
35
+ "$ref": "#/$defs/ruleSeverity"
36
+ },
37
+ {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": ["severity"],
41
+ "properties": {
42
+ "severity": {
43
+ "$ref": "#/$defs/ruleSeverity"
44
+ }
45
+ }
46
+ }
47
+ ]
48
+ }
49
+ }
50
+ },
51
+ "suppressions": {
52
+ "type": "object",
53
+ "additionalProperties": false,
54
+ "properties": {
55
+ "allow": {
56
+ "type": "boolean",
57
+ "description": "Set false to report every awguard-disable comment as invalid."
58
+ },
59
+ "allowedRules": {
60
+ "type": "array",
61
+ "description": "Optional list of rule IDs that may be suppressed inline.",
62
+ "items": {
63
+ "$ref": "#/$defs/ruleId"
64
+ },
65
+ "uniqueItems": true
66
+ },
67
+ "minimumReasonLength": {
68
+ "type": "integer",
69
+ "minimum": 1,
70
+ "description": "Minimum characters required after the suppression reason separator."
71
+ }
72
+ }
73
+ },
74
+ "scan": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "properties": {
78
+ "include": {
79
+ "$ref": "#/$defs/stringList",
80
+ "description": "Optional glob-style file patterns that narrow discovered AWGuard scan files."
81
+ },
82
+ "exclude": {
83
+ "$ref": "#/$defs/stringList",
84
+ "description": "Optional glob-style file patterns removed from discovered AWGuard scan files."
85
+ },
86
+ "maxFiles": {
87
+ "type": "integer",
88
+ "minimum": 1,
89
+ "description": "Optional cap on the number of discovered scan files."
90
+ },
91
+ "maxFileBytes": {
92
+ "type": "integer",
93
+ "minimum": 1,
94
+ "description": "Optional cap on the byte size of each scanned file."
95
+ }
96
+ }
97
+ },
98
+ "policy": {
99
+ "type": "object",
100
+ "additionalProperties": false,
101
+ "properties": {
102
+ "approvedFiles": {
103
+ "$ref": "#/$defs/stringList",
104
+ "description": "Reviewed workflow, instruction, prompt, skill, and MCP config files. Glob-style * suffixes are supported by AWGuard."
105
+ },
106
+ "approvedMcpServers": {
107
+ "$ref": "#/$defs/stringList",
108
+ "description": "Reviewed MCP server names."
109
+ },
110
+ "approvedMcpPackages": {
111
+ "$ref": "#/$defs/stringList",
112
+ "description": "Reviewed MCP package or container specs, preferably pinned to exact versions or digests."
113
+ },
114
+ "approvedMcpPackageScopes": {
115
+ "$ref": "#/$defs/stringList",
116
+ "description": "Reviewed npm package scopes or prefixes for optional offline MCP package reputation checks, for example @modelcontextprotocol/."
117
+ },
118
+ "approvedMcpCommands": {
119
+ "$ref": "#/$defs/stringList",
120
+ "description": "Reviewed MCP launch commands such as node, npx, uvx, or docker."
121
+ }
122
+ }
123
+ }
124
+ },
125
+ "$defs": {
126
+ "preset": {
127
+ "type": "string",
128
+ "enum": ["strict", "claude-code", "codex", "aider", "triage-bot"]
129
+ },
130
+ "ruleId": {
131
+ "type": "string",
132
+ "enum": [
133
+ "AWG001",
134
+ "AWG002",
135
+ "AWG003",
136
+ "AWG004",
137
+ "AWG005",
138
+ "AWG006",
139
+ "AWG007",
140
+ "AWG008",
141
+ "AWG009",
142
+ "AWG010",
143
+ "AWG011",
144
+ "AWG012",
145
+ "AWG013",
146
+ "AWG014",
147
+ "AWG015",
148
+ "AWG016",
149
+ "AWG017",
150
+ "AWG018",
151
+ "AWG019"
152
+ ]
153
+ },
154
+ "ruleSeverity": {
155
+ "type": "string",
156
+ "enum": ["off", "low", "medium", "high", "critical"]
157
+ },
158
+ "stringList": {
159
+ "type": "array",
160
+ "items": {
161
+ "type": "string",
162
+ "minLength": 1
163
+ },
164
+ "uniqueItems": true
165
+ }
166
+ }
167
+ }
@@ -0,0 +1,124 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/Mughal-Baig/agentic-workflow-guard/main/schemas/awguard.inventory.schema.json",
4
+ "title": "Agentic Workflow Guard inventory report",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["root", "summary", "surfaces", "files", "recommendations"],
8
+ "properties": {
9
+ "root": {
10
+ "type": "string"
11
+ },
12
+ "summary": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": ["scannedFiles", "surfaces", "findings", "highest"],
16
+ "properties": {
17
+ "scannedFiles": {
18
+ "type": "integer",
19
+ "minimum": 0
20
+ },
21
+ "surfaces": {
22
+ "type": "integer",
23
+ "minimum": 0
24
+ },
25
+ "findings": {
26
+ "type": "integer",
27
+ "minimum": 0
28
+ },
29
+ "highest": {
30
+ "$ref": "#/$defs/severity"
31
+ }
32
+ }
33
+ },
34
+ "surfaces": {
35
+ "type": "array",
36
+ "items": {
37
+ "$ref": "#/$defs/surface"
38
+ }
39
+ },
40
+ "files": {
41
+ "type": "array",
42
+ "items": {
43
+ "$ref": "#/$defs/file"
44
+ }
45
+ },
46
+ "recommendations": {
47
+ "type": "array",
48
+ "items": {
49
+ "type": "string"
50
+ }
51
+ }
52
+ },
53
+ "$defs": {
54
+ "severity": {
55
+ "type": "string",
56
+ "enum": ["none", "low", "medium", "high", "critical"]
57
+ },
58
+ "surfaceName": {
59
+ "type": "string",
60
+ "enum": ["github-workflow", "agent-context", "mcp-config", "other"]
61
+ },
62
+ "ruleIdList": {
63
+ "type": "array",
64
+ "items": {
65
+ "type": "string",
66
+ "pattern": "^AWG[0-9]{3}$"
67
+ },
68
+ "uniqueItems": true
69
+ },
70
+ "surface": {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "required": ["surface", "label", "files", "findings", "highest", "rules"],
74
+ "properties": {
75
+ "surface": {
76
+ "$ref": "#/$defs/surfaceName"
77
+ },
78
+ "label": {
79
+ "type": "string"
80
+ },
81
+ "files": {
82
+ "type": "integer",
83
+ "minimum": 0
84
+ },
85
+ "findings": {
86
+ "type": "integer",
87
+ "minimum": 0
88
+ },
89
+ "highest": {
90
+ "$ref": "#/$defs/severity"
91
+ },
92
+ "rules": {
93
+ "$ref": "#/$defs/ruleIdList"
94
+ }
95
+ }
96
+ },
97
+ "file": {
98
+ "type": "object",
99
+ "additionalProperties": false,
100
+ "required": ["file", "surface", "label", "findings", "highest", "rules"],
101
+ "properties": {
102
+ "file": {
103
+ "type": "string"
104
+ },
105
+ "surface": {
106
+ "$ref": "#/$defs/surfaceName"
107
+ },
108
+ "label": {
109
+ "type": "string"
110
+ },
111
+ "findings": {
112
+ "type": "integer",
113
+ "minimum": 0
114
+ },
115
+ "highest": {
116
+ "$ref": "#/$defs/severity"
117
+ },
118
+ "rules": {
119
+ "$ref": "#/$defs/ruleIdList"
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,121 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/Mughal-Baig/agentic-workflow-guard/main/schemas/awguard.report.schema.json",
4
+ "title": "Agentic Workflow Guard JSON report",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["root", "scannedFiles", "summary", "findings"],
8
+ "properties": {
9
+ "root": {
10
+ "type": "string"
11
+ },
12
+ "scannedFiles": {
13
+ "type": "array",
14
+ "items": {
15
+ "type": "string"
16
+ }
17
+ },
18
+ "summary": {
19
+ "$ref": "#/$defs/summary"
20
+ },
21
+ "findings": {
22
+ "type": "array",
23
+ "items": {
24
+ "$ref": "#/$defs/finding"
25
+ }
26
+ }
27
+ },
28
+ "$defs": {
29
+ "severity": {
30
+ "type": "string",
31
+ "enum": ["none", "low", "medium", "high", "critical"]
32
+ },
33
+ "ruleId": {
34
+ "type": "string",
35
+ "pattern": "^AWG[0-9]{3}$"
36
+ },
37
+ "summary": {
38
+ "type": "object",
39
+ "additionalProperties": true,
40
+ "required": ["total", "highest"],
41
+ "properties": {
42
+ "total": {
43
+ "type": "integer",
44
+ "minimum": 0
45
+ },
46
+ "highest": {
47
+ "$ref": "#/$defs/severity"
48
+ },
49
+ "bySeverity": {
50
+ "type": "object",
51
+ "additionalProperties": {
52
+ "type": "integer",
53
+ "minimum": 0
54
+ }
55
+ },
56
+ "baseline": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": ["new", "known"],
60
+ "properties": {
61
+ "new": {
62
+ "type": "integer",
63
+ "minimum": 0
64
+ },
65
+ "known": {
66
+ "type": "integer",
67
+ "minimum": 0
68
+ }
69
+ }
70
+ }
71
+ }
72
+ },
73
+ "finding": {
74
+ "type": "object",
75
+ "additionalProperties": false,
76
+ "required": ["ruleId", "title", "severity", "file", "line", "message", "suggestion"],
77
+ "properties": {
78
+ "ruleId": {
79
+ "$ref": "#/$defs/ruleId"
80
+ },
81
+ "title": {
82
+ "type": "string"
83
+ },
84
+ "severity": {
85
+ "$ref": "#/$defs/severity"
86
+ },
87
+ "file": {
88
+ "type": "string"
89
+ },
90
+ "line": {
91
+ "type": "integer",
92
+ "minimum": 1
93
+ },
94
+ "column": {
95
+ "type": ["integer", "null"],
96
+ "minimum": 1
97
+ },
98
+ "message": {
99
+ "type": "string"
100
+ },
101
+ "evidence": {
102
+ "type": ["string", "null"]
103
+ },
104
+ "remediationCode": {
105
+ "type": ["string", "null"],
106
+ "pattern": "^[a-z][a-z0-9.-]*$"
107
+ },
108
+ "suggestion": {
109
+ "type": "string"
110
+ },
111
+ "fingerprint": {
112
+ "type": ["string", "null"]
113
+ },
114
+ "baselineState": {
115
+ "type": ["string", "null"],
116
+ "enum": ["new", "known", null]
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
package/src/autofix.js ADDED
@@ -0,0 +1,201 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const fixableRules = new Set(['AWG004', 'AWG008', 'AWG016']);
5
+
6
+ export function buildAutofixPlan(result) {
7
+ const byFile = new Map();
8
+
9
+ for (const finding of result.findings) {
10
+ if (finding.baselineState === 'known') continue;
11
+ if (!fixableRules.has(finding.ruleId)) continue;
12
+ if (!isWorkflowFile(finding.absoluteFile || finding.file)) continue;
13
+
14
+ const absoluteFile = finding.absoluteFile || path.join(result.root, finding.file);
15
+ const existing = byFile.get(absoluteFile) || [];
16
+ existing.push(finding);
17
+ byFile.set(absoluteFile, existing);
18
+ }
19
+
20
+ const files = [];
21
+ for (const [absoluteFile, findings] of byFile.entries()) {
22
+ const plan = buildFilePlan(absoluteFile, findings, result.root);
23
+ if (plan.changes.length > 0) files.push(plan);
24
+ }
25
+
26
+ return {
27
+ files,
28
+ changes: files.reduce((count, file) => count + file.changes.length, 0)
29
+ };
30
+ }
31
+
32
+ export function applyAutofixPlan(plan) {
33
+ for (const file of plan.files) {
34
+ fs.writeFileSync(file.absoluteFile, file.nextText);
35
+ }
36
+
37
+ return plan;
38
+ }
39
+
40
+ export function renderAutofixPlan(plan, { applied = false } = {}) {
41
+ const lines = [applied ? 'Agentic Workflow Guard Autofix Applied' : 'Agentic Workflow Guard Autofix Plan', ''];
42
+
43
+ if (plan.changes === 0) {
44
+ lines.push('No safe autofixes available for the current findings.');
45
+ lines.push('Run with --fix-dry-run for remediation guidance on findings that require human review.');
46
+ return lines.join('\n');
47
+ }
48
+
49
+ lines.push(`${applied ? 'Applied' : 'Would apply'} ${plan.changes} change(s) in ${plan.files.length} file(s).`, '');
50
+
51
+ for (const file of plan.files) {
52
+ lines.push(`## ${file.file}`);
53
+ for (const change of file.changes) {
54
+ lines.push(`- ${change.ruleId}: ${change.description}`);
55
+ }
56
+ lines.push('');
57
+ }
58
+
59
+ if (applied) {
60
+ lines.push('Rollback: review git diff, then revert the edited hunks if needed.');
61
+ } else {
62
+ lines.push('Apply: rerun with --fix. Review git diff before committing.');
63
+ }
64
+
65
+ return lines.join('\n');
66
+ }
67
+
68
+ function buildFilePlan(absoluteFile, findings, root) {
69
+ const originalText = fs.readFileSync(absoluteFile, 'utf8');
70
+ const newline = originalText.includes('\r\n') ? '\r\n' : '\n';
71
+ const hadFinalNewline = originalText.endsWith('\n');
72
+ const lines = originalText.replace(/\r?\n$/, '').split(/\r?\n/);
73
+ const changes = [];
74
+
75
+ const lineSpecificFindings = findings
76
+ .filter((finding) => finding.ruleId === 'AWG004' || finding.ruleId === 'AWG016')
77
+ .sort((a, b) => b.line - a.line);
78
+
79
+ for (const finding of lineSpecificFindings) {
80
+ if (finding.ruleId === 'AWG004') {
81
+ applyWriteAllFix(lines, finding, changes);
82
+ } else if (finding.ruleId === 'AWG016') {
83
+ applyCheckoutCredentialsFix(lines, finding, changes);
84
+ }
85
+ }
86
+
87
+ if (findings.some((finding) => finding.ruleId === 'AWG008')) {
88
+ applyMissingPermissionsFix(lines, changes);
89
+ }
90
+
91
+ const nextText = `${lines.join(newline)}${hadFinalNewline ? newline : ''}`;
92
+
93
+ return {
94
+ file: path.relative(root, absoluteFile).split(path.sep).join('/') || path.basename(absoluteFile),
95
+ absoluteFile,
96
+ changes,
97
+ nextText
98
+ };
99
+ }
100
+
101
+ function applyWriteAllFix(lines, finding, changes) {
102
+ const index = finding.line - 1;
103
+ const line = lines[index] || '';
104
+ const match = line.match(/^(\s*)permissions\s*:\s*write-all\s*(?:#.*)?$/i);
105
+ if (!match) return;
106
+
107
+ const indent = match[1];
108
+ lines.splice(index, 1, `${indent}permissions:`, `${indent} contents: read`);
109
+ changes.push({
110
+ ruleId: 'AWG004',
111
+ description: `replace write-all permissions at line ${finding.line} with contents: read`
112
+ });
113
+ }
114
+
115
+ function applyCheckoutCredentialsFix(lines, finding, changes) {
116
+ const checkoutIndex = findCheckoutLine(lines, finding.line - 1);
117
+ if (checkoutIndex === -1) return;
118
+
119
+ const checkoutLine = lines[checkoutIndex];
120
+ const checkoutIndent = leadingSpaces(checkoutLine);
121
+ const childIndent = `${checkoutIndent} `;
122
+ const valueIndent = `${childIndent} `;
123
+ const blockEnd = findStepBlockEnd(lines, checkoutIndex, checkoutIndent.length);
124
+ const persistIndex = findKeyInRange(lines, checkoutIndex + 1, blockEnd, 'persist-credentials');
125
+
126
+ if (persistIndex !== -1) {
127
+ if (/:\s*false\s*(?:#.*)?$/i.test(lines[persistIndex])) return;
128
+ lines[persistIndex] = `${leadingSpaces(lines[persistIndex])}persist-credentials: false`;
129
+ changes.push({
130
+ ruleId: 'AWG016',
131
+ description: `set actions/checkout persist-credentials: false at line ${persistIndex + 1}`
132
+ });
133
+ return;
134
+ }
135
+
136
+ const withIndex = findKeyInRange(lines, checkoutIndex + 1, blockEnd, 'with');
137
+ if (withIndex !== -1) {
138
+ lines.splice(withIndex + 1, 0, `${leadingSpaces(lines[withIndex])} persist-credentials: false`);
139
+ } else {
140
+ lines.splice(checkoutIndex + 1, 0, `${childIndent}with:`, `${valueIndent}persist-credentials: false`);
141
+ }
142
+
143
+ changes.push({
144
+ ruleId: 'AWG016',
145
+ description: `add actions/checkout persist-credentials: false after line ${checkoutIndex + 1}`
146
+ });
147
+ }
148
+
149
+ function applyMissingPermissionsFix(lines, changes) {
150
+ if (hasTopLevelPermissions(lines)) return;
151
+ const jobsIndex = lines.findIndex((line) => /^jobs\s*:\s*(?:#.*)?$/i.test(line));
152
+ if (jobsIndex === -1) return;
153
+
154
+ lines.splice(jobsIndex, 0, 'permissions:', ' contents: read', '');
155
+ changes.push({
156
+ ruleId: 'AWG008',
157
+ description: 'add top-level permissions: contents: read before jobs'
158
+ });
159
+ }
160
+
161
+ function findCheckoutLine(lines, index) {
162
+ for (let cursor = index; cursor >= Math.max(0, index - 8); cursor -= 1) {
163
+ if (/uses\s*:\s*actions\/checkout@/i.test(lines[cursor] || '')) return cursor;
164
+ }
165
+ return -1;
166
+ }
167
+
168
+ function findStepBlockEnd(lines, startIndex, startIndentWidth) {
169
+ for (let index = startIndex + 1; index < lines.length; index += 1) {
170
+ const line = lines[index];
171
+ if (!line.trim()) continue;
172
+ const indentWidth = leadingSpaces(line).length;
173
+ if (indentWidth <= startIndentWidth && /^\s*-\s+/.test(line)) return index;
174
+ if (indentWidth < startIndentWidth) return index;
175
+ }
176
+ return lines.length;
177
+ }
178
+
179
+ function findKeyInRange(lines, start, end, key) {
180
+ const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*:`, 'i');
181
+ for (let index = start; index < end; index += 1) {
182
+ if (pattern.test(lines[index])) return index;
183
+ }
184
+ return -1;
185
+ }
186
+
187
+ function hasTopLevelPermissions(lines) {
188
+ return lines.some((line) => /^permissions\s*:/i.test(line));
189
+ }
190
+
191
+ function leadingSpaces(line) {
192
+ return line.match(/^\s*/)[0];
193
+ }
194
+
195
+ function isWorkflowFile(file) {
196
+ return ['.yml', '.yaml'].includes(path.extname(file || '').toLowerCase());
197
+ }
198
+
199
+ function escapeRegex(value) {
200
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
201
+ }
package/src/badges.js ADDED
@@ -0,0 +1,63 @@
1
+ export function renderBadgeSnippets({
2
+ repo = 'OWNER/REPO',
3
+ branch = 'main',
4
+ badgeFile = 'docs/awguard-badge.json',
5
+ site = ''
6
+ } = {}) {
7
+ const rawBadgeUrl = `https://raw.githubusercontent.com/${repo}/${branch}/${badgeFile}`;
8
+ const badgeEndpoint = `https://img.shields.io/endpoint?url=${rawBadgeUrl}`;
9
+ const lines = [
10
+ '# AWGuard Badge Snippets',
11
+ '',
12
+ 'Copy the snippets that match your repository setup.',
13
+ '',
14
+ '## AWI Risk',
15
+ '',
16
+ '```markdown',
17
+ `[![AWI risk](${badgeEndpoint})](${badgeFile})`,
18
+ '```',
19
+ '',
20
+ 'Generate the badge JSON with:',
21
+ '',
22
+ '```bash',
23
+ `npx awguard@latest . --format badge --output ${badgeFile}`,
24
+ '```',
25
+ '',
26
+ '## GitHub Action',
27
+ '',
28
+ '```markdown',
29
+ `[![AWGuard](https://github.com/${repo}/actions/workflows/awguard.yml/badge.svg)](https://github.com/${repo}/actions/workflows/awguard.yml)`,
30
+ '```',
31
+ '',
32
+ '## Code Scanning',
33
+ '',
34
+ '```markdown',
35
+ `[![Code Scanning](https://github.com/${repo}/actions/workflows/code-scanning.yml/badge.svg)](https://github.com/${repo}/actions/workflows/code-scanning.yml)`,
36
+ '```',
37
+ '',
38
+ '## npm',
39
+ '',
40
+ '```markdown',
41
+ '[![npm](https://img.shields.io/npm/v/awguard)](https://www.npmjs.com/package/awguard)',
42
+ '```',
43
+ '',
44
+ '## Release',
45
+ '',
46
+ '```markdown',
47
+ `[![GitHub release](https://img.shields.io/github/v/release/${repo})](https://github.com/${repo}/releases)`,
48
+ '```'
49
+ ];
50
+
51
+ if (site) {
52
+ lines.push(
53
+ '',
54
+ '## Project Site',
55
+ '',
56
+ '```markdown',
57
+ `[![Project site](https://img.shields.io/badge/site-live-0f766e)](${site})`,
58
+ '```'
59
+ );
60
+ }
61
+
62
+ return lines.join('\n');
63
+ }