qa360 1.0.4 → 1.1.1
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/commands/history.js +1 -1
- package/dist/commands/pack.js +1 -1
- package/dist/commands/run.d.ts +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +1 -1
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/serve.js +1 -1
- package/dist/commands/verify.js +1 -1
- package/dist/core/adapters/gitleaks-secrets.d.ts +115 -0
- package/dist/core/adapters/gitleaks-secrets.d.ts.map +1 -0
- package/dist/core/adapters/gitleaks-secrets.js +410 -0
- package/dist/core/adapters/k6-perf.d.ts +86 -0
- package/dist/core/adapters/k6-perf.d.ts.map +1 -0
- package/dist/core/adapters/k6-perf.js +398 -0
- package/dist/core/adapters/osv-deps.d.ts +124 -0
- package/dist/core/adapters/osv-deps.d.ts.map +1 -0
- package/dist/core/adapters/osv-deps.js +372 -0
- package/dist/core/adapters/playwright-api.d.ts +82 -0
- package/dist/core/adapters/playwright-api.d.ts.map +1 -0
- package/dist/core/adapters/playwright-api.js +252 -0
- package/dist/core/adapters/playwright-ui.d.ts +115 -0
- package/dist/core/adapters/playwright-ui.d.ts.map +1 -0
- package/dist/core/adapters/playwright-ui.js +346 -0
- package/dist/core/adapters/semgrep-sast.d.ts +100 -0
- package/dist/core/adapters/semgrep-sast.d.ts.map +1 -0
- package/dist/core/adapters/semgrep-sast.js +322 -0
- package/dist/core/adapters/zap-dast.d.ts +134 -0
- package/dist/core/adapters/zap-dast.d.ts.map +1 -0
- package/dist/core/adapters/zap-dast.js +424 -0
- package/dist/core/hooks/compose.d.ts +62 -0
- package/dist/core/hooks/compose.d.ts.map +1 -0
- package/dist/core/hooks/compose.js +225 -0
- package/dist/core/hooks/runner.d.ts +69 -0
- package/dist/core/hooks/runner.d.ts.map +1 -0
- package/dist/core/hooks/runner.js +303 -0
- package/dist/core/index.d.ts +74 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +39 -0
- package/dist/core/pack/migrator.d.ts +52 -0
- package/dist/core/pack/migrator.d.ts.map +1 -0
- package/dist/core/pack/migrator.js +304 -0
- package/dist/core/pack/validator.d.ts +43 -0
- package/dist/core/pack/validator.d.ts.map +1 -0
- package/dist/core/pack/validator.js +292 -0
- package/dist/core/proof/bundle.d.ts +138 -0
- package/dist/core/proof/bundle.d.ts.map +1 -0
- package/dist/core/proof/bundle.js +160 -0
- package/dist/core/proof/canonicalize.d.ts +48 -0
- package/dist/core/proof/canonicalize.d.ts.map +1 -0
- package/dist/core/proof/canonicalize.js +105 -0
- package/dist/core/proof/index.d.ts +14 -0
- package/dist/core/proof/index.d.ts.map +1 -0
- package/dist/core/proof/index.js +18 -0
- package/dist/core/proof/schema.d.ts +218 -0
- package/dist/core/proof/schema.d.ts.map +1 -0
- package/dist/core/proof/schema.js +263 -0
- package/dist/core/proof/signer.d.ts +112 -0
- package/dist/core/proof/signer.d.ts.map +1 -0
- package/dist/core/proof/signer.js +226 -0
- package/dist/core/proof/verifier.d.ts +98 -0
- package/dist/core/proof/verifier.d.ts.map +1 -0
- package/dist/core/proof/verifier.js +302 -0
- package/dist/core/runner/phase3-runner.d.ts +102 -0
- package/dist/core/runner/phase3-runner.d.ts.map +1 -0
- package/dist/core/runner/phase3-runner.js +471 -0
- package/dist/core/secrets/crypto.d.ts +76 -0
- package/dist/core/secrets/crypto.d.ts.map +1 -0
- package/dist/core/secrets/crypto.js +225 -0
- package/dist/core/secrets/manager.d.ts +77 -0
- package/dist/core/secrets/manager.d.ts.map +1 -0
- package/dist/core/secrets/manager.js +219 -0
- package/dist/core/security/redaction-patterns-extended.d.ts +28 -0
- package/dist/core/security/redaction-patterns-extended.d.ts.map +1 -0
- package/dist/core/security/redaction-patterns-extended.js +247 -0
- package/dist/core/security/redactor.d.ts +72 -0
- package/dist/core/security/redactor.d.ts.map +1 -0
- package/dist/core/security/redactor.js +279 -0
- package/dist/core/serve/diagnostics-collector.d.ts +33 -0
- package/dist/core/serve/diagnostics-collector.d.ts.map +1 -0
- package/dist/core/serve/diagnostics-collector.js +149 -0
- package/dist/core/serve/health-checker.d.ts +45 -0
- package/dist/core/serve/health-checker.d.ts.map +1 -0
- package/dist/core/serve/health-checker.js +219 -0
- package/dist/core/serve/index.d.ts +9 -0
- package/dist/core/serve/index.d.ts.map +1 -0
- package/dist/core/serve/index.js +8 -0
- package/dist/core/serve/metrics-collector.d.ts +25 -0
- package/dist/core/serve/metrics-collector.d.ts.map +1 -0
- package/dist/core/serve/metrics-collector.js +322 -0
- package/dist/core/serve/process-manager.d.ts +37 -0
- package/dist/core/serve/process-manager.d.ts.map +1 -0
- package/dist/core/serve/process-manager.js +213 -0
- package/dist/core/serve/server.d.ts +37 -0
- package/dist/core/serve/server.d.ts.map +1 -0
- package/dist/core/serve/server.js +191 -0
- package/dist/core/types/pack-v1.d.ts +162 -0
- package/dist/core/types/pack-v1.d.ts.map +1 -0
- package/dist/core/types/pack-v1.js +5 -0
- package/dist/core/types/trust-score.d.ts +70 -0
- package/dist/core/types/trust-score.d.ts.map +1 -0
- package/dist/core/types/trust-score.js +191 -0
- package/dist/core/vault/cas.d.ts +87 -0
- package/dist/core/vault/cas.d.ts.map +1 -0
- package/dist/core/vault/cas.js +255 -0
- package/dist/core/vault/index.d.ts +205 -0
- package/dist/core/vault/index.d.ts.map +1 -0
- package/dist/core/vault/index.js +631 -0
- package/package.json +13 -6
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Evidence Vault Engine
|
|
3
|
+
* SQLite WAL-based storage with idempotence and CAS integration
|
|
4
|
+
*/
|
|
5
|
+
import sqlite3pkg from 'sqlite3';
|
|
6
|
+
const { Database } = sqlite3pkg;
|
|
7
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join, resolve } from 'path';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
import { ContentAddressableStorage } from './cas.js';
|
|
12
|
+
import { SecurityRedactor } from '../security/redactor.js';
|
|
13
|
+
export class EvidenceVault {
|
|
14
|
+
db;
|
|
15
|
+
cas;
|
|
16
|
+
redactor;
|
|
17
|
+
dbPath;
|
|
18
|
+
baseDir;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.baseDir = resolve(config.baseDir);
|
|
21
|
+
this.dbPath = join(this.baseDir, 'vault.db');
|
|
22
|
+
this.cas = new ContentAddressableStorage(join(this.baseDir, 'runs'));
|
|
23
|
+
this.redactor = SecurityRedactor.forReports();
|
|
24
|
+
this.ensureDirectories();
|
|
25
|
+
this.initializeDatabase(config);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Open vault connection and initialize schema
|
|
29
|
+
*/
|
|
30
|
+
static async open(baseDir, config) {
|
|
31
|
+
const fullConfig = {
|
|
32
|
+
baseDir,
|
|
33
|
+
enableWAL: true,
|
|
34
|
+
maxConnections: 10,
|
|
35
|
+
retentionDays: 90,
|
|
36
|
+
...config
|
|
37
|
+
};
|
|
38
|
+
const vault = new EvidenceVault(fullConfig);
|
|
39
|
+
await vault.initializeSchema();
|
|
40
|
+
return vault;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Begin a new run with idempotence support
|
|
44
|
+
*/
|
|
45
|
+
async beginRun(options) {
|
|
46
|
+
// Check for existing run with same run_key
|
|
47
|
+
if (options.run_key) {
|
|
48
|
+
const existing = await this.getRunByKey(options.run_key);
|
|
49
|
+
if (existing) {
|
|
50
|
+
console.log(`[VAULT] RUN_IDEMPOTENT_REUSED: ${existing.id} (key: ${options.run_key})`);
|
|
51
|
+
return { runId: existing.id, isReused: true };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const runId = randomUUID();
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const run = {
|
|
57
|
+
id: runId,
|
|
58
|
+
run_key: options.run_key,
|
|
59
|
+
started_at: now,
|
|
60
|
+
pack_hash: options.pack_hash,
|
|
61
|
+
pack_path: options.pack_path,
|
|
62
|
+
status: 'running',
|
|
63
|
+
weights_json: options.weights ? JSON.stringify(options.weights) : undefined,
|
|
64
|
+
pinned: 0,
|
|
65
|
+
created_at: now,
|
|
66
|
+
updated_at: now
|
|
67
|
+
};
|
|
68
|
+
await this.insertRun(run);
|
|
69
|
+
console.log(`[VAULT] RUN_STARTED: ${runId} (key: ${options.run_key || 'none'})`);
|
|
70
|
+
return { runId, isReused: false };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Finish a run with final status and signature
|
|
74
|
+
*/
|
|
75
|
+
async finishRun(runId, options) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const updates = {
|
|
78
|
+
ended_at: now,
|
|
79
|
+
status: options.status,
|
|
80
|
+
trust_score: options.trust_score,
|
|
81
|
+
signature_hex: options.signature,
|
|
82
|
+
updated_at: now
|
|
83
|
+
};
|
|
84
|
+
if (options.weights) {
|
|
85
|
+
updates.weights_json = JSON.stringify(options.weights);
|
|
86
|
+
}
|
|
87
|
+
if (options.proof_pdf_sha) {
|
|
88
|
+
const artifact = await this.getArtifact(options.proof_pdf_sha);
|
|
89
|
+
if (artifact) {
|
|
90
|
+
updates.proof_pdf_path = artifact.cas_path;
|
|
91
|
+
await this.attachArtifact(runId, { sha256: options.proof_pdf_sha, label: 'proof_pdf' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
await this.updateRun(runId, updates);
|
|
95
|
+
console.log(`[VAULT] RUN_FINISHED: ${runId} (status: ${options.status}, trust: ${options.trust_score})`);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Record gate execution result
|
|
99
|
+
*/
|
|
100
|
+
async recordGate(runId, gate) {
|
|
101
|
+
const gateRecord = {
|
|
102
|
+
run_id: runId,
|
|
103
|
+
created_at: Date.now(),
|
|
104
|
+
...gate
|
|
105
|
+
};
|
|
106
|
+
const gateId = await this.insertGate(gateRecord);
|
|
107
|
+
console.log(`[VAULT] GATE_RECORDED: ${runId}/${gate.name} (status: ${gate.status}, duration: ${gate.duration_ms}ms)`);
|
|
108
|
+
return gateId;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Record security/quality finding
|
|
112
|
+
*/
|
|
113
|
+
async recordFinding(runId, finding) {
|
|
114
|
+
// Redact sensitive information
|
|
115
|
+
const redactedFinding = {
|
|
116
|
+
run_id: runId,
|
|
117
|
+
created_at: Date.now(),
|
|
118
|
+
...finding,
|
|
119
|
+
message: this.redactor.redact(finding.message),
|
|
120
|
+
raw_output: finding.raw_output ? this.redactor.redact(finding.raw_output) : undefined,
|
|
121
|
+
fingerprint: finding.fingerprint || this.generateFindingFingerprint(finding)
|
|
122
|
+
};
|
|
123
|
+
const findingId = await this.insertFinding(redactedFinding);
|
|
124
|
+
console.log(`[VAULT] FINDING_RECORDED: ${runId}/${finding.gate} (severity: ${finding.severity}, rule: ${finding.rule})`);
|
|
125
|
+
return findingId;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Store artifact in CAS and link to run
|
|
129
|
+
*/
|
|
130
|
+
async storeArtifact(runId, content, mimeType, label, originalName) {
|
|
131
|
+
const casArtifact = await this.cas.saveArtifact(content, mimeType, originalName);
|
|
132
|
+
// Store artifact metadata
|
|
133
|
+
const artifact = {
|
|
134
|
+
sha256: casArtifact.sha256,
|
|
135
|
+
mime_type: mimeType,
|
|
136
|
+
size_bytes: casArtifact.size,
|
|
137
|
+
cas_path: casArtifact.path,
|
|
138
|
+
original_name: originalName,
|
|
139
|
+
created_at: Date.now(),
|
|
140
|
+
last_accessed: Date.now()
|
|
141
|
+
};
|
|
142
|
+
await this.insertArtifact(artifact);
|
|
143
|
+
await this.attachArtifact(runId, { sha256: casArtifact.sha256, label });
|
|
144
|
+
console.log(`[VAULT] ARTIFACT_STORED: ${runId}/${label} (sha256: ${casArtifact.sha256.substring(0, 8)}..., size: ${casArtifact.size})`);
|
|
145
|
+
return casArtifact;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Attach existing artifact to run
|
|
149
|
+
*/
|
|
150
|
+
async attachArtifact(runId, link) {
|
|
151
|
+
const linkRecord = {
|
|
152
|
+
run_id: runId,
|
|
153
|
+
sha256: link.sha256,
|
|
154
|
+
label: link.label,
|
|
155
|
+
created_at: Date.now()
|
|
156
|
+
};
|
|
157
|
+
await this.insertRunArtifact(linkRecord);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get run by ID
|
|
161
|
+
*/
|
|
162
|
+
async getRun(runId) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
this.db.get('SELECT * FROM runs WHERE id = ?', [runId], (err, row) => {
|
|
165
|
+
if (err)
|
|
166
|
+
reject(err);
|
|
167
|
+
else
|
|
168
|
+
resolve(row || null);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get run by run_key
|
|
174
|
+
*/
|
|
175
|
+
async getRunByKey(runKey) {
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
this.db.get('SELECT * FROM runs WHERE run_key = ?', [runKey], (err, row) => {
|
|
178
|
+
if (err)
|
|
179
|
+
reject(err);
|
|
180
|
+
else
|
|
181
|
+
resolve(row || null);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* List runs with filters
|
|
187
|
+
*/
|
|
188
|
+
async listRuns(filters) {
|
|
189
|
+
let query = 'SELECT * FROM runs WHERE 1=1';
|
|
190
|
+
const params = [];
|
|
191
|
+
if (filters?.status?.length) {
|
|
192
|
+
query += ` AND status IN (${filters.status.map(() => '?').join(',')})`;
|
|
193
|
+
params.push(...filters.status);
|
|
194
|
+
}
|
|
195
|
+
if (filters?.since) {
|
|
196
|
+
query += ' AND started_at >= ?';
|
|
197
|
+
params.push(filters.since.getTime());
|
|
198
|
+
}
|
|
199
|
+
if (filters?.until) {
|
|
200
|
+
query += ' AND started_at <= ?';
|
|
201
|
+
params.push(filters.until.getTime());
|
|
202
|
+
}
|
|
203
|
+
if (filters?.pack_hash) {
|
|
204
|
+
query += ' AND pack_hash = ?';
|
|
205
|
+
params.push(filters.pack_hash);
|
|
206
|
+
}
|
|
207
|
+
query += ' ORDER BY started_at DESC';
|
|
208
|
+
if (filters?.limit) {
|
|
209
|
+
query += ' LIMIT ?';
|
|
210
|
+
params.push(filters.limit);
|
|
211
|
+
if (filters?.offset) {
|
|
212
|
+
query += ' OFFSET ?';
|
|
213
|
+
params.push(filters.offset);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
this.db.all(query, params, (err, rows) => {
|
|
218
|
+
if (err)
|
|
219
|
+
reject(err);
|
|
220
|
+
else
|
|
221
|
+
resolve(rows);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get gates for a run
|
|
227
|
+
*/
|
|
228
|
+
async getGates(runId) {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
this.db.all('SELECT * FROM gates WHERE run_id = ? ORDER BY created_at', [runId], (err, rows) => {
|
|
231
|
+
if (err)
|
|
232
|
+
reject(err);
|
|
233
|
+
else
|
|
234
|
+
resolve(rows);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Get findings for a run
|
|
240
|
+
*/
|
|
241
|
+
async getFindings(runId, gate) {
|
|
242
|
+
let query = 'SELECT * FROM findings WHERE run_id = ?';
|
|
243
|
+
const params = [runId];
|
|
244
|
+
if (gate) {
|
|
245
|
+
query += ' AND gate = ?';
|
|
246
|
+
params.push(gate);
|
|
247
|
+
}
|
|
248
|
+
query += ' ORDER BY severity DESC, created_at';
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
this.db.all(query, params, (err, rows) => {
|
|
251
|
+
if (err)
|
|
252
|
+
reject(err);
|
|
253
|
+
else
|
|
254
|
+
resolve(rows);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Get artifacts for a run
|
|
260
|
+
*/
|
|
261
|
+
async getRunArtifacts(runId) {
|
|
262
|
+
return new Promise((resolve, reject) => {
|
|
263
|
+
this.db.all(`
|
|
264
|
+
SELECT a.*, ra.label
|
|
265
|
+
FROM artifacts a
|
|
266
|
+
JOIN run_artifacts ra ON a.sha256 = ra.sha256
|
|
267
|
+
WHERE ra.run_id = ?
|
|
268
|
+
ORDER BY ra.created_at
|
|
269
|
+
`, [runId], (err, rows) => {
|
|
270
|
+
if (err)
|
|
271
|
+
reject(err);
|
|
272
|
+
else
|
|
273
|
+
resolve(rows);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Get artifact by SHA256
|
|
279
|
+
*/
|
|
280
|
+
async getArtifact(sha256) {
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
this.db.get('SELECT * FROM artifacts WHERE sha256 = ?', [sha256], (err, row) => {
|
|
283
|
+
if (err)
|
|
284
|
+
reject(err);
|
|
285
|
+
else
|
|
286
|
+
resolve(row || null);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Pin/unpin run to protect from GC
|
|
292
|
+
*/
|
|
293
|
+
async pinRun(runId, pinned = true) {
|
|
294
|
+
await this.updateRun(runId, { pinned: pinned ? 1 : 0 });
|
|
295
|
+
console.log(`[VAULT] RUN_${pinned ? 'PINNED' : 'UNPINNED'}: ${runId}`);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Calculate pack hash for idempotence
|
|
299
|
+
*/
|
|
300
|
+
static calculatePackHash(packContent) {
|
|
301
|
+
// Normalize pack content (remove comments, sort keys, etc.)
|
|
302
|
+
const normalized = packContent
|
|
303
|
+
.split('\n')
|
|
304
|
+
.filter(line => !line.trim().startsWith('#'))
|
|
305
|
+
.join('\n')
|
|
306
|
+
.trim();
|
|
307
|
+
return createHash('sha256').update(normalized).digest('hex');
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Get vault statistics
|
|
311
|
+
*/
|
|
312
|
+
async getStats() {
|
|
313
|
+
const queries = [
|
|
314
|
+
'SELECT COUNT(*) as count FROM runs',
|
|
315
|
+
'SELECT COUNT(*) as count FROM gates',
|
|
316
|
+
'SELECT COUNT(*) as count FROM findings',
|
|
317
|
+
'SELECT COUNT(*) as count FROM artifacts',
|
|
318
|
+
'SELECT MIN(started_at) as oldest, MAX(started_at) as newest FROM runs'
|
|
319
|
+
];
|
|
320
|
+
const results = await Promise.all(queries.map(query => new Promise((resolve, reject) => {
|
|
321
|
+
this.db.get(query, (err, row) => {
|
|
322
|
+
if (err)
|
|
323
|
+
reject(err);
|
|
324
|
+
else
|
|
325
|
+
resolve(row);
|
|
326
|
+
});
|
|
327
|
+
})));
|
|
328
|
+
const casStats = await this.cas.getStats();
|
|
329
|
+
return {
|
|
330
|
+
totalRuns: results[0].count,
|
|
331
|
+
totalGates: results[1].count,
|
|
332
|
+
totalFindings: results[2].count,
|
|
333
|
+
totalArtifacts: results[3].count,
|
|
334
|
+
vaultSizeBytes: casStats.totalBytes,
|
|
335
|
+
oldestRun: results[4].oldest ? new Date(results[4].oldest) : undefined,
|
|
336
|
+
newestRun: results[4].newest ? new Date(results[4].newest) : undefined
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
// Private helper methods
|
|
340
|
+
async insertRun(run) {
|
|
341
|
+
const fields = Object.keys(run).join(', ');
|
|
342
|
+
const placeholders = Object.keys(run).map(() => '?').join(', ');
|
|
343
|
+
const values = Object.values(run);
|
|
344
|
+
return new Promise((resolve, reject) => {
|
|
345
|
+
this.db.run(`INSERT INTO runs (${fields}) VALUES (${placeholders})`, values, (err) => {
|
|
346
|
+
if (err)
|
|
347
|
+
reject(err);
|
|
348
|
+
else
|
|
349
|
+
resolve();
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async updateRun(runId, updates) {
|
|
354
|
+
const fields = Object.keys(updates).map(key => `${key} = ?`).join(', ');
|
|
355
|
+
const values = [...Object.values(updates), runId];
|
|
356
|
+
return new Promise((resolve, reject) => {
|
|
357
|
+
this.db.run(`UPDATE runs SET ${fields} WHERE id = ?`, values, (err) => {
|
|
358
|
+
if (err)
|
|
359
|
+
reject(err);
|
|
360
|
+
else
|
|
361
|
+
resolve();
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
async insertGate(gate) {
|
|
366
|
+
const fields = Object.keys(gate).join(', ');
|
|
367
|
+
const placeholders = Object.keys(gate).map(() => '?').join(', ');
|
|
368
|
+
const values = Object.values(gate);
|
|
369
|
+
return new Promise((resolve, reject) => {
|
|
370
|
+
this.db.run(`INSERT INTO gates (${fields}) VALUES (${placeholders})`, values, function (err) {
|
|
371
|
+
if (err)
|
|
372
|
+
reject(err);
|
|
373
|
+
else
|
|
374
|
+
resolve(this.lastID);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async insertFinding(finding) {
|
|
379
|
+
const fields = Object.keys(finding).join(', ');
|
|
380
|
+
const placeholders = Object.keys(finding).map(() => '?').join(', ');
|
|
381
|
+
const values = Object.values(finding);
|
|
382
|
+
return new Promise((resolve, reject) => {
|
|
383
|
+
this.db.run(`INSERT INTO findings (${fields}) VALUES (${placeholders})`, values, function (err) {
|
|
384
|
+
if (err)
|
|
385
|
+
reject(err);
|
|
386
|
+
else
|
|
387
|
+
resolve(this.lastID);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
async insertArtifact(artifact) {
|
|
392
|
+
return new Promise((resolve, reject) => {
|
|
393
|
+
this.db.run(`INSERT OR IGNORE INTO artifacts (sha256, mime_type, size_bytes, cas_path, original_name, created_at, last_accessed)
|
|
394
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
395
|
+
artifact.sha256,
|
|
396
|
+
artifact.mime_type,
|
|
397
|
+
artifact.size_bytes,
|
|
398
|
+
artifact.cas_path,
|
|
399
|
+
artifact.original_name,
|
|
400
|
+
artifact.created_at,
|
|
401
|
+
artifact.last_accessed
|
|
402
|
+
], (err) => {
|
|
403
|
+
if (err)
|
|
404
|
+
reject(err);
|
|
405
|
+
else
|
|
406
|
+
resolve();
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
async insertRunArtifact(link) {
|
|
411
|
+
return new Promise((resolve, reject) => {
|
|
412
|
+
this.db.run('INSERT OR IGNORE INTO run_artifacts (run_id, sha256, label, created_at) VALUES (?, ?, ?, ?)', [link.run_id, link.sha256, link.label, link.created_at], (err) => {
|
|
413
|
+
if (err)
|
|
414
|
+
reject(err);
|
|
415
|
+
else
|
|
416
|
+
resolve();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
generateFindingFingerprint(finding) {
|
|
421
|
+
const content = `${finding.gate}:${finding.rule}:${finding.location || ''}:${finding.message}`;
|
|
422
|
+
return createHash('sha256').update(content).digest('hex').substring(0, 16);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Initialize database connection
|
|
426
|
+
*/
|
|
427
|
+
initializeDatabase(config) {
|
|
428
|
+
this.db = new Database(this.dbPath);
|
|
429
|
+
if (config.enableWAL) {
|
|
430
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
431
|
+
this.db.exec('PRAGMA synchronous = NORMAL');
|
|
432
|
+
}
|
|
433
|
+
this.db.exec('PRAGMA foreign_keys = ON');
|
|
434
|
+
this.db.exec('PRAGMA cache_size = 10000');
|
|
435
|
+
this.db.exec('PRAGMA temp_store = MEMORY');
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Initialize database schema
|
|
439
|
+
*/
|
|
440
|
+
async initializeSchema() {
|
|
441
|
+
const schema = `
|
|
442
|
+
-- QA360 Evidence Vault Schema
|
|
443
|
+
-- SQLite WAL mode for concurrent access and performance
|
|
444
|
+
|
|
445
|
+
-- Runs table: core execution records
|
|
446
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
447
|
+
id TEXT PRIMARY KEY,
|
|
448
|
+
run_key TEXT UNIQUE,
|
|
449
|
+
started_at INTEGER NOT NULL,
|
|
450
|
+
ended_at INTEGER,
|
|
451
|
+
pack_hash TEXT NOT NULL,
|
|
452
|
+
pack_path TEXT NOT NULL,
|
|
453
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
454
|
+
trust_score INTEGER,
|
|
455
|
+
weights_json TEXT,
|
|
456
|
+
sign_alg TEXT DEFAULT 'ed25519',
|
|
457
|
+
signature_hex TEXT,
|
|
458
|
+
proof_pdf_path TEXT,
|
|
459
|
+
pinned INTEGER DEFAULT 0,
|
|
460
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
461
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
-- Gates table: individual test gate results
|
|
465
|
+
CREATE TABLE IF NOT EXISTS gates (
|
|
466
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
467
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
468
|
+
name TEXT NOT NULL,
|
|
469
|
+
status TEXT NOT NULL,
|
|
470
|
+
duration_ms INTEGER,
|
|
471
|
+
metrics_json TEXT,
|
|
472
|
+
budgets_json TEXT,
|
|
473
|
+
started_at INTEGER,
|
|
474
|
+
ended_at INTEGER,
|
|
475
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
-- Findings table: security and quality findings
|
|
479
|
+
CREATE TABLE IF NOT EXISTS findings (
|
|
480
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
481
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
482
|
+
gate TEXT NOT NULL,
|
|
483
|
+
severity TEXT NOT NULL,
|
|
484
|
+
rule TEXT NOT NULL,
|
|
485
|
+
location TEXT,
|
|
486
|
+
message TEXT,
|
|
487
|
+
raw_output TEXT,
|
|
488
|
+
fingerprint TEXT,
|
|
489
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
-- Artifacts table: Content-Addressable Storage metadata
|
|
493
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
494
|
+
sha256 TEXT PRIMARY KEY,
|
|
495
|
+
mime_type TEXT NOT NULL,
|
|
496
|
+
size_bytes INTEGER NOT NULL,
|
|
497
|
+
cas_path TEXT NOT NULL,
|
|
498
|
+
original_name TEXT,
|
|
499
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
500
|
+
last_accessed INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
-- Run artifacts table: N-N relationship between runs and artifacts
|
|
504
|
+
CREATE TABLE IF NOT EXISTS run_artifacts (
|
|
505
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
506
|
+
sha256 TEXT NOT NULL REFERENCES artifacts(sha256) ON DELETE CASCADE,
|
|
507
|
+
label TEXT NOT NULL,
|
|
508
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
509
|
+
PRIMARY KEY (run_id, sha256, label)
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
-- Vault metadata table: schema version and configuration
|
|
513
|
+
CREATE TABLE IF NOT EXISTS vault_metadata (
|
|
514
|
+
key TEXT PRIMARY KEY,
|
|
515
|
+
value TEXT NOT NULL,
|
|
516
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
-- Indexes for performance
|
|
520
|
+
CREATE INDEX IF NOT EXISTS idx_runs_started_at ON runs(started_at DESC);
|
|
521
|
+
CREATE INDEX IF NOT EXISTS idx_runs_run_key ON runs(run_key) WHERE run_key IS NOT NULL;
|
|
522
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
523
|
+
CREATE INDEX IF NOT EXISTS idx_runs_pack_hash ON runs(pack_hash);
|
|
524
|
+
CREATE INDEX IF NOT EXISTS idx_runs_pinned ON runs(pinned) WHERE pinned = 1;
|
|
525
|
+
|
|
526
|
+
CREATE INDEX IF NOT EXISTS idx_gates_run_id ON gates(run_id);
|
|
527
|
+
CREATE INDEX IF NOT EXISTS idx_gates_run_name ON gates(run_id, name);
|
|
528
|
+
CREATE INDEX IF NOT EXISTS idx_gates_status ON gates(status);
|
|
529
|
+
CREATE INDEX IF NOT EXISTS idx_gates_name ON gates(name);
|
|
530
|
+
|
|
531
|
+
CREATE INDEX IF NOT EXISTS idx_findings_run_id ON findings(run_id);
|
|
532
|
+
CREATE INDEX IF NOT EXISTS idx_findings_run_gate ON findings(run_id, gate);
|
|
533
|
+
CREATE INDEX IF NOT EXISTS idx_findings_severity ON findings(severity);
|
|
534
|
+
CREATE INDEX IF NOT EXISTS idx_findings_rule ON findings(rule);
|
|
535
|
+
CREATE INDEX IF NOT EXISTS idx_findings_fingerprint ON findings(fingerprint);
|
|
536
|
+
|
|
537
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_mime_type ON artifacts(mime_type);
|
|
538
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_size ON artifacts(size_bytes);
|
|
539
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_created_at ON artifacts(created_at);
|
|
540
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_last_accessed ON artifacts(last_accessed);
|
|
541
|
+
|
|
542
|
+
CREATE INDEX IF NOT EXISTS idx_run_artifacts_run_id ON run_artifacts(run_id);
|
|
543
|
+
CREATE INDEX IF NOT EXISTS idx_run_artifacts_sha256 ON run_artifacts(sha256);
|
|
544
|
+
CREATE INDEX IF NOT EXISTS idx_run_artifacts_label ON run_artifacts(label);
|
|
545
|
+
|
|
546
|
+
-- Insert initial metadata
|
|
547
|
+
INSERT OR IGNORE INTO vault_metadata (key, value) VALUES
|
|
548
|
+
('schema_version', '1.0.0'),
|
|
549
|
+
('created_at', strftime('%s', 'now') * 1000),
|
|
550
|
+
('vault_format', 'qa360-evidence-vault-v1');
|
|
551
|
+
|
|
552
|
+
-- Triggers for updated_at timestamps
|
|
553
|
+
CREATE TRIGGER IF NOT EXISTS trigger_runs_updated_at
|
|
554
|
+
AFTER UPDATE ON runs
|
|
555
|
+
BEGIN
|
|
556
|
+
UPDATE runs SET updated_at = strftime('%s', 'now') * 1000 WHERE id = NEW.id;
|
|
557
|
+
END;
|
|
558
|
+
|
|
559
|
+
CREATE TRIGGER IF NOT EXISTS trigger_artifacts_last_accessed
|
|
560
|
+
AFTER UPDATE ON artifacts
|
|
561
|
+
BEGIN
|
|
562
|
+
UPDATE artifacts SET last_accessed = strftime('%s', 'now') * 1000 WHERE sha256 = NEW.sha256;
|
|
563
|
+
END;
|
|
564
|
+
|
|
565
|
+
-- Views for common queries
|
|
566
|
+
CREATE VIEW IF NOT EXISTS v_recent_runs AS
|
|
567
|
+
SELECT
|
|
568
|
+
r.id,
|
|
569
|
+
r.run_key,
|
|
570
|
+
r.started_at,
|
|
571
|
+
r.ended_at,
|
|
572
|
+
r.status,
|
|
573
|
+
r.trust_score,
|
|
574
|
+
r.pack_path,
|
|
575
|
+
COUNT(g.id) as gate_count,
|
|
576
|
+
COUNT(CASE WHEN g.status = 'passed' THEN 1 END) as gates_passed,
|
|
577
|
+
COUNT(CASE WHEN g.status = 'failed' THEN 1 END) as gates_failed,
|
|
578
|
+
COUNT(f.id) as finding_count,
|
|
579
|
+
COUNT(CASE WHEN f.severity IN ('high', 'critical') THEN 1 END) as critical_findings
|
|
580
|
+
FROM runs r
|
|
581
|
+
LEFT JOIN gates g ON r.id = g.run_id
|
|
582
|
+
LEFT JOIN findings f ON r.id = f.run_id
|
|
583
|
+
GROUP BY r.id
|
|
584
|
+
ORDER BY r.started_at DESC;
|
|
585
|
+
|
|
586
|
+
CREATE VIEW IF NOT EXISTS v_gate_trends AS
|
|
587
|
+
SELECT
|
|
588
|
+
g.name,
|
|
589
|
+
g.status,
|
|
590
|
+
r.started_at,
|
|
591
|
+
r.trust_score,
|
|
592
|
+
g.duration_ms,
|
|
593
|
+
g.metrics_json
|
|
594
|
+
FROM gates g
|
|
595
|
+
JOIN runs r ON g.run_id = r.id
|
|
596
|
+
WHERE r.status IN ('passed', 'failed')
|
|
597
|
+
ORDER BY r.started_at DESC;
|
|
598
|
+
`;
|
|
599
|
+
return new Promise((resolve, reject) => {
|
|
600
|
+
this.db.exec(schema, (err) => {
|
|
601
|
+
if (err) {
|
|
602
|
+
reject(new Error(`Failed to initialize schema: ${err.message}`));
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
resolve();
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Ensure required directories exist
|
|
612
|
+
*/
|
|
613
|
+
ensureDirectories() {
|
|
614
|
+
if (!existsSync(this.baseDir)) {
|
|
615
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Close database connection
|
|
620
|
+
*/
|
|
621
|
+
async close() {
|
|
622
|
+
return new Promise((resolve, reject) => {
|
|
623
|
+
this.db.close((err) => {
|
|
624
|
+
if (err)
|
|
625
|
+
reject(err);
|
|
626
|
+
else
|
|
627
|
+
resolve();
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qa360",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "QA360 Proof CLI - Quality as Cryptographic Proof",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,21 +18,28 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc --project tsconfig.json",
|
|
20
20
|
"build:pkg": "tsc",
|
|
21
|
+
"build:bundle": "bash scripts/bundle-for-npm.sh",
|
|
21
22
|
"test": "vitest run",
|
|
22
23
|
"test:coverage": "vitest run --coverage",
|
|
23
24
|
"test:watch": "vitest",
|
|
24
25
|
"dev": "tsx watch src/index.ts",
|
|
25
|
-
"clean": "rimraf dist",
|
|
26
|
-
"prepublishOnly": "npm run build",
|
|
27
|
-
"publish:dry": "npm publish --dry-run",
|
|
28
|
-
"publish:real": "npm publish --access public --provenance"
|
|
26
|
+
"clean": "rimraf dist src/core",
|
|
27
|
+
"prepublishOnly": "npm run build:bundle",
|
|
28
|
+
"publish:dry": "npm run build:bundle && npm publish --dry-run",
|
|
29
|
+
"publish:real": "npm run build:bundle && npm publish --access public --provenance"
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
32
|
+
"@playwright/test": "^1.49.0",
|
|
33
|
+
"ajv": "^8.17.1",
|
|
34
|
+
"ajv-draft-04": "^1.0.0",
|
|
35
|
+
"ajv-formats": "^2.1.1",
|
|
31
36
|
"chalk": "^4.1.2",
|
|
32
37
|
"commander": "^11.0.0",
|
|
33
38
|
"inquirer": "^8.2.7",
|
|
39
|
+
"js-yaml": "^4.1.0",
|
|
34
40
|
"ora": "^5.4.1",
|
|
35
|
-
"
|
|
41
|
+
"sqlite3": "^5.1.6",
|
|
42
|
+
"tweetnacl": "^1.0.3"
|
|
36
43
|
},
|
|
37
44
|
"devDependencies": {
|
|
38
45
|
"@types/inquirer": "^9.0.9",
|