actions-warden 0.1.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/LICENSE +21 -0
- package/README.md +355 -0
- package/SECURITY.md +52 -0
- package/package.json +68 -0
- package/src/cli.js +155 -0
- package/src/commands/audit.js +161 -0
- package/src/commands/pin.js +199 -0
- package/src/commands/report.js +93 -0
- package/src/commands/upgrade.js +281 -0
- package/src/index.js +19 -0
- package/src/lib/cache.js +68 -0
- package/src/lib/formatter.js +136 -0
- package/src/lib/ignore.js +134 -0
- package/src/lib/parser.js +279 -0
- package/src/lib/paths.js +99 -0
- package/src/lib/redact.js +44 -0
- package/src/lib/resolver.js +224 -0
- package/src/lib/writer.js +56 -0
- package/src/rules/excessive-permissions.js +79 -0
- package/src/rules/index.js +18 -0
- package/src/rules/pull-request-target-checkout.js +46 -0
- package/src/rules/script-injection.js +59 -0
- package/src/rules/secrets-in-env.js +51 -0
- package/src/rules/unpinned-action.js +39 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit command - scan workflows for security findings.
|
|
3
|
+
*
|
|
4
|
+
* Programmatic API:
|
|
5
|
+
* const result = await audit({ cwd, workflows, severity, explain });
|
|
6
|
+
* result.findings: Finding[]
|
|
7
|
+
* result.summary: { files, findings, critical, high, medium, low }
|
|
8
|
+
* result.status: 'OK' | 'FAIL'
|
|
9
|
+
*
|
|
10
|
+
* A finding looks like:
|
|
11
|
+
* { id, ruleId, severity, file, line, fields, explain }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { parseWorkflowFile } from '../lib/parser.js';
|
|
16
|
+
import { discoverWorkflows, resolveWorkflowArg } from '../lib/paths.js';
|
|
17
|
+
import { format, summarize, SEVERITY_ORDER } from '../lib/formatter.js';
|
|
18
|
+
import { parseIgnoreDirectives, isIgnored } from '../lib/ignore.js';
|
|
19
|
+
import { RULES } from '../rules/index.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} Finding
|
|
23
|
+
* @property {string} id - unique id (sha1 short)
|
|
24
|
+
* @property {string} ruleId
|
|
25
|
+
* @property {string} severity
|
|
26
|
+
* @property {string} file
|
|
27
|
+
* @property {number} line
|
|
28
|
+
* @property {Record<string, unknown>} fields
|
|
29
|
+
* @property {string} explain
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} opts
|
|
34
|
+
* @param {string} [opts.cwd]
|
|
35
|
+
* @param {string[]} [opts.workflows] - explicit file/glob args
|
|
36
|
+
* @param {'low'|'medium'|'high'|'critical'} [opts.severity] - minimum severity
|
|
37
|
+
* @param {boolean} [opts.explain]
|
|
38
|
+
* @returns {Promise<{findings: Finding[], summary: object, status: 'OK'|'FAIL', files: string[]}>}
|
|
39
|
+
*/
|
|
40
|
+
export async function audit({ cwd = process.cwd(), workflows, severity, explain = false } = {}) {
|
|
41
|
+
const files = await resolveTargets(workflows, cwd);
|
|
42
|
+
/** @type {Finding[]} */
|
|
43
|
+
const findings = [];
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
let doc;
|
|
46
|
+
try {
|
|
47
|
+
doc = await parseWorkflowFile(file);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
findings.push({
|
|
50
|
+
id: shortId(`parse:${file}`),
|
|
51
|
+
ruleId: 'parse-error',
|
|
52
|
+
severity: 'high',
|
|
53
|
+
file,
|
|
54
|
+
line: 0,
|
|
55
|
+
fields: { type: 'parse-error', sev: 'high', file },
|
|
56
|
+
explain: String(err.message ?? err),
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const ignore = parseIgnoreDirectives(doc.source);
|
|
61
|
+
for (const rule of RULES) {
|
|
62
|
+
const ruleFindings = rule.check(doc);
|
|
63
|
+
for (const f of ruleFindings) {
|
|
64
|
+
if (isIgnored(ignore, f.line, rule.id)) continue;
|
|
65
|
+
const finding = {
|
|
66
|
+
id: shortId(`${rule.id}:${file}:${f.line}:${JSON.stringify(f.fields)}`),
|
|
67
|
+
ruleId: rule.id,
|
|
68
|
+
severity: f.severity,
|
|
69
|
+
file,
|
|
70
|
+
line: f.line,
|
|
71
|
+
fields: { ...f.fields, file: relPath(file, cwd) },
|
|
72
|
+
explain: f.explain,
|
|
73
|
+
};
|
|
74
|
+
findings.push(finding);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const filtered = filterBySeverity(findings, severity);
|
|
79
|
+
const counts = summarize(filtered);
|
|
80
|
+
const status = filtered.length === 0 ? 'OK' : 'FAIL';
|
|
81
|
+
return {
|
|
82
|
+
files,
|
|
83
|
+
findings: explain ? filtered : filtered.map(stripExplain),
|
|
84
|
+
summary: { files: files.length, findings: filtered.length, ...counts },
|
|
85
|
+
status,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function stripExplain(f) {
|
|
90
|
+
const { explain: _unused, ...rest } = f;
|
|
91
|
+
return rest;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string[]|undefined} workflows
|
|
96
|
+
* @param {string} cwd
|
|
97
|
+
*/
|
|
98
|
+
async function resolveTargets(workflows, cwd) {
|
|
99
|
+
if (!workflows || workflows.length === 0) {
|
|
100
|
+
return discoverWorkflows({ cwd });
|
|
101
|
+
}
|
|
102
|
+
const out = new Set();
|
|
103
|
+
for (const w of workflows) {
|
|
104
|
+
const files = await resolveWorkflowArg(w, cwd);
|
|
105
|
+
for (const f of files) out.add(f);
|
|
106
|
+
}
|
|
107
|
+
return [...out].sort();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {Finding[]} findings
|
|
112
|
+
* @param {string|undefined} min
|
|
113
|
+
*/
|
|
114
|
+
function filterBySeverity(findings, min) {
|
|
115
|
+
if (!min) return findings;
|
|
116
|
+
const minIdx = SEVERITY_ORDER.indexOf(min);
|
|
117
|
+
if (minIdx === -1) return findings;
|
|
118
|
+
return findings.filter(f => SEVERITY_ORDER.indexOf(f.severity) >= minIdx);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function shortId(input) {
|
|
122
|
+
return createHash('sha1').update(input).digest('hex').slice(0, 10);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function relPath(p, cwd) {
|
|
126
|
+
if (p.startsWith(cwd)) return p.slice(cwd.length + 1);
|
|
127
|
+
return p;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Render an audit result to the chosen format.
|
|
132
|
+
*
|
|
133
|
+
* @param {Awaited<ReturnType<typeof audit>>} result
|
|
134
|
+
* @param {{format: 'toon'|'json'|'text', explain?: boolean, cwd?: string}} opts
|
|
135
|
+
*/
|
|
136
|
+
export function renderAudit(result, opts) {
|
|
137
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
138
|
+
if (opts.format === 'json') {
|
|
139
|
+
return format('json', [], {
|
|
140
|
+
status: result.status,
|
|
141
|
+
json: {
|
|
142
|
+
files: result.files.map(f => relPath(f, cwd)),
|
|
143
|
+
findings: result.findings.map(f => ({ ...f, file: relPath(f.file, cwd) })),
|
|
144
|
+
summary: result.summary,
|
|
145
|
+
status: result.status,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** @type {Array<{label: string, fields: Record<string, unknown>}>} */
|
|
150
|
+
const records = [];
|
|
151
|
+
for (const f of result.files) {
|
|
152
|
+
records.push({ label: 'SCAN', fields: { file: relPath(f, opts.cwd ?? process.cwd()) } });
|
|
153
|
+
}
|
|
154
|
+
for (const finding of result.findings) {
|
|
155
|
+
const fields = { id: finding.id, ...finding.fields, line: finding.line };
|
|
156
|
+
if (opts.explain) fields.explain = finding.explain;
|
|
157
|
+
records.push({ label: 'FINDING', fields });
|
|
158
|
+
}
|
|
159
|
+
records.push({ label: 'SUMMARY', fields: result.summary });
|
|
160
|
+
return format(opts.format, records, { status: result.status });
|
|
161
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pin command - rewrite tag-based `uses:` refs to immutable commit SHAs.
|
|
3
|
+
*
|
|
4
|
+
* Format: `uses: owner/repo@<sha> # <original-ref>`
|
|
5
|
+
*
|
|
6
|
+
* The original tag is preserved as an inline comment so upgrades can find
|
|
7
|
+
* the human-readable version later.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { parseWorkflowSource, collectUses } from '../lib/parser.js';
|
|
13
|
+
import { discoverWorkflows, resolveWorkflowArg } from '../lib/paths.js';
|
|
14
|
+
import { resolveRefToSha, resolveToken } from '../lib/resolver.js';
|
|
15
|
+
import { writeFileGuarded } from '../lib/writer.js';
|
|
16
|
+
import { parseIgnoreDirectives, isIgnored } from '../lib/ignore.js';
|
|
17
|
+
import { format } from '../lib/formatter.js';
|
|
18
|
+
|
|
19
|
+
const SHA_RE = /^[0-9a-f]{40}$/i;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} PinChange
|
|
23
|
+
* @property {string} id
|
|
24
|
+
* @property {string} file
|
|
25
|
+
* @property {string} action - owner/repo
|
|
26
|
+
* @property {string} fromRef
|
|
27
|
+
* @property {string} toSha
|
|
28
|
+
* @property {number} line
|
|
29
|
+
* @property {'tag'|'branch'|'commit'} refType
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} opts
|
|
34
|
+
* @param {string} [opts.cwd]
|
|
35
|
+
* @param {string[]} [opts.workflows]
|
|
36
|
+
* @param {boolean} [opts.dryRun]
|
|
37
|
+
* @param {string} [opts.token]
|
|
38
|
+
* @param {string} [opts.fix] - finding/change id to apply (skip others)
|
|
39
|
+
* @returns {Promise<{changes: PinChange[], errors: object[], status: 'OK'|'FAIL'}>}
|
|
40
|
+
*/
|
|
41
|
+
export async function pin({ cwd = process.cwd(), workflows, dryRun = true, token, fix } = {}) {
|
|
42
|
+
const files = await resolveTargets(workflows, cwd);
|
|
43
|
+
const tok = resolveToken(token);
|
|
44
|
+
/** @type {PinChange[]} */
|
|
45
|
+
const changes = [];
|
|
46
|
+
const errors = [];
|
|
47
|
+
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
let source;
|
|
50
|
+
try {
|
|
51
|
+
source = await readFile(file, 'utf8');
|
|
52
|
+
} catch (err) {
|
|
53
|
+
errors.push({ file, error: String(err.message ?? err) });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
let doc;
|
|
57
|
+
try {
|
|
58
|
+
doc = parseWorkflowSource(source, file);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
errors.push({ file, error: String(err.message ?? err) });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
/** @type {Array<{ref: import('../lib/parser.js').ActionRef, sha: string, type: string}>} */
|
|
64
|
+
const planned = [];
|
|
65
|
+
const ignore = parseIgnoreDirectives(source);
|
|
66
|
+
for (const { ref } of collectUses(doc)) {
|
|
67
|
+
if (ref.kind !== 'external' && ref.kind !== 'reusable-workflow') continue;
|
|
68
|
+
if (!ref.ref || SHA_RE.test(ref.ref)) continue;
|
|
69
|
+
if (isIgnored(ignore, ref.line, 'unpinned-action')) continue;
|
|
70
|
+
try {
|
|
71
|
+
const resolved = await resolveRefToSha({
|
|
72
|
+
owner: ref.owner,
|
|
73
|
+
repo: ref.repo,
|
|
74
|
+
ref: ref.ref,
|
|
75
|
+
token: tok,
|
|
76
|
+
cwd,
|
|
77
|
+
});
|
|
78
|
+
planned.push({ ref, sha: resolved.sha, type: resolved.type });
|
|
79
|
+
} catch (err) {
|
|
80
|
+
errors.push({ file, action: ref.raw, error: String(err.message ?? err) });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (planned.length === 0) continue;
|
|
84
|
+
|
|
85
|
+
let newSource = source;
|
|
86
|
+
for (const { ref, sha, type } of planned) {
|
|
87
|
+
const change = {
|
|
88
|
+
id: changeId(file, ref.raw),
|
|
89
|
+
file,
|
|
90
|
+
action: `${ref.owner}/${ref.repo}${ref.subpath ? `/${ref.subpath}` : ''}`,
|
|
91
|
+
fromRef: ref.ref,
|
|
92
|
+
toSha: sha,
|
|
93
|
+
line: ref.line,
|
|
94
|
+
refType: type,
|
|
95
|
+
};
|
|
96
|
+
if (fix && fix !== change.id) continue;
|
|
97
|
+
newSource = rewriteUses(newSource, ref, sha);
|
|
98
|
+
changes.push(change);
|
|
99
|
+
}
|
|
100
|
+
if (newSource !== source) {
|
|
101
|
+
await writeFileGuarded({ path: file, content: newSource, dryRun, cwd });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { changes, errors, status: errors.length === 0 ? 'OK' : 'FAIL' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Replace `uses: owner/repo[/sub]@<ref>` with the pinned SHA + comment.
|
|
109
|
+
*
|
|
110
|
+
* Operates on the source string; preserves quoting and whitespace.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} source
|
|
113
|
+
* @param {import('../lib/parser.js').ActionRef} ref
|
|
114
|
+
* @param {string} sha
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function rewriteUses(source, ref, sha) {
|
|
118
|
+
const left = ref.subpath ? `${ref.owner}/${ref.repo}/${ref.subpath}` : `${ref.owner}/${ref.repo}`;
|
|
119
|
+
const escLeft = escapeRegExp(left);
|
|
120
|
+
const escRef = escapeRegExp(ref.ref);
|
|
121
|
+
// Match: optional quote, owner/repo[/sub]@ref, optional quote, optional comment.
|
|
122
|
+
const re = new RegExp(
|
|
123
|
+
`(uses\\s*:\\s*['"]?)${escLeft}@${escRef}(['"]?)([^\\n]*)`,
|
|
124
|
+
'g',
|
|
125
|
+
);
|
|
126
|
+
return source.replace(re, (_, prefix, closingQuote, trailing) => {
|
|
127
|
+
const tail = stripExistingVersionComment(trailing);
|
|
128
|
+
return `${prefix}${left}@${sha}${closingQuote}${tail} # ${ref.ref}`;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function stripExistingVersionComment(trailing) {
|
|
133
|
+
// Remove any existing inline comment so we don't stack `# v3 # v3`.
|
|
134
|
+
const idx = trailing.indexOf('#');
|
|
135
|
+
if (idx === -1) return trailing;
|
|
136
|
+
return trailing.slice(0, idx).replace(/\s+$/, '');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function escapeRegExp(s) {
|
|
140
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function changeId(file, raw) {
|
|
144
|
+
return createHash('sha1').update(`pin:${file}:${raw}`).digest('hex').slice(0, 10);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function resolveTargets(workflows, cwd) {
|
|
148
|
+
if (!workflows || workflows.length === 0) return discoverWorkflows({ cwd });
|
|
149
|
+
const out = new Set();
|
|
150
|
+
for (const w of workflows) {
|
|
151
|
+
for (const f of await resolveWorkflowArg(w, cwd)) out.add(f);
|
|
152
|
+
}
|
|
153
|
+
return [...out].sort();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {Awaited<ReturnType<typeof pin>>} result
|
|
158
|
+
* @param {{format: 'toon'|'json'|'text', dryRun: boolean, cwd?: string}} opts
|
|
159
|
+
*/
|
|
160
|
+
export function renderPin(result, opts) {
|
|
161
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
162
|
+
if (opts.format === 'json') {
|
|
163
|
+
return format('json', [], {
|
|
164
|
+
status: result.status,
|
|
165
|
+
json: {
|
|
166
|
+
dryRun: opts.dryRun,
|
|
167
|
+
changes: result.changes.map(c => ({ ...c, file: rel(c.file, cwd) })),
|
|
168
|
+
errors: result.errors,
|
|
169
|
+
status: result.status,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const records = [];
|
|
174
|
+
for (const c of result.changes) {
|
|
175
|
+
records.push({
|
|
176
|
+
label: 'PIN',
|
|
177
|
+
fields: {
|
|
178
|
+
id: c.id,
|
|
179
|
+
file: rel(c.file, cwd),
|
|
180
|
+
line: c.line,
|
|
181
|
+
action: c.action,
|
|
182
|
+
from: c.fromRef,
|
|
183
|
+
to: c.toSha,
|
|
184
|
+
kind: c.refType,
|
|
185
|
+
applied: !opts.dryRun,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
for (const e of result.errors) {
|
|
190
|
+
records.push({ label: 'ERROR', fields: { file: rel(e.file ?? '', cwd), action: e.action ?? '', msg: e.error } });
|
|
191
|
+
}
|
|
192
|
+
records.push({ label: 'SUMMARY', fields: { changes: result.changes.length, errors: result.errors.length, dry_run: opts.dryRun } });
|
|
193
|
+
return format(opts.format, records, { status: result.status });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function rel(p, cwd) {
|
|
197
|
+
if (p && p.startsWith(cwd)) return p.slice(cwd.length + 1);
|
|
198
|
+
return p;
|
|
199
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report command - runs audit + dry-run pin + dry-run upgrade and produces
|
|
3
|
+
* a combined view. Useful for "what would change?" review and LLM prompting.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { audit } from './audit.js';
|
|
7
|
+
import { pin } from './pin.js';
|
|
8
|
+
import { upgrade } from './upgrade.js';
|
|
9
|
+
import { format } from '../lib/formatter.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {string} [opts.cwd]
|
|
14
|
+
* @param {string[]} [opts.workflows]
|
|
15
|
+
* @param {string} [opts.token]
|
|
16
|
+
* @param {'major'|'minor'|'patch'} [opts.mode]
|
|
17
|
+
* @param {boolean} [opts.skipResolve] - when true, skip pin/upgrade (offline mode)
|
|
18
|
+
*/
|
|
19
|
+
export async function report({
|
|
20
|
+
cwd = process.cwd(),
|
|
21
|
+
workflows,
|
|
22
|
+
token,
|
|
23
|
+
mode = 'minor',
|
|
24
|
+
skipResolve = false,
|
|
25
|
+
minAgeDays = 7,
|
|
26
|
+
} = {}) {
|
|
27
|
+
const auditResult = await audit({ cwd, workflows, explain: true });
|
|
28
|
+
let pinResult = { changes: [], errors: [], status: 'OK' };
|
|
29
|
+
let upgradeResult = { changes: [], errors: [], skipped: [], status: 'OK' };
|
|
30
|
+
if (!skipResolve) {
|
|
31
|
+
pinResult = await pin({ cwd, workflows, dryRun: true, token });
|
|
32
|
+
upgradeResult = await upgrade({ cwd, workflows, dryRun: true, token, mode, minAgeDays });
|
|
33
|
+
}
|
|
34
|
+
const status = [auditResult.status, pinResult.status, upgradeResult.status].includes('FAIL')
|
|
35
|
+
? 'FAIL' : 'OK';
|
|
36
|
+
return { audit: auditResult, pin: pinResult, upgrade: upgradeResult, status };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {Awaited<ReturnType<typeof report>>} result
|
|
41
|
+
* @param {{format: 'toon'|'json'|'text', mode: string, cwd?: string}} opts
|
|
42
|
+
*/
|
|
43
|
+
export function renderReport(result, opts) {
|
|
44
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
45
|
+
if (opts.format === 'json') {
|
|
46
|
+
return format('json', [], {
|
|
47
|
+
status: result.status,
|
|
48
|
+
json: {
|
|
49
|
+
audit: { files: result.audit.files.map(f => rel(f, cwd)), findings: result.audit.findings, summary: result.audit.summary },
|
|
50
|
+
pin: { changes: result.pin.changes.map(c => ({ ...c, file: rel(c.file, cwd) })) },
|
|
51
|
+
upgrade: { changes: result.upgrade.changes.map(c => ({ ...c, file: rel(c.file, cwd) })), mode: opts.mode },
|
|
52
|
+
status: result.status,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const records = [];
|
|
57
|
+
for (const finding of result.audit.findings) {
|
|
58
|
+
records.push({
|
|
59
|
+
label: 'FINDING',
|
|
60
|
+
fields: {
|
|
61
|
+
id: finding.id,
|
|
62
|
+
...finding.fields,
|
|
63
|
+
line: finding.line,
|
|
64
|
+
explain: finding.explain,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
for (const c of result.pin.changes) {
|
|
69
|
+
records.push({ label: 'PIN', fields: { id: c.id, file: rel(c.file, cwd), action: c.action, from: c.fromRef, to: c.toSha } });
|
|
70
|
+
}
|
|
71
|
+
for (const c of result.upgrade.changes) {
|
|
72
|
+
records.push({ label: 'UPGRADE', fields: { id: c.id, file: rel(c.file, cwd), action: c.action, from: c.fromVersion ?? c.fromRef, to: c.toTag, level: c.level } });
|
|
73
|
+
}
|
|
74
|
+
records.push({
|
|
75
|
+
label: 'SUMMARY',
|
|
76
|
+
fields: {
|
|
77
|
+
files: result.audit.summary.files,
|
|
78
|
+
findings: result.audit.summary.findings,
|
|
79
|
+
critical: result.audit.summary.critical,
|
|
80
|
+
high: result.audit.summary.high,
|
|
81
|
+
medium: result.audit.summary.medium,
|
|
82
|
+
low: result.audit.summary.low,
|
|
83
|
+
pins: result.pin.changes.length,
|
|
84
|
+
upgrades: result.upgrade.changes.length,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return format(opts.format, records, { status: result.status });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function rel(p, cwd) {
|
|
91
|
+
if (p && p.startsWith(cwd)) return p.slice(cwd.length + 1);
|
|
92
|
+
return p;
|
|
93
|
+
}
|