forged-cli 0.2.0 → 0.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/forged.js +5 -1
- package/package.json +1 -1
- package/src/commands/scan.js +22 -5
- package/src/utils/trustedPublishers.js +27 -0
- package/src/utils/verifyIntegrity.js +6 -4
package/bin/forged.js
CHANGED
|
@@ -44,9 +44,13 @@ if (command === 'readme') {
|
|
|
44
44
|
|
|
45
45
|
if (command === 'scan') {
|
|
46
46
|
const { scanCommand } = await import('../src/commands/scan.js');
|
|
47
|
+
const { resolve } = await import('path');
|
|
47
48
|
const report = args.includes('--report') || args.includes('--report-md');
|
|
48
49
|
const reportFormat = args.includes('--report-md') ? 'markdown' : 'json';
|
|
49
|
-
|
|
50
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
51
|
+
const pathArg = args.find(a => !a.startsWith('--'));
|
|
52
|
+
const targetPath = pathArg ? resolve(pathArg) : process.cwd();
|
|
53
|
+
await scanCommand(targetPath, { report, reportFormat, verbose });
|
|
50
54
|
process.exit(0);
|
|
51
55
|
}
|
|
52
56
|
|
package/package.json
CHANGED
package/src/commands/scan.js
CHANGED
|
@@ -107,12 +107,12 @@ function saveMarkdownReport(reportPath, data) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
export async function scanCommand(cwd = process.cwd(), opts = {}) {
|
|
110
|
-
const { report, reportFormat = 'json' } = opts;
|
|
110
|
+
const { report, reportFormat = 'json', verbose = false } = opts;
|
|
111
111
|
|
|
112
112
|
console.log(`\n${bold}⚒ Forged Scanner${reset}`);
|
|
113
113
|
console.log(` Scanning: ${cwd}\n`);
|
|
114
114
|
|
|
115
|
-
const findings = { warnings: [], errors: [], info: [] };
|
|
115
|
+
const findings = { warnings: [], errors: [], info: [], suppressed: [] };
|
|
116
116
|
let packagesVerified = 0;
|
|
117
117
|
|
|
118
118
|
// 1. Parse package.json
|
|
@@ -127,7 +127,9 @@ export async function scanCommand(cwd = process.cwd(), opts = {}) {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
// 3. Typosquat check
|
|
130
|
+
const popularSet = new Set(POPULAR_PACKAGES);
|
|
130
131
|
for (const name of Object.keys(deps)) {
|
|
132
|
+
if (popularSet.has(name)) continue;
|
|
131
133
|
for (const { known, distance } of isSuspiciousName(name, POPULAR_PACKAGES)) {
|
|
132
134
|
findings.warnings.push({
|
|
133
135
|
package: name,
|
|
@@ -153,7 +155,9 @@ export async function scanCommand(cwd = process.cwd(), opts = {}) {
|
|
|
153
155
|
} else {
|
|
154
156
|
packagesVerified = total || 0;
|
|
155
157
|
for (const f of integrityFindings) {
|
|
156
|
-
(f.type === 'error'
|
|
158
|
+
if (f.type === 'error') findings.errors.push(f);
|
|
159
|
+
else if (f.type === 'info') findings.suppressed.push(f);
|
|
160
|
+
else findings.warnings.push(f);
|
|
157
161
|
}
|
|
158
162
|
if (total) findings.info.push(`Verified ${total} packages against npm registry`);
|
|
159
163
|
}
|
|
@@ -192,12 +196,25 @@ export async function scanCommand(cwd = process.cwd(), opts = {}) {
|
|
|
192
196
|
}
|
|
193
197
|
}
|
|
194
198
|
|
|
199
|
+
// Verbose: show suppressed trusted rotations
|
|
200
|
+
if (verbose && findings.suppressed.length > 0) {
|
|
201
|
+
console.log(`${bold}SUPPRESSED — trusted publisher rotations:${reset}`);
|
|
202
|
+
for (const s of findings.suppressed) {
|
|
203
|
+
console.log(` ${green}~${reset} ${s.package}@${s.version}: ${s.message}`);
|
|
204
|
+
}
|
|
205
|
+
console.log();
|
|
206
|
+
}
|
|
207
|
+
|
|
195
208
|
if (findings.info.length > 0) {
|
|
196
209
|
for (const i of findings.info) console.log(` ${green}ℹ${reset} ${i}`);
|
|
197
|
-
console.log();
|
|
198
210
|
}
|
|
211
|
+
if (findings.suppressed.length > 0) {
|
|
212
|
+
const note = verbose ? '' : ' (run with --verbose to see them)';
|
|
213
|
+
console.log(` ${green}ℹ${reset} ${findings.suppressed.length} trusted publisher rotation(s) suppressed${note}`);
|
|
214
|
+
}
|
|
215
|
+
console.log();
|
|
199
216
|
|
|
200
|
-
console.log(`${bold}Summary:${reset} ${findings.errors.length} error(s), ${findings.warnings.length} warning(s)\n`);
|
|
217
|
+
console.log(`${bold}Summary:${reset} ${findings.errors.length} error(s), ${findings.warnings.length} warning(s), ${findings.suppressed.length} suppressed\n`);
|
|
201
218
|
|
|
202
219
|
// Save report if requested
|
|
203
220
|
if (report) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Known npm core team — ownership rotates freely between these accounts
|
|
2
|
+
export const NPM_TEAM = new Set([
|
|
3
|
+
'isaacs', 'gar', 'lukekarrys', 'nlf', 'zkat', 'ruyadorno',
|
|
4
|
+
'npm-cli-ops', 'iarna', 'darcyclarke', 'wraithgar', 'fritzy',
|
|
5
|
+
'GitHub Actions',
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
// Trusted community maintainers — well-known, long-standing accounts
|
|
9
|
+
export const TRUSTED_COMMUNITY = new Set([
|
|
10
|
+
'sindresorhus', 'jbnicolai', 'jonschlinkert', 'doowb',
|
|
11
|
+
'tjholowaychuk', 'dominicbarnes', 'loganfsmyth', 'eemeli',
|
|
12
|
+
'styfle', 'leo', 'dsblv', 'ljharb', 'nicolo-ribaudo',
|
|
13
|
+
'mysticatea', 'feross', 'mafintosh',
|
|
14
|
+
'phated', 'nodejs-foundation', 'legendecas',
|
|
15
|
+
'ulisesgascon', 'formidablelabs',
|
|
16
|
+
'blakeembrey', 'wesleytodd', 'danez', 'domenic', 'panva', 'joshuakgoldberg',
|
|
17
|
+
'matteo.collina', 'rvagg',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export function classifyPublisherChange(from, to) {
|
|
21
|
+
const fromTrusted = NPM_TEAM.has(from) || TRUSTED_COMMUNITY.has(from);
|
|
22
|
+
const toTrusted = NPM_TEAM.has(to) || TRUSTED_COMMUNITY.has(to);
|
|
23
|
+
|
|
24
|
+
if (toTrusted) return 'info'; // handed to a trusted publisher — suppress
|
|
25
|
+
if (fromTrusted) return 'warn'; // trusted → unknown — flag it
|
|
26
|
+
return 'warn'; // unknown → unknown — flag it
|
|
27
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { classifyPublisherChange } from './trustedPublishers.js';
|
|
3
4
|
|
|
4
5
|
// Fetch registry metadata with a simple in-memory cache to avoid duplicate requests
|
|
5
6
|
const registryCache = new Map();
|
|
@@ -108,12 +109,13 @@ export async function verifyTarballIntegrity(cwd, onProgress) {
|
|
|
108
109
|
const versions = Object.keys(registryMeta.versions || {});
|
|
109
110
|
const idx = versions.indexOf(meta.version);
|
|
110
111
|
if (idx > 0) {
|
|
111
|
-
const prevVersion
|
|
112
|
-
const prevPublisher
|
|
113
|
-
const currPublisher
|
|
112
|
+
const prevVersion = versions[idx - 1];
|
|
113
|
+
const prevPublisher = registryMeta.versions[prevVersion]?._npmUser?.name;
|
|
114
|
+
const currPublisher = versionData._npmUser?.name;
|
|
114
115
|
if (prevPublisher && currPublisher && prevPublisher !== currPublisher) {
|
|
116
|
+
const severity = classifyPublisherChange(prevPublisher, currPublisher);
|
|
115
117
|
findings.push({
|
|
116
|
-
type:
|
|
118
|
+
type: severity,
|
|
117
119
|
package: name,
|
|
118
120
|
version: meta.version,
|
|
119
121
|
message: `Publisher changed from "${prevPublisher}" to "${currPublisher}" in this version`,
|