@secureci/cli 0.5.1 → 0.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.
@@ -0,0 +1,603 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AdvisoryChecker = exports.ADVISORY_TTL_MS = exports.VULNERABLE_ACTION_RULE = exports.FAIL_ON_LEVELS = void 0;
37
+ exports.parseVersion = parseVersion;
38
+ exports.compareVersions = compareVersions;
39
+ exports.versionInRange = versionInRange;
40
+ exports.chooseVersion = chooseVersion;
41
+ exports.parseAdvisories = parseAdvisories;
42
+ exports.matchAdvisories = matchAdvisories;
43
+ exports.findingLevel = findingLevel;
44
+ exports.describeFinding = describeFinding;
45
+ exports.advisoryValidationErrors = advisoryValidationErrors;
46
+ exports.describeUnchecked = describeUnchecked;
47
+ exports.getAdvisoryCacheDir = getAdvisoryCacheDir;
48
+ exports.fetchRepoAdvisories = fetchRepoAdvisories;
49
+ exports.targetsFromReferences = targetsFromReferences;
50
+ exports.targetsFromInventory = targetsFromInventory;
51
+ exports.describeReport = describeReport;
52
+ exports.checkFileAdvisories = checkFileAdvisories;
53
+ exports.mergeReports = mergeReports;
54
+ const fs = __importStar(require("fs"));
55
+ const path = __importStar(require("path"));
56
+ const action_fetcher_1 = require("./runner/action-fetcher");
57
+ const inventory_1 = require("./inventory");
58
+ /** Contract `advisories.fail-on` levels, most severe first. */
59
+ exports.FAIL_ON_LEVELS = ['critical', 'high', 'medium', 'low'];
60
+ exports.VULNERABLE_ACTION_RULE = 'secureci/vulnerable-action';
61
+ /** Cached advisories are re-fetched after this long. */
62
+ exports.ADVISORY_TTL_MS = 24 * 60 * 60 * 1000;
63
+ const ADVISORY_TIMEOUT_MS = 15000;
64
+ const VERSION = /^v?(\d+(?:\.\d+)*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
65
+ /** `v1.2.3`, `1.2`, `v4`, `2.0.0-rc.1`; null for anything else. */
66
+ function parseVersion(text) {
67
+ const match = VERSION.exec(text.trim());
68
+ if (!match)
69
+ return null;
70
+ return {
71
+ nums: match[1].split('.').map(Number),
72
+ pre: match[2] ? match[2].split('.') : []
73
+ };
74
+ }
75
+ function comparePre(a, b) {
76
+ // A release ranks above its prereleases.
77
+ if (a.length === 0 || b.length === 0)
78
+ return b.length - a.length;
79
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
80
+ if (a[i] === undefined)
81
+ return -1;
82
+ if (b[i] === undefined)
83
+ return 1;
84
+ const an = /^\d+$/.test(a[i]);
85
+ const bn = /^\d+$/.test(b[i]);
86
+ if (an && bn) {
87
+ const d = Number(a[i]) - Number(b[i]);
88
+ if (d !== 0)
89
+ return d;
90
+ }
91
+ else if (an !== bn) {
92
+ return an ? -1 : 1;
93
+ }
94
+ else if (a[i] !== b[i]) {
95
+ return a[i] < b[i] ? -1 : 1;
96
+ }
97
+ }
98
+ return 0;
99
+ }
100
+ /** Semver order; missing parts count as 0 (`v4` == `4.0.0`). */
101
+ function compareVersions(a, b) {
102
+ for (let i = 0; i < Math.max(a.nums.length, b.nums.length, 3); i++) {
103
+ const d = (a.nums[i] ?? 0) - (b.nums[i] ?? 0);
104
+ if (d !== 0)
105
+ return d;
106
+ }
107
+ return comparePre(a.pre, b.pre);
108
+ }
109
+ const FULL_SHA = /^[0-9a-f]{40}$/i;
110
+ const CONSTRAINT = /^(<=|>=|<|>|=)?\s*(\S+)$/;
111
+ /**
112
+ * Whether `version` satisfies an advisory's `vulnerable_version_range`
113
+ * (comma-separated `<`, `<=`, `>`, `>=`, `=` constraints, all of which
114
+ * must hold), or undefined when either can't be read.
115
+ */
116
+ function versionInRange(version, range) {
117
+ const v = parseVersion(version);
118
+ if (!v || !range.trim())
119
+ return undefined;
120
+ let result = true;
121
+ for (const part of range.split(',')) {
122
+ const match = CONSTRAINT.exec(part.trim());
123
+ const bound = match && parseVersion(match[2]);
124
+ if (!match || !bound)
125
+ return undefined;
126
+ const d = compareVersions(v, bound);
127
+ const ok = match[1] === '<'
128
+ ? d < 0
129
+ : match[1] === '<='
130
+ ? d <= 0
131
+ : match[1] === '>'
132
+ ? d > 0
133
+ : match[1] === '>='
134
+ ? d >= 0
135
+ : d === 0;
136
+ result && (result = ok);
137
+ }
138
+ return result;
139
+ }
140
+ /**
141
+ * The version to compare for a reference:
142
+ * - a full `x.y.z` tag is used as is;
143
+ * - otherwise the commit (`sha`, or what the tag points at in `tags`) is
144
+ * mapped to the most specific release tag at that commit, so a SHA pin
145
+ * or a moving `v4` becomes e.g. `v4.2.1`;
146
+ * - a partial tag that can't be mapped (`v4`, `v4.1`) is compared by its
147
+ * lower bound (`4.0.0`, `4.1.0`) and marked approximate: the release it
148
+ * points at today is unknown, so it "may be affected";
149
+ * - anything else (an untagged commit, a branch) is undefined.
150
+ */
151
+ function chooseVersion(ref, sha, tags) {
152
+ if (!ref)
153
+ return undefined;
154
+ const parsed = FULL_SHA.test(ref) ? null : parseVersion(ref);
155
+ if (parsed && parsed.nums.length >= 3) {
156
+ return { version: ref, approximate: false };
157
+ }
158
+ const commit = sha ?? tags?.get(ref);
159
+ if (commit && tags) {
160
+ let best;
161
+ for (const [name, at] of tags) {
162
+ if (at !== commit.toLowerCase())
163
+ continue;
164
+ const version = parseVersion(name);
165
+ if (!version || version.nums.length < 3)
166
+ continue;
167
+ if (!best || compareVersions(version, best.version) > 0) {
168
+ best = { name, version };
169
+ }
170
+ }
171
+ if (best)
172
+ return { version: best.name, approximate: false };
173
+ }
174
+ if (parsed)
175
+ return { version: ref, approximate: true };
176
+ return undefined;
177
+ }
178
+ // ---------------------------------------------------------------------------
179
+ // Advisories
180
+ const SEVERITIES = {
181
+ critical: 'critical',
182
+ high: 'high',
183
+ medium: 'medium',
184
+ moderate: 'medium',
185
+ low: 'low'
186
+ };
187
+ function asString(value) {
188
+ return typeof value === 'string' && value ? value : undefined;
189
+ }
190
+ /**
191
+ * Advisories from a `GET /advisories` response body, keeping the fields
192
+ * used here. Withdrawn advisories and other ecosystems are dropped.
193
+ */
194
+ function parseAdvisories(body) {
195
+ if (!Array.isArray(body))
196
+ return [];
197
+ const advisories = [];
198
+ for (const raw of body) {
199
+ if (!raw || typeof raw !== 'object')
200
+ continue;
201
+ const item = raw;
202
+ const ghsaId = asString(item.ghsa_id);
203
+ if (!ghsaId || item.withdrawn_at)
204
+ continue;
205
+ const vulnerabilities = [];
206
+ for (const v of Array.isArray(item.vulnerabilities)
207
+ ? item.vulnerabilities
208
+ : []) {
209
+ if (!v || typeof v !== 'object')
210
+ continue;
211
+ const vuln = v;
212
+ const pkg = (vuln.package ?? {});
213
+ const name = asString(pkg.name);
214
+ const range = asString(vuln.vulnerable_version_range);
215
+ if (!name || !range)
216
+ continue;
217
+ if (pkg.ecosystem !== undefined && pkg.ecosystem !== 'actions')
218
+ continue;
219
+ // A string in /advisories; an { identifier } object in other APIs.
220
+ const fp = vuln.first_patched_version;
221
+ const firstPatched = asString(fp) ??
222
+ asString(fp?.identifier);
223
+ vulnerabilities.push({
224
+ package: name.toLowerCase(),
225
+ range,
226
+ ...(firstPatched ? { firstPatched } : {})
227
+ });
228
+ }
229
+ const cveId = asString(item.cve_id);
230
+ const url = asString(item.html_url);
231
+ advisories.push({
232
+ ghsaId,
233
+ ...(cveId ? { cveId } : {}),
234
+ summary: asString(item.summary) ?? '',
235
+ severity: SEVERITIES[String(item.severity ?? '').toLowerCase()] ?? 'unknown',
236
+ ...(url ? { url } : {}),
237
+ vulnerabilities
238
+ });
239
+ }
240
+ return advisories;
241
+ }
242
+ function packageNames(target) {
243
+ const repo = `${target.owner}/${target.repo}`.toLowerCase();
244
+ return target.subpath
245
+ ? [repo, `${repo}/${target.subpath.toLowerCase()}`]
246
+ : [repo];
247
+ }
248
+ /** The advisories (of the target's repository) that name this action. */
249
+ function relevant(target, advisories) {
250
+ const names = packageNames(target);
251
+ return advisories
252
+ .map((advisory) => ({
253
+ advisory,
254
+ vulns: advisory.vulnerabilities.filter((v) => names.includes(v.package))
255
+ }))
256
+ .filter((a) => a.vulns.length > 0);
257
+ }
258
+ /** The findings for one reference at `version`. */
259
+ function matchAdvisories(target, version, advisories) {
260
+ const findings = [];
261
+ for (const { advisory, vulns } of relevant(target, advisories)) {
262
+ for (const vuln of vulns) {
263
+ const inRange = versionInRange(version.version, vuln.range);
264
+ if (inRange === false)
265
+ continue;
266
+ findings.push({
267
+ uses: target.uses,
268
+ ghsaId: advisory.ghsaId,
269
+ ...(advisory.cveId ? { cveId: advisory.cveId } : {}),
270
+ severity: advisory.severity,
271
+ summary: advisory.summary,
272
+ ...(advisory.url ? { url: advisory.url } : {}),
273
+ range: vuln.range,
274
+ ...(vuln.firstPatched ? { firstPatched: vuln.firstPatched } : {}),
275
+ version: version.version,
276
+ // An unreadable range can't rule the version out: may be affected.
277
+ approximate: version.approximate || inRange === undefined,
278
+ ...(target.path ? { path: target.path } : {})
279
+ });
280
+ break;
281
+ }
282
+ }
283
+ return findings;
284
+ }
285
+ const RANK = {
286
+ critical: 4,
287
+ high: 3,
288
+ medium: 2,
289
+ low: 1,
290
+ unknown: 0
291
+ };
292
+ /**
293
+ * `error` for a finding at or above `failOn` (default `high`, so
294
+ * critical/high fail and medium/low warn). An approximate match is always
295
+ * a warning: which release a moving tag points at isn't known.
296
+ */
297
+ function findingLevel(finding, failOn = 'high') {
298
+ if (finding.approximate)
299
+ return 'warning';
300
+ return RANK[finding.severity] >= RANK[failOn] ? 'error' : 'warning';
301
+ }
302
+ /** A one-line description of a finding. */
303
+ function describeFinding(f) {
304
+ const id = `${f.ghsaId} (${f.cveId ? `${f.cveId}, ` : ''}${f.severity})`;
305
+ const head = f.approximate
306
+ ? `${f.uses} may be affected by a known security advisory: ${id}`
307
+ : `${f.uses} has a known security advisory: ${id}`;
308
+ const summary = f.summary ? `: ${f.summary.replace(/\.?\s*$/, '.')}` : '.';
309
+ const fix = f.firstPatched
310
+ ? `first patched version: ${f.firstPatched}`
311
+ : 'no patched version yet';
312
+ const nums = parseVersion(f.version)?.nums ?? [];
313
+ const lowerBound = [...nums, 0, 0].slice(0, Math.max(3, nums.length));
314
+ const approx = !f.approximate
315
+ ? ''
316
+ : nums.length > 0 && nums.length < 3
317
+ ? ` ${f.version} is a moving tag whose release could not be determined, so it was compared as ${lowerBound.join('.')}; pin a release tag or SHA to check it exactly.`
318
+ : ' The affected range could not be read.';
319
+ return `${head}${summary} Affected versions: ${f.range}; ${fix}.${approx}${f.url ? ` ${f.url}` : ''}`;
320
+ }
321
+ /** Findings as `secureci/vulnerable-action` validation errors. */
322
+ function advisoryValidationErrors(report, failOn) {
323
+ return report.findings.map((f) => ({
324
+ path: f.path ?? f.uses,
325
+ message: describeFinding(f),
326
+ severity: findingLevel(f, failOn),
327
+ rule: exports.VULNERABLE_ACTION_RULE
328
+ }));
329
+ }
330
+ /** One warning line for repositories that weren't checked, if any. */
331
+ function describeUnchecked(unchecked) {
332
+ if (unchecked.length === 0)
333
+ return undefined;
334
+ const byReason = new Map();
335
+ for (const { repo, reason } of unchecked) {
336
+ byReason.set(reason, [...(byReason.get(reason) ?? []), repo]);
337
+ }
338
+ return `Security advisories not checked for ${[...byReason]
339
+ .map(([reason, repos]) => `${repos.join(', ')} (${reason})`)
340
+ .join('; ')}.`;
341
+ }
342
+ // ---------------------------------------------------------------------------
343
+ // Fetching and caching
344
+ /** Where advisory responses are cached. */
345
+ function getAdvisoryCacheDir() {
346
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
347
+ return path.join(homeDir, '.secureci', 'advisories');
348
+ }
349
+ const NAME = /^[A-Za-z0-9_.-]+$/;
350
+ const validName = (name) => NAME.test(name) && name !== '.' && name !== '..';
351
+ function describeError(error, authenticated) {
352
+ const status = error.status;
353
+ if (status === 403 || status === 429) {
354
+ return `HTTP ${status}: rate-limited or forbidden${authenticated ? '' : '; set GITHUB_TOKEN for a higher rate limit'}`;
355
+ }
356
+ return error instanceof Error ? error.message : String(error);
357
+ }
358
+ /**
359
+ * The advisories naming `owner/repo` in the actions ecosystem, cached per
360
+ * repository for `ttlMs` (default 24h). Offline, the cache is used at any
361
+ * age. If the API fails, a stale cache is used when there is one.
362
+ * Only the first page (100 advisories) is read.
363
+ */
364
+ async function fetchRepoAdvisories(owner, repo, options = {}) {
365
+ if (!validName(owner) || !validName(repo))
366
+ return { advisories: [] };
367
+ const api = (options.apiUrl ?? (0, action_fetcher_1.apiUrl)()).replace(/\/+$/, '');
368
+ const now = (options.now ?? Date.now)();
369
+ const ttl = options.ttlMs ?? exports.ADVISORY_TTL_MS;
370
+ const file = path.join(options.cacheDir ?? getAdvisoryCacheDir(), owner.toLowerCase(), `${repo.toLowerCase()}.json`);
371
+ let cached;
372
+ try {
373
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
374
+ if (data.api === api && Array.isArray(data.advisories))
375
+ cached = data;
376
+ }
377
+ catch {
378
+ cached = undefined;
379
+ }
380
+ const isStale = cached ? now - cached.fetchedAt >= ttl : false;
381
+ if (cached && !isStale)
382
+ return { advisories: cached.advisories };
383
+ if (options.offline) {
384
+ return cached
385
+ ? { advisories: cached.advisories, stale: true }
386
+ : { error: 'offline and not cached' };
387
+ }
388
+ const httpGet = options.httpGet ??
389
+ ((url, headers) => (0, action_fetcher_1.getText)(url, headers, ADVISORY_TIMEOUT_MS));
390
+ const query = new URLSearchParams({
391
+ ecosystem: 'actions',
392
+ affects: `${owner}/${repo}`,
393
+ per_page: '100'
394
+ });
395
+ const url = `${api}/advisories?${query}`;
396
+ const headers = (token) => ({
397
+ 'User-Agent': 'secureci-cli',
398
+ Accept: 'application/vnd.github+json',
399
+ 'X-GitHub-Api-Version': '2022-11-28',
400
+ ...(token ? { Authorization: `Bearer ${token}` } : {})
401
+ });
402
+ let token = options.token === undefined ? (0, action_fetcher_1.githubToken)() : options.token;
403
+ let body;
404
+ try {
405
+ try {
406
+ body = await httpGet(url, headers(token));
407
+ }
408
+ catch (error) {
409
+ // A rejected token shouldn't block public advisories.
410
+ if (!token || error.status !== 401)
411
+ throw error;
412
+ token = null;
413
+ body = await httpGet(url, headers(null));
414
+ }
415
+ const advisories = parseAdvisories(JSON.parse(body));
416
+ fs.mkdirSync(path.dirname(file), { recursive: true });
417
+ const entry = { api, fetchedAt: now, advisories };
418
+ fs.writeFileSync(file, JSON.stringify(entry, null, 2));
419
+ return { advisories };
420
+ }
421
+ catch (error) {
422
+ const message = describeError(error, !!token);
423
+ return cached
424
+ ? { advisories: cached.advisories, stale: true, error: message }
425
+ : { error: message };
426
+ }
427
+ }
428
+ /**
429
+ * Checks references against the advisory database, querying each
430
+ * repository at most once (and listing its tags at most once, only when a
431
+ * version has to be worked out for a repository that has advisories).
432
+ */
433
+ class AdvisoryChecker {
434
+ constructor(options = {}) {
435
+ this.options = options;
436
+ this.lookups = new Map();
437
+ this.tags = new Map();
438
+ }
439
+ lookup(owner, repo) {
440
+ const key = `${owner}/${repo}`.toLowerCase();
441
+ let found = this.lookups.get(key);
442
+ if (!found) {
443
+ this.options.onLookup?.(`${owner}/${repo}`);
444
+ found = fetchRepoAdvisories(owner, repo, this.options);
445
+ this.lookups.set(key, found);
446
+ }
447
+ return found;
448
+ }
449
+ tagsOf(owner, repo) {
450
+ const key = `${owner}/${repo}`.toLowerCase();
451
+ let found = this.tags.get(key);
452
+ if (!found) {
453
+ const list = this.options.listTags ??
454
+ ((o, r) => (0, action_fetcher_1.lsRemoteTags)(`${(0, action_fetcher_1.serverUrl)()}/${o}/${r}.git`));
455
+ found = list(owner, repo);
456
+ this.tags.set(key, found);
457
+ }
458
+ return found;
459
+ }
460
+ async check(targets) {
461
+ const report = {
462
+ findings: [],
463
+ unknownVersions: [],
464
+ unchecked: [],
465
+ stale: []
466
+ };
467
+ const seen = new Set();
468
+ for (const target of targets) {
469
+ const repoName = `${target.owner}/${target.repo}`;
470
+ const key = repoName.toLowerCase();
471
+ const result = await this.lookup(target.owner, target.repo);
472
+ if (!seen.has(key)) {
473
+ seen.add(key);
474
+ if (!result.advisories) {
475
+ report.unchecked.push({
476
+ repo: repoName,
477
+ reason: result.error
478
+ });
479
+ }
480
+ else if (result.stale) {
481
+ report.stale.push(repoName);
482
+ }
483
+ }
484
+ const advisories = relevant(target, result.advisories ?? []);
485
+ if (advisories.length === 0)
486
+ continue;
487
+ let version = chooseVersion(target.ref, target.sha);
488
+ if ((!version || version.approximate) && !this.options.offline) {
489
+ const tags = await this.tagsOf(target.owner, target.repo);
490
+ version = chooseVersion(target.ref, target.sha, tags);
491
+ }
492
+ if (!version) {
493
+ report.unknownVersions.push({
494
+ uses: target.uses,
495
+ advisories: advisories.map((a) => a.advisory.ghsaId)
496
+ });
497
+ continue;
498
+ }
499
+ report.findings.push(...matchAdvisories(target, version, result.advisories ?? []));
500
+ }
501
+ return report;
502
+ }
503
+ }
504
+ exports.AdvisoryChecker = AdvisoryChecker;
505
+ // ---------------------------------------------------------------------------
506
+ // Wiring for `inventory` and `validate`
507
+ /** Distinct remote references as targets (first path kept). */
508
+ function targetsFromReferences(refs) {
509
+ const byUses = new Map();
510
+ for (const ref of refs) {
511
+ if (ref.kind !== 'action' && ref.kind !== 'reusable-workflow')
512
+ continue;
513
+ if (!ref.owner || !ref.repo || byUses.has(ref.uses))
514
+ continue;
515
+ byUses.set(ref.uses, {
516
+ uses: ref.uses,
517
+ owner: ref.owner,
518
+ repo: ref.repo,
519
+ subpath: ref.subpath,
520
+ ref: ref.ref,
521
+ sha: ref.pinned ? ref.ref?.toLowerCase() : undefined,
522
+ path: ref.path
523
+ });
524
+ }
525
+ return [...byUses.values()];
526
+ }
527
+ /** Inventory actions as targets, with the commit each resolved to. */
528
+ function targetsFromInventory(actions) {
529
+ return actions.map((a) => ({
530
+ uses: a.uses,
531
+ owner: a.owner,
532
+ repo: a.repo,
533
+ subpath: a.subpath,
534
+ ref: a.ref,
535
+ sha: a.resolvedSha,
536
+ path: a.paths[0]
537
+ }));
538
+ }
539
+ /** Human-readable lines for a report, with the level to log each at. */
540
+ function describeReport(report, failOn) {
541
+ const lines = report.findings.map((f) => ({
542
+ level: findingLevel(f, failOn) === 'error' ? 'error' : 'warn',
543
+ kind: 'finding',
544
+ text: describeFinding(f)
545
+ }));
546
+ for (const u of report.unknownVersions) {
547
+ const name = u.uses.slice(0, u.uses.lastIndexOf('@'));
548
+ const repo = name.split('/').slice(0, 2).join('/');
549
+ lines.push({
550
+ level: 'info',
551
+ kind: 'note',
552
+ text: `${u.uses}: unknown version (the commit is not at a release tag), so advisories ${u.advisories.join(', ')} for ${repo} could not be checked against it.`
553
+ });
554
+ }
555
+ const unchecked = describeUnchecked(report.unchecked);
556
+ if (unchecked)
557
+ lines.push({ level: 'warn', kind: 'note', text: unchecked });
558
+ if (report.stale.length > 0) {
559
+ lines.push({
560
+ level: 'warn',
561
+ kind: 'note',
562
+ text: `Used cached security advisories older than 24h for ${report.stale.join(', ')} (they could not be refreshed).`
563
+ });
564
+ }
565
+ return lines;
566
+ }
567
+ /**
568
+ * Check a workflow's (or composite action's) own `uses:` references,
569
+ * including those in local composite actions and reusable workflows it
570
+ * calls, as `secureci/vulnerable-action` validation errors.
571
+ */
572
+ async function checkFileAdvisories(parsed, checker, failOn) {
573
+ const report = await checker.check(targetsFromReferences((0, inventory_1.collectReferences)(parsed)));
574
+ return { errors: advisoryValidationErrors(report, failOn), report };
575
+ }
576
+ /** Several reports as one, each reference and repository listed once. */
577
+ function mergeReports(reports) {
578
+ const merged = {
579
+ findings: [],
580
+ unknownVersions: [],
581
+ unchecked: [],
582
+ stale: []
583
+ };
584
+ for (const r of reports) {
585
+ merged.findings.push(...r.findings);
586
+ for (const u of r.unknownVersions) {
587
+ if (!merged.unknownVersions.some((m) => m.uses === u.uses)) {
588
+ merged.unknownVersions.push(u);
589
+ }
590
+ }
591
+ for (const u of r.unchecked) {
592
+ if (!merged.unchecked.some((m) => m.repo === u.repo)) {
593
+ merged.unchecked.push(u);
594
+ }
595
+ }
596
+ for (const s of r.stale) {
597
+ if (!merged.stale.includes(s))
598
+ merged.stale.push(s);
599
+ }
600
+ }
601
+ return merged;
602
+ }
603
+ //# sourceMappingURL=advisories.js.map