@node-core/utils 7.2.2 → 7.3.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/bin/ncu-ci.js +52 -6
- package/lib/ci/ci_failure_parser.js +132 -117
- package/lib/ci/failure_file_scanner.js +153 -0
- package/lib/ci/resume_ci.js +150 -0
- package/lib/ci/run_ci.js +14 -4
- package/lib/cli.js +7 -1
- package/lib/prepare_security.js +56 -107
- package/lib/request.js +52 -8
- package/lib/security-release/draft.js +32 -0
- package/lib/security-release/preparation.js +168 -0
- package/lib/security-release/security-release.js +32 -2
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { getReportSeverity, getSummary } from './security-release.js';
|
|
2
|
+
|
|
3
|
+
// Request.getTriagedReports rejects failed pages rather than returning partial data.
|
|
4
|
+
// Selection, metadata edits, and all mutations belong to the caller.
|
|
5
|
+
export async function listSecurityReleaseCandidates(request) {
|
|
6
|
+
const { data } = await request.getTriagedReports();
|
|
7
|
+
return data;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getReportPRURL(report) {
|
|
11
|
+
const customFieldValues = report.relationships.custom_field_values?.data ?? [];
|
|
12
|
+
return customFieldValues[0]?.attributes?.value ?? '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildIncludedTriagedReport(report, options = {}) {
|
|
16
|
+
const {
|
|
17
|
+
affectedVersions = '',
|
|
18
|
+
patchAuthors = [],
|
|
19
|
+
prURL = getReportPRURL(report)
|
|
20
|
+
} = options;
|
|
21
|
+
const {
|
|
22
|
+
id,
|
|
23
|
+
attributes: { title, cve_ids = [] },
|
|
24
|
+
relationships: { reporter }
|
|
25
|
+
} = report;
|
|
26
|
+
const link = `https://hackerone.com/reports/${id}`;
|
|
27
|
+
const summaryContent = getSummary(report);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
id,
|
|
31
|
+
title,
|
|
32
|
+
cveIds: cve_ids,
|
|
33
|
+
severity: getReportSeverity(report),
|
|
34
|
+
summary: summaryContent ?? '',
|
|
35
|
+
patchAuthors,
|
|
36
|
+
prURL,
|
|
37
|
+
affectedVersions: affectedVersions
|
|
38
|
+
.split(',')
|
|
39
|
+
.map((v) => v.replace('v', '').trim())
|
|
40
|
+
.filter(Boolean),
|
|
41
|
+
link,
|
|
42
|
+
reporter: reporter?.data?.attributes?.username ?? ''
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getMissingReportInformation(report) {
|
|
47
|
+
const missing = [];
|
|
48
|
+
|
|
49
|
+
if (!report.severity?.rating) missing.push('severity rating');
|
|
50
|
+
if (!report.severity?.cvss_vector_string) missing.push('CVSS vector');
|
|
51
|
+
if (!report.severity?.weakness_id) missing.push('weakness ID');
|
|
52
|
+
if (!report.summary) missing.push('team summary');
|
|
53
|
+
if (!report.prURL) missing.push('PR URL');
|
|
54
|
+
if (!report.patchAuthors?.length) missing.push('patch authors');
|
|
55
|
+
if (!Object.keys(report.affectedVersions ?? {}).length) missing.push('affected versions');
|
|
56
|
+
|
|
57
|
+
return missing;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function groupMissingReportInformation(reports) {
|
|
61
|
+
const grouped = new Map();
|
|
62
|
+
|
|
63
|
+
for (const report of reports) {
|
|
64
|
+
for (const field of report.missing) {
|
|
65
|
+
const current = grouped.get(field) ?? [];
|
|
66
|
+
current.push(report);
|
|
67
|
+
grouped.set(field, current);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Array.from(grouped.entries())
|
|
72
|
+
.map(([field, fieldReports]) => ({
|
|
73
|
+
field,
|
|
74
|
+
reports: fieldReports
|
|
75
|
+
}))
|
|
76
|
+
.sort((a, b) => b.reports.length - a.reports.length);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeReleaseDate(value) {
|
|
80
|
+
if (value === 'TBD') return value;
|
|
81
|
+
if (typeof value !== 'string' || !/^\d{4}([/-])\d{2}\1\d{2}$/.test(value)) {
|
|
82
|
+
throw new Error('Release date must be YYYY-MM-DD, YYYY/MM/DD, or TBD');
|
|
83
|
+
}
|
|
84
|
+
const date = value.replaceAll('/', '-');
|
|
85
|
+
const timestamp = Date.parse(`${date}T00:00:00Z`);
|
|
86
|
+
if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString().slice(0, 10) !== date) {
|
|
87
|
+
throw new Error('Invalid release date');
|
|
88
|
+
}
|
|
89
|
+
return date;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeAffectedVersions(value, fallbackPR = '') {
|
|
93
|
+
if (value == null || value === '') return {};
|
|
94
|
+
if (typeof fallbackPR !== 'string') throw new Error('PR URL must be a string');
|
|
95
|
+
let entries;
|
|
96
|
+
if (typeof value === 'string' || Array.isArray(value)) {
|
|
97
|
+
const lines = typeof value === 'string' ? value.split(',') : value;
|
|
98
|
+
entries = lines.map((line) => [line, fallbackPR]);
|
|
99
|
+
} else if (typeof value === 'object') {
|
|
100
|
+
entries = Object.entries(value);
|
|
101
|
+
} else {
|
|
102
|
+
throw new Error('Affected versions must be release lines or a PR map');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const result = new Map();
|
|
106
|
+
for (const [line, prURL] of entries) {
|
|
107
|
+
if (typeof line !== 'string') throw new Error('Release line must be a string');
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
const match = /^v?(\d+)(?:\.x)?$/.exec(trimmed);
|
|
110
|
+
const normalized = trimmed === 'main' ? 'main' : match && `${match[1]}.x`;
|
|
111
|
+
if (!normalized) throw new Error(`Invalid release line: ${line}`);
|
|
112
|
+
if (typeof prURL !== 'string') throw new Error(`PR URL for ${normalized} must be a string`);
|
|
113
|
+
if (result.has(normalized) && result.get(normalized) !== prURL) {
|
|
114
|
+
throw new Error(`Conflicting PR URLs for ${normalized}`);
|
|
115
|
+
}
|
|
116
|
+
result.set(normalized, prURL);
|
|
117
|
+
}
|
|
118
|
+
return Object.fromEntries(result);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// This function only builds data. It does not fetch reports, prompt, write files,
|
|
122
|
+
// change branches, commit, push, or create a pull request.
|
|
123
|
+
export function prepareSecurityRelease({ releaseDate, reports, dependencies = {} }) {
|
|
124
|
+
const date = normalizeReleaseDate(releaseDate);
|
|
125
|
+
if (!Array.isArray(reports)) throw new Error('Reports must be an array');
|
|
126
|
+
if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) {
|
|
127
|
+
throw new Error('Dependencies must be an object');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ids = new Set();
|
|
131
|
+
const selected = reports.map((report) => {
|
|
132
|
+
if (!report || typeof report.id !== 'string' || !/^\d+$/.test(report.id)) {
|
|
133
|
+
throw new Error('Report ID must be a numeric string');
|
|
134
|
+
}
|
|
135
|
+
if (ids.has(report.id)) throw new Error(`Duplicate report ID: ${report.id}`);
|
|
136
|
+
ids.add(report.id);
|
|
137
|
+
return {
|
|
138
|
+
...structuredClone(report),
|
|
139
|
+
// A canonical patch is not evidence of a backport for every affected line.
|
|
140
|
+
affectedVersions: normalizeAffectedVersions(report.affectedVersions)
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const deps = Object.fromEntries(Object.entries(dependencies).map(([name, updates]) => {
|
|
145
|
+
const normalizeUpdate = (update) => {
|
|
146
|
+
if (!update || typeof update !== 'object' || Array.isArray(update)) {
|
|
147
|
+
throw new Error(`Invalid dependency update: ${name}`);
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
...structuredClone(update),
|
|
151
|
+
affectedVersions: normalizeAffectedVersions(update.affectedVersions, update.prURL ?? '')
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
const normalized = Array.isArray(updates)
|
|
155
|
+
? updates.map(normalizeUpdate)
|
|
156
|
+
: normalizeUpdate(updates);
|
|
157
|
+
return [name, normalized];
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
release: { releaseDate: date, reports: selected, dependencies: deps },
|
|
162
|
+
missingInformation: selected.flatMap((report) => {
|
|
163
|
+
const missing = getMissingReportInformation(report);
|
|
164
|
+
if (!missing.length) return [];
|
|
165
|
+
return [{ id: report.id, title: report.title, link: report.link, missing }];
|
|
166
|
+
})
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -79,10 +79,24 @@ export async function checkoutOnSecurityReleaseBranch(cli, repository) {
|
|
|
79
79
|
cli.info(`Current branch: ${currentBranch} `);
|
|
80
80
|
|
|
81
81
|
if (currentBranch !== NEXT_SECURITY_RELEASE_BRANCH) {
|
|
82
|
+
const refs = new Set(runSync('git', [
|
|
83
|
+
'for-each-ref', '--format=%(refname)',
|
|
84
|
+
`refs/heads/${NEXT_SECURITY_RELEASE_BRANCH}`,
|
|
85
|
+
`refs/remotes/origin/${NEXT_SECURITY_RELEASE_BRANCH}`
|
|
86
|
+
]).trim().split('\n'));
|
|
87
|
+
let args;
|
|
88
|
+
if (refs.has(`refs/heads/${NEXT_SECURITY_RELEASE_BRANCH}`)) {
|
|
89
|
+
args = ['checkout', NEXT_SECURITY_RELEASE_BRANCH];
|
|
90
|
+
} else if (refs.has(`refs/remotes/origin/${NEXT_SECURITY_RELEASE_BRANCH}`)) {
|
|
91
|
+
args = ['checkout', '-b', NEXT_SECURITY_RELEASE_BRANCH,
|
|
92
|
+
'--track', `origin/${NEXT_SECURITY_RELEASE_BRANCH}`];
|
|
93
|
+
} else {
|
|
94
|
+
args = ['checkout', '-b', NEXT_SECURITY_RELEASE_BRANCH];
|
|
95
|
+
}
|
|
82
96
|
await runSecurityGitCommand(
|
|
83
97
|
cli,
|
|
84
|
-
|
|
85
|
-
`This
|
|
98
|
+
args,
|
|
99
|
+
`This switches to ${NEXT_SECURITY_RELEASE_BRANCH} without resetting an existing branch.`
|
|
86
100
|
);
|
|
87
101
|
cli.ok(`Checkout on branch: ${NEXT_SECURITY_RELEASE_BRANCH} `);
|
|
88
102
|
}
|
|
@@ -95,6 +109,22 @@ export async function commitAndPushVulnerabilitiesJSON(
|
|
|
95
109
|
) {
|
|
96
110
|
await checkRemote(cli, repository);
|
|
97
111
|
|
|
112
|
+
const files = (Array.isArray(filePath) ? filePath : [filePath])
|
|
113
|
+
.map((file) => path.resolve(file));
|
|
114
|
+
const stagedFiles = runSync('git', ['diff', '--cached', '--name-only', '-z'])
|
|
115
|
+
.split('\0').filter(Boolean);
|
|
116
|
+
const unrelated = stagedFiles.filter((file) => {
|
|
117
|
+
const absolute = path.resolve(file);
|
|
118
|
+
return !files.some((target) =>
|
|
119
|
+
absolute === target || absolute.startsWith(`${target}${path.sep}`));
|
|
120
|
+
});
|
|
121
|
+
if (unrelated.length) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'Unrelated staged changes would be included in the security release commit: ' +
|
|
124
|
+
unrelated.join(', ') + '. Commit or unstage them before continuing.'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
98
128
|
if (Array.isArray(filePath)) {
|
|
99
129
|
for (const currentPath of filePath) {
|
|
100
130
|
runSync('git', ['add', currentPath]);
|