dw-kit 1.3.4 → 1.3.5
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/.claude/hooks/supply-chain-scan.sh +102 -0
- package/.claude/settings.json +4 -0
- package/.dw/security/ioc-namespaces.json +40 -0
- package/CLAUDE.md +3 -1
- package/README.md +14 -2
- package/package.json +2 -1
- package/src/cli.mjs +27 -0
- package/src/commands/doctor.mjs +21 -0
- package/src/commands/init.mjs +45 -2
- package/src/commands/security-scan.mjs +427 -0
- package/src/commands/upgrade.mjs +54 -0
- package/src/lib/gitignore.mjs +86 -0
- package/src/lib/sc-install.mjs +93 -0
- package/src/lib/sc-scanner.mjs +272 -0
- package/src/lib/sc-sync.mjs +198 -0
- package/src/lib/telemetry.mjs +7 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const LOCKFILE_NAMES = ['package-lock.json', 'npm-shrinkwrap.json'];
|
|
5
|
+
|
|
6
|
+
export function findLockfile(rootDir = process.cwd()) {
|
|
7
|
+
for (const name of LOCKFILE_NAMES) {
|
|
8
|
+
const p = join(rootDir, name);
|
|
9
|
+
if (existsSync(p)) return p;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parsePackageLockfile(filepath) {
|
|
15
|
+
const raw = readFileSync(filepath, 'utf-8');
|
|
16
|
+
const data = JSON.parse(raw);
|
|
17
|
+
|
|
18
|
+
const out = new Map();
|
|
19
|
+
|
|
20
|
+
if (data.packages && typeof data.packages === 'object') {
|
|
21
|
+
for (const [key, info] of Object.entries(data.packages)) {
|
|
22
|
+
if (!key || key === '') continue;
|
|
23
|
+
if (!info || !info.version) continue;
|
|
24
|
+
|
|
25
|
+
const name = info.name || key.replace(/^node_modules\//, '').replace(/.*\/node_modules\//, '');
|
|
26
|
+
if (!name) continue;
|
|
27
|
+
if (!out.has(name)) out.set(name, info.version);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (data.dependencies && typeof data.dependencies === 'object') {
|
|
33
|
+
walkDeps(data.dependencies, out);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function walkDeps(deps, out) {
|
|
40
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
41
|
+
if (info && info.version && !out.has(name)) {
|
|
42
|
+
out.set(name, info.version);
|
|
43
|
+
}
|
|
44
|
+
if (info && info.dependencies) walkDeps(info.dependencies, out);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function compareVersions(a, b) {
|
|
49
|
+
const norm = (v) => String(v).split('-')[0].split('+')[0].split('.').map((s) => parseInt(s, 10) || 0);
|
|
50
|
+
const pa = norm(a);
|
|
51
|
+
const pb = norm(b);
|
|
52
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
53
|
+
const x = pa[i] || 0;
|
|
54
|
+
const y = pb[i] || 0;
|
|
55
|
+
if (x !== y) return x - y;
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function versionInRange(version, range) {
|
|
61
|
+
if (!range || !range.events || !Array.isArray(range.events)) return false;
|
|
62
|
+
|
|
63
|
+
let affected = false;
|
|
64
|
+
let lastIntroduced = null;
|
|
65
|
+
let lastFixed = null;
|
|
66
|
+
|
|
67
|
+
const events = range.events.slice().sort((e1, e2) => {
|
|
68
|
+
const v1 = e1.introduced || e1.fixed || e1.last_affected || '0.0.0';
|
|
69
|
+
const v2 = e2.introduced || e2.fixed || e2.last_affected || '0.0.0';
|
|
70
|
+
return compareVersions(v1, v2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
for (const evt of events) {
|
|
74
|
+
if (evt.introduced !== undefined) lastIntroduced = evt.introduced;
|
|
75
|
+
if (evt.fixed !== undefined) lastFixed = evt.fixed;
|
|
76
|
+
if (evt.last_affected !== undefined) lastFixed = evt.last_affected;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (lastIntroduced !== null && compareVersions(version, lastIntroduced) >= 0) {
|
|
80
|
+
affected = true;
|
|
81
|
+
}
|
|
82
|
+
if (affected && lastFixed !== null && compareVersions(version, lastFixed) >= 0) {
|
|
83
|
+
if (range.type === 'SEMVER' || range.type === 'ECOSYSTEM') affected = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return affected;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function matchAdvisory(packageName, version, advisory) {
|
|
90
|
+
if (!advisory || !advisory.affected) return null;
|
|
91
|
+
|
|
92
|
+
for (const aff of advisory.affected) {
|
|
93
|
+
const affName = aff.package?.name || aff.package_name || aff.name;
|
|
94
|
+
if (affName !== packageName) continue;
|
|
95
|
+
|
|
96
|
+
if (Array.isArray(aff.versions) && aff.versions.includes(version)) {
|
|
97
|
+
return buildMatch(packageName, version, advisory, aff);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (Array.isArray(aff.ranges)) {
|
|
101
|
+
for (const range of aff.ranges) {
|
|
102
|
+
if (versionInRange(version, range)) {
|
|
103
|
+
return buildMatch(packageName, version, advisory, aff);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildMatch(packageName, version, advisory, aff) {
|
|
112
|
+
const fixVersions = [];
|
|
113
|
+
if (Array.isArray(aff.ranges)) {
|
|
114
|
+
for (const range of aff.ranges) {
|
|
115
|
+
if (Array.isArray(range.events)) {
|
|
116
|
+
for (const evt of range.events) {
|
|
117
|
+
if (evt.fixed) fixVersions.push(evt.fixed);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const severity = pickSeverity(advisory);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
package: packageName,
|
|
127
|
+
version,
|
|
128
|
+
advisory_id: advisory.id,
|
|
129
|
+
summary: advisory.summary || '',
|
|
130
|
+
severity,
|
|
131
|
+
fix_versions: fixVersions,
|
|
132
|
+
references: (advisory.references || []).map((r) => r.url || r).filter(Boolean),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function pickSeverity(advisory) {
|
|
137
|
+
if (Array.isArray(advisory.severity) && advisory.severity.length > 0) {
|
|
138
|
+
const cvss = advisory.severity.find((s) => s.type && s.type.startsWith('CVSS'));
|
|
139
|
+
if (cvss && cvss.score) return cvssToLabel(cvss.score);
|
|
140
|
+
}
|
|
141
|
+
if (advisory.database_specific?.severity) return String(advisory.database_specific.severity).toLowerCase();
|
|
142
|
+
return 'unknown';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function cvssToLabel(score) {
|
|
146
|
+
if (typeof score === 'string') {
|
|
147
|
+
const m = score.match(/\d+(\.\d+)?/);
|
|
148
|
+
if (m) score = parseFloat(m[0]);
|
|
149
|
+
}
|
|
150
|
+
if (typeof score !== 'number') return 'unknown';
|
|
151
|
+
if (score >= 9.0) return 'critical';
|
|
152
|
+
if (score >= 7.0) return 'high';
|
|
153
|
+
if (score >= 4.0) return 'medium';
|
|
154
|
+
if (score > 0) return 'low';
|
|
155
|
+
return 'unknown';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function scanProject(rootDir, snapshot) {
|
|
159
|
+
const result = {
|
|
160
|
+
lockfile: null,
|
|
161
|
+
packages_scanned: 0,
|
|
162
|
+
matches: [],
|
|
163
|
+
snapshot_meta: snapshot ? { fetched_at: snapshot.fetched_at, source: snapshot.source } : null,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const lockPath = findLockfile(rootDir);
|
|
167
|
+
if (!lockPath) {
|
|
168
|
+
result.error = 'no_lockfile';
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
result.lockfile = lockPath;
|
|
172
|
+
|
|
173
|
+
if (!snapshot || !Array.isArray(snapshot.advisories)) {
|
|
174
|
+
result.error = 'no_snapshot';
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const packages = parsePackageLockfile(lockPath);
|
|
179
|
+
result.packages_scanned = packages.size;
|
|
180
|
+
|
|
181
|
+
for (const [name, version] of packages) {
|
|
182
|
+
for (const adv of snapshot.advisories) {
|
|
183
|
+
const m = matchAdvisory(name, version, adv);
|
|
184
|
+
if (m) result.matches.push(m);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function severityRank(label) {
|
|
192
|
+
return { critical: 4, high: 3, medium: 2, low: 1, unknown: 0 }[label] || 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function worstSeverity(matches) {
|
|
196
|
+
if (!matches || matches.length === 0) return null;
|
|
197
|
+
return matches.reduce((acc, m) => (severityRank(m.severity) > severityRank(acc) ? m.severity : acc), 'unknown');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Pre-install scan helpers (package.json without lockfile) ────────────────
|
|
201
|
+
|
|
202
|
+
export function parsePackageJson(filepath) {
|
|
203
|
+
const raw = readFileSync(filepath, 'utf-8');
|
|
204
|
+
const data = JSON.parse(raw);
|
|
205
|
+
const out = new Map();
|
|
206
|
+
for (const section of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
207
|
+
if (data[section] && typeof data[section] === 'object') {
|
|
208
|
+
for (const [name, range] of Object.entries(data[section])) {
|
|
209
|
+
if (typeof range === 'string' && !out.has(name)) out.set(name, range);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function findPackageJson(rootDir = process.cwd()) {
|
|
217
|
+
const p = join(rootDir, 'package.json');
|
|
218
|
+
return existsSync(p) ? p : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function matchPackageByName(packageName, advisories) {
|
|
222
|
+
const hits = [];
|
|
223
|
+
for (const adv of advisories || []) {
|
|
224
|
+
if (!adv.affected) continue;
|
|
225
|
+
for (const aff of adv.affected) {
|
|
226
|
+
const affName = aff.package?.name || aff.package_name || aff.name;
|
|
227
|
+
if (affName !== packageName) continue;
|
|
228
|
+
|
|
229
|
+
const fixVersions = [];
|
|
230
|
+
if (Array.isArray(aff.ranges)) {
|
|
231
|
+
for (const range of aff.ranges) {
|
|
232
|
+
if (Array.isArray(range.events)) {
|
|
233
|
+
for (const evt of range.events) if (evt.fixed) fixVersions.push(evt.fixed);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
hits.push({
|
|
238
|
+
advisory_id: adv.id,
|
|
239
|
+
summary: adv.summary || '',
|
|
240
|
+
severity: pickSeverity(adv),
|
|
241
|
+
fix_versions: fixVersions,
|
|
242
|
+
references: (adv.references || []).map((r) => r.url || r).filter(Boolean),
|
|
243
|
+
});
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return hits;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function matchNamespaceFixture(packages, fixture) {
|
|
251
|
+
const now = new Date();
|
|
252
|
+
const hits = [];
|
|
253
|
+
for (const entry of fixture?.namespaces || []) {
|
|
254
|
+
if (entry.active_until && new Date(entry.active_until) < now) continue;
|
|
255
|
+
if (!entry.pattern) continue;
|
|
256
|
+
|
|
257
|
+
for (const [name] of packages) {
|
|
258
|
+
if (name.startsWith(entry.pattern) || name === entry.pattern) {
|
|
259
|
+
hits.push({
|
|
260
|
+
package: name,
|
|
261
|
+
namespace_pattern: entry.pattern,
|
|
262
|
+
reason: entry.reason,
|
|
263
|
+
advisory_url: entry.advisory,
|
|
264
|
+
active_until: entry.active_until,
|
|
265
|
+
guidance: entry.guidance,
|
|
266
|
+
severity: entry.severity || 'high',
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return hits;
|
|
272
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { parsePackageLockfile, findLockfile } from './sc-scanner.mjs';
|
|
5
|
+
|
|
6
|
+
const SECURITY_DIR = '.dw/security';
|
|
7
|
+
const SNAPSHOT_FILE = 'advisory-snapshot.json';
|
|
8
|
+
const SCHEMA_VERSION = '1.0';
|
|
9
|
+
const OSV_BATCH_ENDPOINT = 'https://api.osv.dev/v1/querybatch';
|
|
10
|
+
const STALE_DAYS_DEFAULT = 7;
|
|
11
|
+
const FETCH_TIMEOUT_MS = 15000;
|
|
12
|
+
|
|
13
|
+
export function snapshotPath(rootDir = process.cwd()) {
|
|
14
|
+
return join(rootDir, SECURITY_DIR, SNAPSHOT_FILE);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function loadSnapshot(rootDir = process.cwd()) {
|
|
18
|
+
const p = snapshotPath(rootDir);
|
|
19
|
+
if (!existsSync(p)) return null;
|
|
20
|
+
try {
|
|
21
|
+
const raw = readFileSync(p, 'utf-8');
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function snapshotAgeDays(snapshot) {
|
|
29
|
+
if (!snapshot || !snapshot.fetched_at) return Infinity;
|
|
30
|
+
const fetched = new Date(snapshot.fetched_at).getTime();
|
|
31
|
+
if (!fetched) return Infinity;
|
|
32
|
+
return (Date.now() - fetched) / (1000 * 60 * 60 * 24);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isStale(snapshot, maxDays = STALE_DAYS_DEFAULT) {
|
|
36
|
+
return snapshotAgeDays(snapshot) > maxDays;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isSchemaCompatible(snapshot) {
|
|
40
|
+
if (!snapshot || !snapshot.schema_version) return false;
|
|
41
|
+
return snapshot.schema_version === SCHEMA_VERSION;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ensureSecurityDir(rootDir) {
|
|
45
|
+
const dir = join(rootDir, SECURITY_DIR);
|
|
46
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function saveSnapshot(snapshot, rootDir = process.cwd()) {
|
|
51
|
+
ensureSecurityDir(rootDir);
|
|
52
|
+
const target = snapshotPath(rootDir);
|
|
53
|
+
|
|
54
|
+
const body = JSON.stringify(snapshot, null, 2) + '\n';
|
|
55
|
+
const sha = createHash('sha256').update(body).digest('hex');
|
|
56
|
+
snapshot.snapshot_sha = `sha256:${sha.slice(0, 16)}`;
|
|
57
|
+
|
|
58
|
+
const finalBody = JSON.stringify(snapshot, null, 2) + '\n';
|
|
59
|
+
writeFileSync(target, finalBody, 'utf-8');
|
|
60
|
+
return target;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function fetchOsvBatch(queries, { timeoutMs = FETCH_TIMEOUT_MS } = {}) {
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(OSV_BATCH_ENDPOINT, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'content-type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ queries }),
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
});
|
|
73
|
+
if (!res.ok) throw new Error(`OSV batch HTTP ${res.status}`);
|
|
74
|
+
return await res.json();
|
|
75
|
+
} finally {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function fetchOsvByName(packageName, ecosystem = 'npm', { timeoutMs = FETCH_TIMEOUT_MS } = {}) {
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch('https://api.osv.dev/v1/query', {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'content-type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({ package: { name: packageName, ecosystem } }),
|
|
88
|
+
signal: controller.signal,
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) throw new Error(`OSV name-query HTTP ${res.status}`);
|
|
91
|
+
return await res.json();
|
|
92
|
+
} finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function fetchOsvDetail(vulnId, { timeoutMs = FETCH_TIMEOUT_MS } = {}) {
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(`https://api.osv.dev/v1/vulns/${vulnId}`, { signal: controller.signal });
|
|
102
|
+
if (!res.ok) throw new Error(`OSV detail HTTP ${res.status}`);
|
|
103
|
+
return await res.json();
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function syncSnapshotForProject(rootDir = process.cwd(), { ecosystem = 'npm' } = {}) {
|
|
110
|
+
const lockPath = findLockfile(rootDir);
|
|
111
|
+
if (!lockPath) {
|
|
112
|
+
const err = new Error('No lockfile found (expected package-lock.json)');
|
|
113
|
+
err.code = 'NO_LOCKFILE';
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const packages = parsePackageLockfile(lockPath);
|
|
118
|
+
const queries = [];
|
|
119
|
+
const queryIndex = [];
|
|
120
|
+
for (const [name, version] of packages) {
|
|
121
|
+
queries.push({ package: { name, ecosystem }, version });
|
|
122
|
+
queryIndex.push({ name, version });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (queries.length === 0) {
|
|
126
|
+
const empty = buildEmptySnapshot(ecosystem);
|
|
127
|
+
saveSnapshot(empty, rootDir);
|
|
128
|
+
return { snapshot: empty, advisoryCount: 0, packageCount: 0 };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const batchResults = await fetchOsvBatch(queries);
|
|
132
|
+
const vulnIds = new Set();
|
|
133
|
+
if (batchResults && Array.isArray(batchResults.results)) {
|
|
134
|
+
for (const r of batchResults.results) {
|
|
135
|
+
if (r && Array.isArray(r.vulns)) {
|
|
136
|
+
for (const v of r.vulns) if (v.id) vulnIds.add(v.id);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const advisories = [];
|
|
142
|
+
for (const id of vulnIds) {
|
|
143
|
+
try {
|
|
144
|
+
const detail = await fetchOsvDetail(id);
|
|
145
|
+
if (detail) advisories.push(detail);
|
|
146
|
+
} catch {
|
|
147
|
+
// skip — do not fail entire sync on single detail failure
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const snapshot = {
|
|
152
|
+
schema_version: SCHEMA_VERSION,
|
|
153
|
+
fetched_at: new Date().toISOString(),
|
|
154
|
+
source: 'osv.dev',
|
|
155
|
+
ecosystem,
|
|
156
|
+
package_count: queryIndex.length,
|
|
157
|
+
advisory_count: advisories.length,
|
|
158
|
+
advisories,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
saveSnapshot(snapshot, rootDir);
|
|
162
|
+
return { snapshot, advisoryCount: advisories.length, packageCount: queryIndex.length };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildEmptySnapshot(ecosystem) {
|
|
166
|
+
return {
|
|
167
|
+
schema_version: SCHEMA_VERSION,
|
|
168
|
+
fetched_at: new Date().toISOString(),
|
|
169
|
+
source: 'osv.dev',
|
|
170
|
+
ecosystem,
|
|
171
|
+
package_count: 0,
|
|
172
|
+
advisory_count: 0,
|
|
173
|
+
advisories: [],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function snapshotInfo(rootDir = process.cwd()) {
|
|
178
|
+
const p = snapshotPath(rootDir);
|
|
179
|
+
if (!existsSync(p)) {
|
|
180
|
+
return { exists: false };
|
|
181
|
+
}
|
|
182
|
+
const stat = statSync(p);
|
|
183
|
+
const snap = loadSnapshot(rootDir);
|
|
184
|
+
return {
|
|
185
|
+
exists: true,
|
|
186
|
+
path: p,
|
|
187
|
+
mtimeMs: stat.mtimeMs,
|
|
188
|
+
fetched_at: snap?.fetched_at || null,
|
|
189
|
+
source: snap?.source || null,
|
|
190
|
+
ecosystem: snap?.ecosystem || null,
|
|
191
|
+
schema_version: snap?.schema_version || null,
|
|
192
|
+
advisory_count: snap?.advisory_count ?? (Array.isArray(snap?.advisories) ? snap.advisories.length : 0),
|
|
193
|
+
package_count: snap?.package_count ?? 0,
|
|
194
|
+
age_days: snap ? snapshotAgeDays(snap) : Infinity,
|
|
195
|
+
stale: snap ? isStale(snap) : true,
|
|
196
|
+
schema_compatible: isSchemaCompatible(snap),
|
|
197
|
+
};
|
|
198
|
+
}
|
package/src/lib/telemetry.mjs
CHANGED
|
@@ -62,11 +62,17 @@ export function summarize(events) {
|
|
|
62
62
|
const bySkill = {};
|
|
63
63
|
const byHook = {};
|
|
64
64
|
const byTask = {};
|
|
65
|
+
const bySupplyChain = { scan_run: 0, block: 0, allow: 0, sync: 0 };
|
|
65
66
|
|
|
66
67
|
for (const e of events) {
|
|
67
68
|
if (e.event === 'skill') bySkill[e.name] = (bySkill[e.name] || 0) + 1;
|
|
68
69
|
else if (e.event === 'hook') byHook[e.name] = (byHook[e.name] || 0) + 1;
|
|
69
70
|
else if (e.event === 'task') byTask[e.action || 'unknown'] = (byTask[e.action || 'unknown'] || 0) + 1;
|
|
71
|
+
else if (e.event === 'sc_guard') {
|
|
72
|
+
const action = e.action || e.name || 'scan_run';
|
|
73
|
+
if (bySupplyChain[action] === undefined) bySupplyChain[action] = 0;
|
|
74
|
+
bySupplyChain[action]++;
|
|
75
|
+
}
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
return {
|
|
@@ -74,6 +80,7 @@ export function summarize(events) {
|
|
|
74
80
|
bySkill,
|
|
75
81
|
byHook,
|
|
76
82
|
byTask,
|
|
83
|
+
bySupplyChain,
|
|
77
84
|
dateRange:
|
|
78
85
|
events.length > 0 ? { from: events[0].ts, to: events[events.length - 1].ts } : null,
|
|
79
86
|
};
|