qa360 1.0.3 → 1.1.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/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 +12 -6
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QA360 Pack v1 Validator
|
|
3
|
+
* Validates pack.yml files against the official schema
|
|
4
|
+
*/
|
|
5
|
+
import Ajv from 'ajv';
|
|
6
|
+
import addFormats from 'ajv-formats';
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
export class PackValidator {
|
|
11
|
+
ajv;
|
|
12
|
+
schema;
|
|
13
|
+
constructor() {
|
|
14
|
+
this.ajv = new Ajv({
|
|
15
|
+
allErrors: true,
|
|
16
|
+
verbose: true,
|
|
17
|
+
strict: false
|
|
18
|
+
});
|
|
19
|
+
addFormats(this.ajv);
|
|
20
|
+
// Load schema (ES modules compatible)
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = dirname(__filename);
|
|
23
|
+
const schemaPath = join(__dirname, '../../schemas/pack.schema.json');
|
|
24
|
+
this.schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
|
|
25
|
+
this.ajv.addSchema(this.schema, 'pack-v1');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate a pack configuration
|
|
29
|
+
*/
|
|
30
|
+
validate(pack) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
const warnings = [];
|
|
33
|
+
// Schema validation
|
|
34
|
+
const valid = this.ajv.validate('pack-v1', pack);
|
|
35
|
+
if (!valid && this.ajv.errors) {
|
|
36
|
+
for (const error of this.ajv.errors) {
|
|
37
|
+
errors.push({
|
|
38
|
+
code: this.getErrorCode(error),
|
|
39
|
+
path: error.instancePath || error.schemaPath || 'root',
|
|
40
|
+
message: this.formatErrorMessage(error),
|
|
41
|
+
suggestion: this.getSuggestion(error)
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Custom business logic validation
|
|
46
|
+
if (valid) {
|
|
47
|
+
const businessRules = this.validateBusinessRules(pack);
|
|
48
|
+
errors.push(...businessRules.errors);
|
|
49
|
+
warnings.push(...businessRules.warnings);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
valid: errors.length === 0,
|
|
53
|
+
errors,
|
|
54
|
+
warnings
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Validate business rules beyond schema
|
|
59
|
+
*/
|
|
60
|
+
validateBusinessRules(pack) {
|
|
61
|
+
const errors = [];
|
|
62
|
+
const warnings = [];
|
|
63
|
+
// Check gate-target consistency (only for non-minimal packs)
|
|
64
|
+
if (pack.gates.includes('api_smoke') && !pack.targets?.api) {
|
|
65
|
+
// For minimal smoke packs, this is a warning, not an error
|
|
66
|
+
if (pack.gates.length === 1 && pack.gates[0] === 'api_smoke') {
|
|
67
|
+
warnings.push({
|
|
68
|
+
code: 'QP001',
|
|
69
|
+
path: 'targets.api',
|
|
70
|
+
message: 'API smoke gate without api target - using default configuration',
|
|
71
|
+
suggestion: 'Add targets.api with baseUrl for better control'
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
errors.push({
|
|
76
|
+
code: 'QP001',
|
|
77
|
+
path: 'targets.api',
|
|
78
|
+
message: 'API smoke gate requires api target configuration',
|
|
79
|
+
suggestion: 'Add targets.api with baseUrl'
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (pack.gates.includes('ui') && !pack.targets?.web) {
|
|
84
|
+
errors.push({
|
|
85
|
+
code: 'QP002',
|
|
86
|
+
path: 'targets.web',
|
|
87
|
+
message: 'UI gate requires web target configuration',
|
|
88
|
+
suggestion: 'Add targets.web with baseUrl'
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Check budget recommendations
|
|
92
|
+
if (pack.gates.includes('perf') && !pack.budgets?.perf_p95_ms) {
|
|
93
|
+
warnings.push({
|
|
94
|
+
code: 'QP003',
|
|
95
|
+
path: 'budgets.perf_p95_ms',
|
|
96
|
+
message: 'Performance gate without budget may not fail appropriately',
|
|
97
|
+
suggestion: 'Add budgets.perf_p95_ms (recommended: 800-2000ms)'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (pack.gates.includes('a11y') && !pack.budgets?.a11y_min) {
|
|
101
|
+
warnings.push({
|
|
102
|
+
code: 'QP004',
|
|
103
|
+
path: 'budgets.a11y_min',
|
|
104
|
+
message: 'Accessibility gate without budget may not fail appropriately',
|
|
105
|
+
suggestion: 'Add budgets.a11y_min (recommended: 90-95%)'
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Check security configuration
|
|
109
|
+
if ((pack.gates.includes('sast') || pack.gates.includes('dast')) && !pack.security) {
|
|
110
|
+
warnings.push({
|
|
111
|
+
code: 'QP005',
|
|
112
|
+
path: 'security',
|
|
113
|
+
message: 'Security gates without security configuration',
|
|
114
|
+
suggestion: 'Add security section with sast_max_high and secrets_leak settings'
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// Check for potential secrets in plain text (ignore secret references)
|
|
118
|
+
if (pack.environment) {
|
|
119
|
+
const SECRET_REF = /\$\{\{\s*secrets\.[A-Z0-9_]+\s*\}\}/g;
|
|
120
|
+
for (const [key, value] of Object.entries(pack.environment)) {
|
|
121
|
+
if (typeof value === 'string' && !SECRET_REF.test(value) && this.looksLikeSecret(value)) {
|
|
122
|
+
errors.push({
|
|
123
|
+
code: 'QP006',
|
|
124
|
+
path: `environment.${key}`,
|
|
125
|
+
message: 'Potential secret detected in plain text',
|
|
126
|
+
suggestion: `Use secret reference: \${{ secrets.${key.toUpperCase()} }}`
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Check for dangerous hook commands
|
|
132
|
+
if (pack.hooks) {
|
|
133
|
+
const dangerousCommands = ['rm -rf', 'sudo rm', 'del /f', 'format', 'mkfs', 'dd if='];
|
|
134
|
+
['beforeAll', 'beforeEach', 'afterEach', 'afterAll'].forEach(hookType => {
|
|
135
|
+
const hooks = pack.hooks[hookType];
|
|
136
|
+
if (hooks) {
|
|
137
|
+
hooks.forEach((hook, index) => {
|
|
138
|
+
const command = typeof hook === 'string' ? hook :
|
|
139
|
+
('run' in hook ? hook.run :
|
|
140
|
+
'compose' in hook ? `compose ${hook.compose}` :
|
|
141
|
+
'wait_on' in hook ? `wait_on ${hook.wait_on}` : '');
|
|
142
|
+
if (command && dangerousCommands.some(dangerous => command.toLowerCase().includes(dangerous))) {
|
|
143
|
+
warnings.push({
|
|
144
|
+
code: 'QP007',
|
|
145
|
+
path: `hooks.${hookType}[${index}]`,
|
|
146
|
+
message: 'Potentially dangerous command detected',
|
|
147
|
+
suggestion: 'Review command for safety'
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return { errors, warnings };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if a value looks like a secret
|
|
158
|
+
*/
|
|
159
|
+
looksLikeSecret(value) {
|
|
160
|
+
const secretPatterns = [
|
|
161
|
+
/^[A-Za-z0-9+/]{20,}={0,2}$/, // Base64-like
|
|
162
|
+
/^[a-f0-9]{32,}$/i, // Hex tokens
|
|
163
|
+
/^sk-[a-zA-Z0-9]{32,}$/, // API keys
|
|
164
|
+
/^ghp_[a-zA-Z0-9]{36}$/, // GitHub tokens
|
|
165
|
+
/^xoxb-[a-zA-Z0-9-]+$/, // Slack tokens
|
|
166
|
+
];
|
|
167
|
+
return secretPatterns.some(pattern => pattern.test(value));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check if a value is a secret reference
|
|
171
|
+
*/
|
|
172
|
+
isSecretReference(value) {
|
|
173
|
+
return /^\$\{\{\s*secrets\.[A-Z_][A-Z0-9_]*\s*\}\}$/.test(value);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Validate hook commands for security issues
|
|
177
|
+
*/
|
|
178
|
+
validateHookSecurity(hooks, basePath, warnings) {
|
|
179
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
180
|
+
const hook = hooks[i];
|
|
181
|
+
if (typeof hook.run === 'string') {
|
|
182
|
+
// Check for dangerous commands
|
|
183
|
+
const dangerousPatterns = [
|
|
184
|
+
/rm\s+-rf\s+\//, // rm -rf /
|
|
185
|
+
/sudo\s+/, // sudo commands
|
|
186
|
+
/curl.*\|\s*sh/, // curl | sh
|
|
187
|
+
/wget.*\|\s*sh/, // wget | sh
|
|
188
|
+
];
|
|
189
|
+
for (const pattern of dangerousPatterns) {
|
|
190
|
+
if (pattern.test(hook.run)) {
|
|
191
|
+
warnings.push({
|
|
192
|
+
code: 'QP007',
|
|
193
|
+
path: `${basePath}[${i}].run`,
|
|
194
|
+
message: 'Potentially dangerous command detected',
|
|
195
|
+
suggestion: 'Review command for security implications'
|
|
196
|
+
});
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get error code from AJV error
|
|
205
|
+
*/
|
|
206
|
+
getErrorCode(error) {
|
|
207
|
+
const codeMap = {
|
|
208
|
+
'required': 'QP100',
|
|
209
|
+
'type': 'QP101',
|
|
210
|
+
'format': 'QP102',
|
|
211
|
+
'pattern': 'QP103',
|
|
212
|
+
'enum': 'QP104',
|
|
213
|
+
'const': 'QP105',
|
|
214
|
+
'minimum': 'QP106',
|
|
215
|
+
'maximum': 'QP107',
|
|
216
|
+
'minLength': 'QP108',
|
|
217
|
+
'maxLength': 'QP109',
|
|
218
|
+
'minItems': 'QP110',
|
|
219
|
+
'uniqueItems': 'QP111',
|
|
220
|
+
'additionalProperties': 'QP112'
|
|
221
|
+
};
|
|
222
|
+
return codeMap[error.keyword] || 'QP999';
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Format error message for better UX
|
|
226
|
+
*/
|
|
227
|
+
formatErrorMessage(error) {
|
|
228
|
+
const path = error.instancePath || 'root';
|
|
229
|
+
switch (error.keyword) {
|
|
230
|
+
case 'required':
|
|
231
|
+
return `Missing required field: ${error.params.missingProperty}`;
|
|
232
|
+
case 'type':
|
|
233
|
+
return `Expected ${error.params.type}, got ${typeof error.data}`;
|
|
234
|
+
case 'format':
|
|
235
|
+
return `Invalid ${error.params.format} format`;
|
|
236
|
+
case 'pattern':
|
|
237
|
+
return `Value does not match required pattern`;
|
|
238
|
+
case 'enum':
|
|
239
|
+
return `Value must be one of: ${error.params.allowedValues.join(', ')}`;
|
|
240
|
+
case 'const':
|
|
241
|
+
return `Value must be exactly: ${error.params.allowedValue}`;
|
|
242
|
+
case 'minimum':
|
|
243
|
+
return `Value must be >= ${error.params.limit}`;
|
|
244
|
+
case 'maximum':
|
|
245
|
+
return `Value must be <= ${error.params.limit}`;
|
|
246
|
+
case 'minLength':
|
|
247
|
+
return `Value must be at least ${error.params.limit} characters`;
|
|
248
|
+
case 'maxLength':
|
|
249
|
+
return `Value must be at most ${error.params.limit} characters`;
|
|
250
|
+
case 'minItems':
|
|
251
|
+
return `Array must have at least ${error.params.limit} items`;
|
|
252
|
+
case 'uniqueItems':
|
|
253
|
+
return `Array items must be unique`;
|
|
254
|
+
case 'additionalProperties':
|
|
255
|
+
return `Unknown property: ${error.params.additionalProperty}`;
|
|
256
|
+
default:
|
|
257
|
+
return error.message || 'Validation error';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get suggestion for fixing error
|
|
262
|
+
*/
|
|
263
|
+
getSuggestion(error) {
|
|
264
|
+
switch (error.keyword) {
|
|
265
|
+
case 'required':
|
|
266
|
+
const field = error.params.missingProperty;
|
|
267
|
+
const suggestions = {
|
|
268
|
+
'version': 'Add: version: 1',
|
|
269
|
+
'name': 'Add: name: "my-pack"',
|
|
270
|
+
'gates': 'Add: gates: ["api_smoke"]',
|
|
271
|
+
'baseUrl': 'Add: baseUrl: "https://api.example.com"'
|
|
272
|
+
};
|
|
273
|
+
return suggestions[field];
|
|
274
|
+
case 'enum':
|
|
275
|
+
return `Use one of: ${error.params.allowedValues.join(', ')}`;
|
|
276
|
+
case 'format':
|
|
277
|
+
if (error.params.format === 'uri') {
|
|
278
|
+
return 'Use full URL with protocol: https://example.com';
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
case 'pattern':
|
|
282
|
+
if (error.instancePath.includes('name')) {
|
|
283
|
+
return 'Use only letters, numbers, underscore, and hyphen';
|
|
284
|
+
}
|
|
285
|
+
if (error.instancePath.includes('smoke')) {
|
|
286
|
+
return 'Format: "GET /path -> 200"';
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof Bundle Creation
|
|
3
|
+
*
|
|
4
|
+
* Creates cryptographically signed proof bundles from test run data.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/rfc/proof-bundle-v1.md#3-data-model
|
|
7
|
+
*/
|
|
8
|
+
import { type KeyPair } from './signer.js';
|
|
9
|
+
/**
|
|
10
|
+
* Proof Bundle structure (matches RFC spec)
|
|
11
|
+
*/
|
|
12
|
+
export interface ProofBundle {
|
|
13
|
+
spec: 'qa360.proof.v1';
|
|
14
|
+
run: RunMetadata;
|
|
15
|
+
artifacts: Artifact[];
|
|
16
|
+
results: TestResults;
|
|
17
|
+
signing: SigningMetadata;
|
|
18
|
+
signature: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Run metadata
|
|
22
|
+
*/
|
|
23
|
+
export interface RunMetadata {
|
|
24
|
+
id: string;
|
|
25
|
+
startedAt: string;
|
|
26
|
+
finishedAt: string;
|
|
27
|
+
environment: Environment;
|
|
28
|
+
packHash: string;
|
|
29
|
+
ciContext: CIContext;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Environment information
|
|
33
|
+
*/
|
|
34
|
+
export interface Environment {
|
|
35
|
+
os: 'windows' | 'linux' | 'darwin';
|
|
36
|
+
node: string;
|
|
37
|
+
arch: 'x64' | 'arm64';
|
|
38
|
+
ci: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* CI context
|
|
42
|
+
*/
|
|
43
|
+
export interface CIContext {
|
|
44
|
+
provider: string | null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Artifact metadata
|
|
48
|
+
*/
|
|
49
|
+
export interface Artifact {
|
|
50
|
+
name: string;
|
|
51
|
+
sha256: string;
|
|
52
|
+
size: number;
|
|
53
|
+
path?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Test results
|
|
57
|
+
*/
|
|
58
|
+
export interface TestResults {
|
|
59
|
+
trustScore: number;
|
|
60
|
+
gates: Gate[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Gate result
|
|
64
|
+
*/
|
|
65
|
+
export interface Gate {
|
|
66
|
+
name: string;
|
|
67
|
+
status: 'pass' | 'fail' | 'skip';
|
|
68
|
+
metrics?: Record<string, any>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Signing metadata
|
|
72
|
+
*/
|
|
73
|
+
export interface SigningMetadata {
|
|
74
|
+
algo: 'ed25519';
|
|
75
|
+
signerId: string;
|
|
76
|
+
timestamp: TimestampInfo;
|
|
77
|
+
identity: IdentityInfo;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Timestamp information (Phase 1: none, Phase 2: rfc3161)
|
|
81
|
+
*/
|
|
82
|
+
export interface TimestampInfo {
|
|
83
|
+
type: 'none' | 'rfc3161';
|
|
84
|
+
token: string | null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Identity information (Phase 1: none, Phase 2: did/sigstore)
|
|
88
|
+
*/
|
|
89
|
+
export interface IdentityInfo {
|
|
90
|
+
type: 'none' | 'did' | 'sigstore';
|
|
91
|
+
evidence: string | object | null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Parameters for creating a proof bundle
|
|
95
|
+
*/
|
|
96
|
+
export interface ProofBundleParams {
|
|
97
|
+
runId?: string;
|
|
98
|
+
startedAt: Date;
|
|
99
|
+
finishedAt: Date;
|
|
100
|
+
packHash: string;
|
|
101
|
+
artifacts: Artifact[];
|
|
102
|
+
trustScore: number;
|
|
103
|
+
gates: Gate[];
|
|
104
|
+
signerId?: string;
|
|
105
|
+
keyPair?: KeyPair;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compute SHA-256 hash of string
|
|
109
|
+
*/
|
|
110
|
+
export declare function computeSHA256(data: string): string;
|
|
111
|
+
/**
|
|
112
|
+
* Create and sign a proof bundle
|
|
113
|
+
*
|
|
114
|
+
* @param params - Bundle parameters
|
|
115
|
+
* @returns Signed proof bundle
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* const bundle = await createProofBundle({
|
|
120
|
+
* startedAt: new Date('2025-01-01T00:00:00Z'),
|
|
121
|
+
* finishedAt: new Date('2025-01-01T00:01:00Z'),
|
|
122
|
+
* packHash: 'sha256-abc123...',
|
|
123
|
+
* artifacts: [],
|
|
124
|
+
* trustScore: 87,
|
|
125
|
+
* gates: [{ name: 'api_smoke', status: 'pass' }],
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export declare function createProofBundle(params: ProofBundleParams): Promise<ProofBundle>;
|
|
130
|
+
/**
|
|
131
|
+
* Create proof bundle from pack file content
|
|
132
|
+
*
|
|
133
|
+
* @param packContent - Pack YAML/JSON content
|
|
134
|
+
* @param params - Other bundle parameters
|
|
135
|
+
* @returns Signed proof bundle
|
|
136
|
+
*/
|
|
137
|
+
export declare function createProofBundleFromPack(packContent: string, params: Omit<ProofBundleParams, 'packHash'>): Promise<ProofBundle>;
|
|
138
|
+
//# sourceMappingURL=bundle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/core/proof/bundle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proof Bundle Creation
|
|
3
|
+
*
|
|
4
|
+
* Creates cryptographically signed proof bundles from test run data.
|
|
5
|
+
*
|
|
6
|
+
* @see docs/rfc/proof-bundle-v1.md#3-data-model
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from 'crypto';
|
|
9
|
+
import { platform, arch } from 'os';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
import { canonicalizeForSigning } from './canonicalize.js';
|
|
12
|
+
import { sign, initializeKeys } from './signer.js';
|
|
13
|
+
import { validateProofBundle } from './schema.js';
|
|
14
|
+
/**
|
|
15
|
+
* Get current OS in RFC format
|
|
16
|
+
*/
|
|
17
|
+
function getCurrentOS() {
|
|
18
|
+
const p = platform();
|
|
19
|
+
if (p === 'win32')
|
|
20
|
+
return 'windows';
|
|
21
|
+
if (p === 'darwin')
|
|
22
|
+
return 'darwin';
|
|
23
|
+
return 'linux';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get current architecture in RFC format
|
|
27
|
+
*/
|
|
28
|
+
function getCurrentArch() {
|
|
29
|
+
const a = arch();
|
|
30
|
+
if (a === 'arm64')
|
|
31
|
+
return 'arm64';
|
|
32
|
+
return 'x64';
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get Node.js version (semver format)
|
|
36
|
+
*/
|
|
37
|
+
function getNodeVersion() {
|
|
38
|
+
return process.version.replace(/^v/, '');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Detect if running in CI
|
|
42
|
+
*/
|
|
43
|
+
function isCI() {
|
|
44
|
+
return !!(process.env.CI ||
|
|
45
|
+
process.env.GITHUB_ACTIONS ||
|
|
46
|
+
process.env.GITLAB_CI ||
|
|
47
|
+
process.env.CIRCLECI ||
|
|
48
|
+
process.env.JENKINS_URL);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Detect CI provider
|
|
52
|
+
*/
|
|
53
|
+
function getCIProvider() {
|
|
54
|
+
if (process.env.GITHUB_ACTIONS)
|
|
55
|
+
return 'github-actions';
|
|
56
|
+
if (process.env.GITLAB_CI)
|
|
57
|
+
return 'gitlab-ci';
|
|
58
|
+
if (process.env.CIRCLECI)
|
|
59
|
+
return 'circleci';
|
|
60
|
+
if (process.env.JENKINS_URL)
|
|
61
|
+
return 'jenkins';
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Compute SHA-256 hash of string
|
|
66
|
+
*/
|
|
67
|
+
export function computeSHA256(data) {
|
|
68
|
+
const hash = createHash('sha256').update(data, 'utf-8').digest('hex');
|
|
69
|
+
return `sha256-${hash}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create unsigned proof bundle (without signature)
|
|
73
|
+
*/
|
|
74
|
+
function createUnsignedBundle(params) {
|
|
75
|
+
return {
|
|
76
|
+
spec: 'qa360.proof.v1',
|
|
77
|
+
run: {
|
|
78
|
+
id: params.runId || randomUUID(),
|
|
79
|
+
startedAt: params.startedAt.toISOString(),
|
|
80
|
+
finishedAt: params.finishedAt.toISOString(),
|
|
81
|
+
environment: {
|
|
82
|
+
os: getCurrentOS(),
|
|
83
|
+
node: getNodeVersion(),
|
|
84
|
+
arch: getCurrentArch(),
|
|
85
|
+
ci: isCI(),
|
|
86
|
+
},
|
|
87
|
+
packHash: params.packHash,
|
|
88
|
+
ciContext: {
|
|
89
|
+
provider: getCIProvider(),
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
artifacts: params.artifacts,
|
|
93
|
+
results: {
|
|
94
|
+
trustScore: params.trustScore,
|
|
95
|
+
gates: params.gates,
|
|
96
|
+
},
|
|
97
|
+
signing: {
|
|
98
|
+
algo: 'ed25519',
|
|
99
|
+
signerId: params.signerId || 'local@qa360',
|
|
100
|
+
timestamp: {
|
|
101
|
+
type: 'none',
|
|
102
|
+
token: null,
|
|
103
|
+
},
|
|
104
|
+
identity: {
|
|
105
|
+
type: 'none',
|
|
106
|
+
evidence: null,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create and sign a proof bundle
|
|
113
|
+
*
|
|
114
|
+
* @param params - Bundle parameters
|
|
115
|
+
* @returns Signed proof bundle
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* const bundle = await createProofBundle({
|
|
120
|
+
* startedAt: new Date('2025-01-01T00:00:00Z'),
|
|
121
|
+
* finishedAt: new Date('2025-01-01T00:01:00Z'),
|
|
122
|
+
* packHash: 'sha256-abc123...',
|
|
123
|
+
* artifacts: [],
|
|
124
|
+
* trustScore: 87,
|
|
125
|
+
* gates: [{ name: 'api_smoke', status: 'pass' }],
|
|
126
|
+
* });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export async function createProofBundle(params) {
|
|
130
|
+
// Load or initialize keys
|
|
131
|
+
const keyPair = params.keyPair || await initializeKeys();
|
|
132
|
+
// Create unsigned bundle
|
|
133
|
+
const unsigned = createUnsignedBundle(params);
|
|
134
|
+
// Canonicalize for signing (removes signature field, adds newline)
|
|
135
|
+
const canonical = canonicalizeForSigning(unsigned);
|
|
136
|
+
// Sign the canonical form
|
|
137
|
+
const signature = sign(canonical, keyPair.secretKey);
|
|
138
|
+
// Create final bundle
|
|
139
|
+
const bundle = {
|
|
140
|
+
...unsigned,
|
|
141
|
+
signature,
|
|
142
|
+
};
|
|
143
|
+
// Validate against schema
|
|
144
|
+
const validation = validateProofBundle(bundle);
|
|
145
|
+
if (!validation.valid) {
|
|
146
|
+
throw new Error(`Invalid proof bundle: ${validation.errors?.join(', ')}`);
|
|
147
|
+
}
|
|
148
|
+
return bundle;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Create proof bundle from pack file content
|
|
152
|
+
*
|
|
153
|
+
* @param packContent - Pack YAML/JSON content
|
|
154
|
+
* @param params - Other bundle parameters
|
|
155
|
+
* @returns Signed proof bundle
|
|
156
|
+
*/
|
|
157
|
+
export async function createProofBundleFromPack(packContent, params) {
|
|
158
|
+
const packHash = computeSHA256(packContent);
|
|
159
|
+
return createProofBundle({ ...params, packHash });
|
|
160
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Canonicalization for deterministic hashing
|
|
3
|
+
*
|
|
4
|
+
* Implements RFC 8785-like canonicalization:
|
|
5
|
+
* - Alphabetically sorted keys (recursive)
|
|
6
|
+
* - UTF-8 NFC normalization
|
|
7
|
+
* - No whitespace (compact)
|
|
8
|
+
* - Deterministic across platforms
|
|
9
|
+
*
|
|
10
|
+
* @see docs/rfc/proof-bundle-v1.md#4-canonicalization
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Canonicalize a JavaScript object into deterministic JSON string
|
|
14
|
+
*
|
|
15
|
+
* @param obj - Object to canonicalize
|
|
16
|
+
* @returns Canonical JSON string (compact, sorted keys, UTF-8 NFC)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const obj = { b: 2, a: 1 };
|
|
21
|
+
* const canonical = canonicalize(obj);
|
|
22
|
+
* // Returns: '{"a":1,"b":2}\n'
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function canonicalize(obj: any): string;
|
|
26
|
+
/**
|
|
27
|
+
* Canonicalize and append newline (standard format)
|
|
28
|
+
*
|
|
29
|
+
* @param obj - Object to canonicalize
|
|
30
|
+
* @returns Canonical JSON string with trailing newline
|
|
31
|
+
*/
|
|
32
|
+
export declare function canonicalizeWithNewline(obj: any): string;
|
|
33
|
+
/**
|
|
34
|
+
* Remove signature field and canonicalize
|
|
35
|
+
* Used for signature verification
|
|
36
|
+
*
|
|
37
|
+
* @param bundle - Proof bundle (may contain signature field)
|
|
38
|
+
* @returns Canonical JSON without signature field
|
|
39
|
+
*/
|
|
40
|
+
export declare function canonicalizeForSigning(bundle: any): string;
|
|
41
|
+
/**
|
|
42
|
+
* Verify canonicalization is deterministic
|
|
43
|
+
*
|
|
44
|
+
* @param obj - Object to test
|
|
45
|
+
* @returns true if canonicalization is stable
|
|
46
|
+
*/
|
|
47
|
+
export declare function isCanonicalStable(obj: any): boolean;
|
|
48
|
+
//# sourceMappingURL=canonicalize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonicalize.d.ts","sourceRoot":"","sources":["../../../src/core/proof/canonicalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CA+C7C;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAExD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,CAQ1D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAQnD"}
|