@vltpkg/security-archive 1.0.0-rc.24 → 1.0.0-rc.25
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/dist/index.js +56 -30
- package/dist/update-expired.d.ts +32 -0
- package/dist/update-expired.js +153 -0
- package/package.json +11 -6
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
var _a;
|
|
1
2
|
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
2
4
|
import { mkdirSync } from 'node:fs';
|
|
3
5
|
import { dirname } from 'node:path';
|
|
4
6
|
import { LRUCache } from 'lru-cache';
|
|
@@ -8,6 +10,7 @@ import { asDepID, splitDepID, baseDepID } from '@vltpkg/dep-id';
|
|
|
8
10
|
import { error } from '@vltpkg/error-cause';
|
|
9
11
|
import { XDG } from '@vltpkg/xdg';
|
|
10
12
|
import { asPackageReportData } from "./types.js";
|
|
13
|
+
import { __CODE_SPLIT_SCRIPT_NAME } from "./update-expired.js";
|
|
11
14
|
export * from "./types.js";
|
|
12
15
|
const SOCKET_API_V0_URL = 'https://api.socket.dev/v0/purl?alerts=true';
|
|
13
16
|
const SOCKET_PUBLIC_API_TOKEN = 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api';
|
|
@@ -39,7 +42,6 @@ export const { version } = loadPackageJson(import.meta.filename, process.env.__V
|
|
|
39
42
|
*/
|
|
40
43
|
export class SecurityArchive extends LRUCache {
|
|
41
44
|
#expired = new Set();
|
|
42
|
-
#pUpdateExpired;
|
|
43
45
|
#path;
|
|
44
46
|
#retries;
|
|
45
47
|
#nodesByName = new Map();
|
|
@@ -53,7 +55,7 @@ export class SecurityArchive extends LRUCache {
|
|
|
53
55
|
* Creates a new security archive instance and starts the refresh process.
|
|
54
56
|
*/
|
|
55
57
|
static async start(options) {
|
|
56
|
-
const archive = new
|
|
58
|
+
const archive = new _a(options);
|
|
57
59
|
await archive.refresh(options);
|
|
58
60
|
return archive;
|
|
59
61
|
}
|
|
@@ -71,8 +73,8 @@ export class SecurityArchive extends LRUCache {
|
|
|
71
73
|
}
|
|
72
74
|
constructor(options = {}) {
|
|
73
75
|
super({
|
|
74
|
-
max:
|
|
75
|
-
ttl:
|
|
76
|
+
max: _a.defaultMax,
|
|
77
|
+
ttl: _a.defaultTtl,
|
|
76
78
|
...options,
|
|
77
79
|
allowStale: true,
|
|
78
80
|
});
|
|
@@ -141,35 +143,66 @@ export class SecurityArchive extends LRUCache {
|
|
|
141
143
|
db.prepare('DELETE FROM cache WHERE depID = ?').run(depID);
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
this.#pUpdateExpired = this.#updateExpired(db);
|
|
146
|
+
// Spawn a detached process to revalidate expired entries in the
|
|
147
|
+
// background so that cli commands (vlt ls, vlt query) don't hang
|
|
148
|
+
// waiting for stale-while-revalidate requests to complete.
|
|
149
|
+
this.#spawnUpdateExpired();
|
|
149
150
|
}
|
|
150
151
|
/**
|
|
151
|
-
*
|
|
152
|
+
* Spawns a detached child process to revalidate expired entries.
|
|
153
|
+
* The process runs independently — the main process does not wait for it.
|
|
152
154
|
*/
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
+
#spawnUpdateExpired() {
|
|
156
|
+
const expired = [];
|
|
155
157
|
for (const depID of this.#expired) {
|
|
156
158
|
const node = this.#nodesByID.get(baseDepID(depID));
|
|
157
159
|
/* c8 ignore start */
|
|
158
|
-
if (!node?.version) {
|
|
159
|
-
// skip any missing node or nodes without a version, this
|
|
160
|
+
if (!node?.version || !node.name) {
|
|
161
|
+
// skip any missing node or nodes without a version/name, this
|
|
160
162
|
// should not happen, but some optional dependencies might
|
|
161
163
|
// end up in this state when reading from the lockfile
|
|
162
164
|
continue;
|
|
163
165
|
}
|
|
164
166
|
/* c8 ignore stop */
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
expired.push({
|
|
168
|
+
depID: baseDepID(depID),
|
|
169
|
+
name: node.name,
|
|
170
|
+
version: node.version,
|
|
171
|
+
});
|
|
167
172
|
}
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
const ids = this.#loadFromNDJSON(res);
|
|
171
|
-
this.#storeNewItemsToDatabase(db, ids);
|
|
173
|
+
if (!expired.length) {
|
|
174
|
+
return;
|
|
172
175
|
}
|
|
176
|
+
const isDeno = globalThis.Deno !=
|
|
177
|
+
undefined;
|
|
178
|
+
const payload = {
|
|
179
|
+
dbPath: this.#path,
|
|
180
|
+
retries: this.#retries,
|
|
181
|
+
ttl: _a.defaultTtl,
|
|
182
|
+
expired,
|
|
183
|
+
};
|
|
184
|
+
const args = [];
|
|
185
|
+
// Deno on Windows does not support detached processes
|
|
186
|
+
/* c8 ignore next */
|
|
187
|
+
const detached = !(isDeno && process.platform === 'win32');
|
|
188
|
+
/* c8 ignore start */
|
|
189
|
+
if (isDeno) {
|
|
190
|
+
args.push('--unstable-node-globals', '--unstable-bare-node-builtins');
|
|
191
|
+
}
|
|
192
|
+
/* c8 ignore stop */
|
|
193
|
+
args.push(__CODE_SPLIT_SCRIPT_NAME);
|
|
194
|
+
const proc = spawn(process.execPath, args, {
|
|
195
|
+
detached,
|
|
196
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
197
|
+
env: { ...process.env },
|
|
198
|
+
});
|
|
199
|
+
proc.stdin.write(JSON.stringify(payload));
|
|
200
|
+
proc.stdin.end();
|
|
201
|
+
/* c8 ignore start */
|
|
202
|
+
if (detached) {
|
|
203
|
+
proc.unref();
|
|
204
|
+
}
|
|
205
|
+
/* c8 ignore stop */
|
|
173
206
|
}
|
|
174
207
|
/**
|
|
175
208
|
* Queues up all required packages that are missing from the archive.
|
|
@@ -335,26 +368,18 @@ export class SecurityArchive extends LRUCache {
|
|
|
335
368
|
this.#validateReportData();
|
|
336
369
|
}
|
|
337
370
|
finally {
|
|
338
|
-
|
|
339
|
-
// the this.#close method no longer needs to be async
|
|
340
|
-
void this.#close(db);
|
|
371
|
+
this.#close(db);
|
|
341
372
|
}
|
|
342
373
|
}
|
|
343
374
|
/**
|
|
344
375
|
* Closes the database connection and cleans up the internal state.
|
|
345
376
|
*/
|
|
346
|
-
|
|
347
|
-
// the revalidation of stale entries might not yet be completed
|
|
348
|
-
// so in case we have a pending promise for that, we need to wait
|
|
349
|
-
await this.#pUpdateExpired;
|
|
377
|
+
#close(db) {
|
|
350
378
|
// close db connection
|
|
351
379
|
db.exec('PRAGMA optimize');
|
|
352
380
|
db.close();
|
|
353
381
|
// clean up internal state
|
|
354
382
|
this.#expired.clear();
|
|
355
|
-
this.#pUpdateExpired = undefined;
|
|
356
|
-
// TODO: at this point we could spawn a deref process to
|
|
357
|
-
// remove entries with expired ttl values and vaccum the db
|
|
358
383
|
}
|
|
359
384
|
/**
|
|
360
385
|
* Outputs the current in-memory cache as a JSON object.
|
|
@@ -367,3 +392,4 @@ export class SecurityArchive extends LRUCache {
|
|
|
367
392
|
return obj;
|
|
368
393
|
}
|
|
369
394
|
}
|
|
395
|
+
_a = SecurityArchive;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DepID } from '@vltpkg/dep-id';
|
|
2
|
+
import type EventEmitter from 'node:events';
|
|
3
|
+
export declare const __CODE_SPLIT_SCRIPT_NAME: string;
|
|
4
|
+
export declare const version: string;
|
|
5
|
+
/**
|
|
6
|
+
* Serialized payload sent to the detached process via stdin.
|
|
7
|
+
*/
|
|
8
|
+
export type UpdateExpiredPayload = {
|
|
9
|
+
/** Path to the sqlite database */
|
|
10
|
+
dbPath: string;
|
|
11
|
+
/** Number of retries for fetching remote data */
|
|
12
|
+
retries: number;
|
|
13
|
+
/** TTL in ms to use for new entries */
|
|
14
|
+
ttl: number;
|
|
15
|
+
/** Expired entries to revalidate */
|
|
16
|
+
expired: ExpiredEntry[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Minimal serialized information for each expired entry.
|
|
20
|
+
* Only carries what's needed for the API call + DB write.
|
|
21
|
+
*/
|
|
22
|
+
export type ExpiredEntry = {
|
|
23
|
+
depID: DepID;
|
|
24
|
+
name: string;
|
|
25
|
+
version: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Main entrypoint for the detached process.
|
|
29
|
+
* Reads a JSON payload from stdin, fetches updated security data,
|
|
30
|
+
* and writes it back to the SQLite database.
|
|
31
|
+
*/
|
|
32
|
+
export declare const main: (input?: EventEmitter) => Promise<boolean>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import pRetry, { AbortError } from 'p-retry';
|
|
6
|
+
import { asDepID, baseDepID } from '@vltpkg/dep-id';
|
|
7
|
+
import { error } from '@vltpkg/error-cause';
|
|
8
|
+
import { loadPackageJson } from 'package-json-from-dist';
|
|
9
|
+
import { asPackageReportData } from "./types.js";
|
|
10
|
+
export const __CODE_SPLIT_SCRIPT_NAME = import.meta.filename;
|
|
11
|
+
const SOCKET_API_V0_URL = 'https://api.socket.dev/v0/purl?alerts=true';
|
|
12
|
+
const SOCKET_PUBLIC_API_TOKEN = 'sktsec_t_--RAN5U4ivauy4w37-6aoKyYPDt5ZbaT5JBVMqiwKo_api';
|
|
13
|
+
export const { version } = loadPackageJson(import.meta.filename, process.env.__VLT_INTERNAL_CLI_PACKAGE_JSON);
|
|
14
|
+
/* c8 ignore start */
|
|
15
|
+
const isMain = (path) => path === __CODE_SPLIT_SCRIPT_NAME ||
|
|
16
|
+
path === pathToFileURL(__CODE_SPLIT_SCRIPT_NAME).toString();
|
|
17
|
+
/* c8 ignore stop */
|
|
18
|
+
/**
|
|
19
|
+
* Fetches security information from the socket.dev API.
|
|
20
|
+
*/
|
|
21
|
+
const retrieveRemoteData = async (queue, retries) => {
|
|
22
|
+
return pRetry(async () => {
|
|
23
|
+
const req = await fetch(SOCKET_API_V0_URL, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Basic ${Buffer.from(`${SOCKET_PUBLIC_API_TOKEN}:`).toString('base64url')}`,
|
|
27
|
+
'User-Agent': `@vltpkg/security-archive/${version}`,
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
components: Array.from(queue),
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
if (req.status === 404) {
|
|
34
|
+
throw new AbortError('Missing API');
|
|
35
|
+
}
|
|
36
|
+
if (!req.ok || !(req.status >= 200 && req.status <= 299)) {
|
|
37
|
+
throw error('Failed to fetch security data', {
|
|
38
|
+
response: req,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const str = await req.text();
|
|
42
|
+
return str.trim() + '\n';
|
|
43
|
+
}, { retries });
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Main entrypoint for the detached process.
|
|
47
|
+
* Reads a JSON payload from stdin, fetches updated security data,
|
|
48
|
+
* and writes it back to the SQLite database.
|
|
49
|
+
*/
|
|
50
|
+
export const main = async (input = process.stdin) => {
|
|
51
|
+
const payload = await new Promise((resolve, reject) => {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let chunkLen = 0;
|
|
54
|
+
input.on('data', (chunk) => {
|
|
55
|
+
chunks.push(chunk);
|
|
56
|
+
chunkLen += chunk.length;
|
|
57
|
+
});
|
|
58
|
+
input.on('end', () => {
|
|
59
|
+
try {
|
|
60
|
+
const raw = Buffer.concat(chunks, chunkLen).toString();
|
|
61
|
+
resolve(JSON.parse(raw));
|
|
62
|
+
/* c8 ignore start */
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
reject(err);
|
|
66
|
+
}
|
|
67
|
+
/* c8 ignore stop */
|
|
68
|
+
});
|
|
69
|
+
/* c8 ignore next */
|
|
70
|
+
input.on('error', reject);
|
|
71
|
+
});
|
|
72
|
+
const { dbPath, retries, ttl, expired } = payload;
|
|
73
|
+
if (!expired.length) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
// Build purl queue from expired entries
|
|
77
|
+
const expiredQueue = new Set();
|
|
78
|
+
// Build a lookup map: name@version -> depID
|
|
79
|
+
const entryByNameVersion = new Map();
|
|
80
|
+
for (const entry of expired) {
|
|
81
|
+
const purl = `pkg:npm/${entry.name}@${entry.version}`;
|
|
82
|
+
expiredQueue.add({ purl });
|
|
83
|
+
entryByNameVersion.set(`${entry.name}@${entry.version}`, entry);
|
|
84
|
+
}
|
|
85
|
+
// Fetch updated data
|
|
86
|
+
const res = await retrieveRemoteData(expiredQueue, retries);
|
|
87
|
+
// Parse NDJSON response and collect results
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const results = [];
|
|
90
|
+
const json = res.split('}\n');
|
|
91
|
+
for (const line of json) {
|
|
92
|
+
if (!line.trim())
|
|
93
|
+
continue;
|
|
94
|
+
const data = JSON.parse(line + '}');
|
|
95
|
+
const scope = data.namespace ? `${data.namespace}/` : '';
|
|
96
|
+
const name = `${scope}${data.name}`;
|
|
97
|
+
const key = `${name}@${data.version}`;
|
|
98
|
+
const entry = entryByNameVersion.get(key);
|
|
99
|
+
if (!entry) {
|
|
100
|
+
// eslint-disable-next-line no-console
|
|
101
|
+
console.warn(`security-archive: failed to find entry for ${key} found in the response.`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Calculate average score from all score components
|
|
105
|
+
const scoreComponents = [
|
|
106
|
+
data.score.license,
|
|
107
|
+
data.score.maintenance,
|
|
108
|
+
data.score.quality,
|
|
109
|
+
data.score.supplyChain,
|
|
110
|
+
data.score.vulnerability,
|
|
111
|
+
];
|
|
112
|
+
const newAverageScore = Number((scoreComponents.reduce((sum, score) => sum + score, 0) /
|
|
113
|
+
scoreComponents.length).toFixed(2));
|
|
114
|
+
const reportData = asPackageReportData({
|
|
115
|
+
...data,
|
|
116
|
+
score: { ...data.score, overall: newAverageScore },
|
|
117
|
+
});
|
|
118
|
+
results.push({
|
|
119
|
+
depID: baseDepID(asDepID(entry.depID)),
|
|
120
|
+
report: JSON.stringify(reportData),
|
|
121
|
+
start: now,
|
|
122
|
+
ttl,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (!results.length) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
// Write to database
|
|
129
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
130
|
+
const db = new DatabaseSync(dbPath);
|
|
131
|
+
db.exec('CREATE TABLE IF NOT EXISTS cache ' +
|
|
132
|
+
'(depID TEXT PRIMARY KEY, report TEXT, ttl INTEGER, start INTEGER) ' +
|
|
133
|
+
'WITHOUT ROWID');
|
|
134
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
135
|
+
db.exec('PRAGMA synchronous = NORMAL');
|
|
136
|
+
const dbWrite = db.prepare('INSERT OR REPLACE INTO cache (depID, report, start, ttl) ' +
|
|
137
|
+
'VALUES (?, ?, ?, ?)');
|
|
138
|
+
for (const { depID, report, start, ttl: entryTtl } of results) {
|
|
139
|
+
dbWrite.run(depID, report, start, entryTtl);
|
|
140
|
+
}
|
|
141
|
+
db.exec('PRAGMA optimize');
|
|
142
|
+
db.close();
|
|
143
|
+
return true;
|
|
144
|
+
};
|
|
145
|
+
/* c8 ignore start */
|
|
146
|
+
if (isMain(process.argv[1])) {
|
|
147
|
+
process.title = 'vlt-security-archive-update';
|
|
148
|
+
const res = await main(process.stdin);
|
|
149
|
+
if (!res) {
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/* c8 ignore stop */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vltpkg/security-archive",
|
|
3
3
|
"description": "A database of security information of packages",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.25",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/vltpkg/vltpkg.git",
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"email": "support@vlt.sh"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@vltpkg/dep-id": "1.0.0-rc.
|
|
16
|
-
"@vltpkg/error-cause": "1.0.0-rc.
|
|
17
|
-
"@vltpkg/spec": "1.0.0-rc.
|
|
18
|
-
"@vltpkg/types": "1.0.0-rc.
|
|
19
|
-
"@vltpkg/xdg": "1.0.0-rc.
|
|
15
|
+
"@vltpkg/dep-id": "1.0.0-rc.25",
|
|
16
|
+
"@vltpkg/error-cause": "1.0.0-rc.25",
|
|
17
|
+
"@vltpkg/spec": "1.0.0-rc.25",
|
|
18
|
+
"@vltpkg/types": "1.0.0-rc.25",
|
|
19
|
+
"@vltpkg/xdg": "1.0.0-rc.25",
|
|
20
20
|
"lru-cache": "^11.2.4",
|
|
21
21
|
"p-retry": "^7.1.1",
|
|
22
22
|
"package-json-from-dist": "^1.0.1"
|
|
@@ -63,6 +63,11 @@
|
|
|
63
63
|
"import": {
|
|
64
64
|
"default": "./dist/browser.js"
|
|
65
65
|
}
|
|
66
|
+
},
|
|
67
|
+
"./update-expired": {
|
|
68
|
+
"import": {
|
|
69
|
+
"default": "./dist/update-expired.js"
|
|
70
|
+
}
|
|
66
71
|
}
|
|
67
72
|
},
|
|
68
73
|
"files": [
|