qa360 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/qa360.js +11 -0
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +1 -1
- package/dist/commands/report.d.ts +43 -28
- package/dist/commands/report.d.ts.map +1 -1
- package/dist/commands/report.js +275 -247
- package/dist/commands/run.d.ts +31 -50
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +122 -278
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +29 -6
- package/dist/index.js +17 -8
- package/package.json +14 -9
package/dist/commands/report.js
CHANGED
|
@@ -1,129 +1,121 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* QA360 Report Command -
|
|
2
|
+
* QA360 Report Command - Phase3 Proof Reports
|
|
3
|
+
* Generates PDF/HTML/JSON reports from Phase3Runner proof files
|
|
3
4
|
*/
|
|
4
5
|
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
|
|
5
6
|
import { join } from 'path';
|
|
6
|
-
import { createHash, generateKeyPair, sign } from 'crypto';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import ora from 'ora';
|
|
9
9
|
export class QA360Reporter {
|
|
10
10
|
qa360Dir = join(process.cwd(), '.qa360');
|
|
11
11
|
runsDir = join(this.qa360Dir, 'runs');
|
|
12
|
-
keysDir = join(this.qa360Dir, 'keys');
|
|
13
|
-
privateKeyPath = join(this.keysDir, 'qa360.key');
|
|
14
|
-
publicKeyPath = join(this.keysDir, 'qa360.pub');
|
|
15
12
|
async generateReport(options = {}) {
|
|
16
|
-
console.log(chalk.blue('📋 QA360 Report - Génération de
|
|
13
|
+
console.log(chalk.blue('📋 QA360 Report - Génération de rapport\n'));
|
|
17
14
|
try {
|
|
18
|
-
// 1. Load
|
|
19
|
-
const
|
|
20
|
-
console.log(chalk.green(`📊
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// 3. Generate reports
|
|
15
|
+
// 1. Load proof document
|
|
16
|
+
const proof = await this.loadProofDocument(options.runId);
|
|
17
|
+
console.log(chalk.green(`📊 Preuve chargée: ${proof.pack.name} (${proof.runId})`));
|
|
18
|
+
console.log(chalk.cyan(`🎯 Trust Score: ${proof.execution.trustScore}%`));
|
|
19
|
+
console.log(chalk.gray(`🔏 Signature: ${proof.signature.algorithm}`));
|
|
20
|
+
// 2. Generate reports
|
|
25
21
|
const outputs = [];
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const format = options.format || 'both';
|
|
23
|
+
if (format === 'json' || format === 'both') {
|
|
24
|
+
const jsonPath = await this.generateJsonReport(proof);
|
|
28
25
|
outputs.push(jsonPath);
|
|
29
26
|
}
|
|
30
|
-
if (
|
|
31
|
-
const
|
|
32
|
-
outputs.push(
|
|
27
|
+
if (format === 'html') {
|
|
28
|
+
const htmlPath = await this.generateHtmlOnlyReport(proof);
|
|
29
|
+
outputs.push(htmlPath);
|
|
33
30
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
if (format === 'pdf' || format === 'both') {
|
|
32
|
+
const pdfPath = await this.generatePdfReport(proof);
|
|
33
|
+
outputs.push(pdfPath);
|
|
37
34
|
}
|
|
38
35
|
return outputs;
|
|
39
36
|
}
|
|
40
37
|
catch (error) {
|
|
41
|
-
console.error(chalk.red('❌ Erreur
|
|
38
|
+
console.error(chalk.red('❌ Erreur:'), error);
|
|
42
39
|
throw error;
|
|
43
40
|
}
|
|
44
41
|
}
|
|
45
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Load proof document from Phase3Runner
|
|
44
|
+
*/
|
|
45
|
+
async loadProofDocument(runId) {
|
|
46
46
|
if (!existsSync(this.runsDir)) {
|
|
47
|
-
throw new Error('
|
|
47
|
+
throw new Error('Aucune preuve trouvée. Utilisez \'qa360 run\' d\'abord.');
|
|
48
48
|
}
|
|
49
|
-
let
|
|
49
|
+
let proofPath;
|
|
50
50
|
if (runId) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// Search for specific proof
|
|
52
|
+
proofPath = join(this.runsDir, `${runId}-proof.json`);
|
|
53
|
+
if (!existsSync(proofPath)) {
|
|
54
|
+
// Try with partial match
|
|
55
|
+
const files = readdirSync(this.runsDir).filter(f => f.includes(runId) && f.endsWith('-proof.json'));
|
|
56
|
+
if (files.length === 0) {
|
|
57
|
+
throw new Error(`Preuve non trouvée: ${runId}`);
|
|
58
|
+
}
|
|
59
|
+
proofPath = join(this.runsDir, files[0]);
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
62
|
else {
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
.filter(
|
|
63
|
+
// Find latest proof
|
|
64
|
+
const proofFiles = readdirSync(this.runsDir)
|
|
65
|
+
.filter(f => f.endsWith('-proof.json'))
|
|
60
66
|
.sort()
|
|
61
67
|
.reverse();
|
|
62
|
-
if (
|
|
63
|
-
throw new Error('
|
|
68
|
+
if (proofFiles.length === 0) {
|
|
69
|
+
throw new Error('Aucune preuve trouvée.');
|
|
64
70
|
}
|
|
65
|
-
|
|
71
|
+
proofPath = join(this.runsDir, proofFiles[0]);
|
|
66
72
|
}
|
|
67
|
-
const
|
|
68
|
-
const content = readFileSync(resultsPath, 'utf8');
|
|
73
|
+
const content = readFileSync(proofPath, 'utf8');
|
|
69
74
|
return JSON.parse(content);
|
|
70
75
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
let score = summary.successRate;
|
|
77
|
-
// Penalties and bonuses
|
|
78
|
-
if (summary.failed > 0) {
|
|
79
|
-
score -= Math.min(20, summary.failed * 5); // -5 points per failure, max -20
|
|
80
|
-
}
|
|
81
|
-
if (summary.skipped > summary.total * 0.5) {
|
|
82
|
-
score -= 10; // -10 if more than 50% skipped
|
|
83
|
-
}
|
|
84
|
-
// Bonus for comprehensive testing
|
|
85
|
-
if (runResult.pack.adapters.length > 2) {
|
|
86
|
-
score += 5; // +5 for multi-adapter testing
|
|
87
|
-
}
|
|
88
|
-
// Duration penalty for very slow tests
|
|
89
|
-
if (runResult.duration > 300000) { // 5 minutes
|
|
90
|
-
score -= 5;
|
|
91
|
-
}
|
|
92
|
-
return Math.max(0, Math.min(100, Math.round(score)));
|
|
76
|
+
/**
|
|
77
|
+
* Trust score is already calculated in proof document
|
|
78
|
+
*/
|
|
79
|
+
getTrustScore(proof) {
|
|
80
|
+
return proof.execution.trustScore;
|
|
93
81
|
}
|
|
94
|
-
async generateJsonReport(
|
|
82
|
+
async generateJsonReport(proof) {
|
|
95
83
|
const spinner = ora('Génération rapport JSON...').start();
|
|
96
84
|
try {
|
|
97
85
|
const report = {
|
|
98
86
|
id: this.generateReportId(),
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
87
|
+
generatedAt: new Date().toISOString(),
|
|
88
|
+
// Proof data
|
|
89
|
+
proof: {
|
|
90
|
+
runId: proof.runId,
|
|
91
|
+
timestamp: proof.timestamp,
|
|
92
|
+
version: proof.version
|
|
104
93
|
},
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
94
|
+
// Pack info
|
|
95
|
+
pack: proof.pack,
|
|
96
|
+
// Execution results
|
|
97
|
+
execution: proof.execution,
|
|
98
|
+
// Gates detail
|
|
99
|
+
gates: proof.gates,
|
|
100
|
+
// Hooks timing
|
|
101
|
+
hooks: proof.hooks,
|
|
102
|
+
// Signature
|
|
103
|
+
signature: {
|
|
104
|
+
algorithm: proof.signature.algorithm,
|
|
105
|
+
verified: proof.signature.algorithm.includes('ed25519'),
|
|
106
|
+
publicKey: proof.signature.publicKey ?
|
|
107
|
+
proof.signature.publicKey.substring(0, 16) + '...' : 'none',
|
|
108
|
+
timestamp: proof.signature.timestamp
|
|
109
109
|
},
|
|
110
|
-
|
|
111
|
-
results: runResult.results.map(r => ({
|
|
112
|
-
name: r.name,
|
|
113
|
-
adapter: r.adapter,
|
|
114
|
-
status: r.status,
|
|
115
|
-
duration: r.duration,
|
|
116
|
-
error: r.error,
|
|
117
|
-
output: r.output
|
|
118
|
-
})),
|
|
110
|
+
// Report metadata
|
|
119
111
|
metadata: {
|
|
120
|
-
qa360Version: '0.
|
|
112
|
+
qa360Version: '1.0.1',
|
|
121
113
|
platform: process.platform,
|
|
122
114
|
nodeVersion: process.version,
|
|
123
|
-
|
|
115
|
+
reportType: 'json'
|
|
124
116
|
}
|
|
125
117
|
};
|
|
126
|
-
const reportPath = join(this.
|
|
118
|
+
const reportPath = join(this.runsDir, `${proof.runId}-report.json`);
|
|
127
119
|
writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
128
120
|
spinner.succeed(`Rapport JSON généré: ${reportPath}`);
|
|
129
121
|
return reportPath;
|
|
@@ -133,21 +125,35 @@ export class QA360Reporter {
|
|
|
133
125
|
throw error;
|
|
134
126
|
}
|
|
135
127
|
}
|
|
136
|
-
async generatePdfReport(
|
|
128
|
+
async generatePdfReport(proof) {
|
|
137
129
|
const spinner = ora('Génération rapport PDF...').start();
|
|
138
130
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
const pdfPath = join(runDir, 'proof.pdf');
|
|
144
|
-
// Generate HTML content
|
|
145
|
-
const htmlContent = this.generateHtmlReport(runResult, trustScore);
|
|
131
|
+
const htmlContent = this.generateHtmlReport(proof);
|
|
132
|
+
const htmlPath = join(this.runsDir, `${proof.runId}-report.html`);
|
|
133
|
+
const pdfPath = join(this.runsDir, `${proof.runId}-report.pdf`);
|
|
134
|
+
// Save HTML first
|
|
146
135
|
writeFileSync(htmlPath, htmlContent);
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
136
|
+
// Try PDF generation with Playwright
|
|
137
|
+
try {
|
|
138
|
+
// @ts-ignore - Dynamic import, Playwright is optional
|
|
139
|
+
const { chromium } = await import('playwright');
|
|
140
|
+
const browser = await chromium.launch({ headless: true });
|
|
141
|
+
const page = await browser.newPage();
|
|
142
|
+
await page.setContent(htmlContent, { waitUntil: 'networkidle' });
|
|
143
|
+
await page.pdf({
|
|
144
|
+
path: pdfPath,
|
|
145
|
+
format: 'A4',
|
|
146
|
+
margin: { top: '20px', bottom: '20px', left: '20px', right: '20px' },
|
|
147
|
+
printBackground: true
|
|
148
|
+
});
|
|
149
|
+
await browser.close();
|
|
150
|
+
spinner.succeed(`Rapport PDF généré: ${pdfPath}`);
|
|
151
|
+
}
|
|
152
|
+
catch (playwrightError) {
|
|
153
|
+
// Fallback: just keep HTML
|
|
154
|
+
writeFileSync(pdfPath, htmlContent);
|
|
155
|
+
spinner.warn(`Rapport HTML généré (Playwright non disponible): ${htmlPath}`);
|
|
156
|
+
}
|
|
151
157
|
return pdfPath;
|
|
152
158
|
}
|
|
153
159
|
catch (error) {
|
|
@@ -155,180 +161,205 @@ export class QA360Reporter {
|
|
|
155
161
|
throw error;
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
|
-
|
|
159
|
-
const
|
|
164
|
+
async generateHtmlOnlyReport(proof) {
|
|
165
|
+
const spinner = ora('Génération rapport HTML...').start();
|
|
166
|
+
const htmlContent = this.generateHtmlReport(proof);
|
|
167
|
+
const htmlPath = join(this.runsDir, `${proof.runId}-report.html`);
|
|
168
|
+
writeFileSync(htmlPath, htmlContent);
|
|
169
|
+
spinner.succeed(`Rapport HTML généré: ${htmlPath}`);
|
|
170
|
+
return htmlPath;
|
|
171
|
+
}
|
|
172
|
+
generateHtmlReport(proof) {
|
|
173
|
+
const { pack, execution, gates, signature } = proof;
|
|
174
|
+
const trustScore = execution.trustScore;
|
|
160
175
|
const trustColor = trustScore >= 80 ? '#22c55e' : trustScore >= 60 ? '#f59e0b' : '#ef4444';
|
|
161
176
|
const trustLabel = trustScore >= 80 ? 'ÉLEVÉ' : trustScore >= 60 ? 'MOYEN' : 'FAIBLE';
|
|
177
|
+
const signatureStatus = signature.algorithm.includes('ed25519')
|
|
178
|
+
? '✅ Signé Ed25519'
|
|
179
|
+
: '⚠️ Non signé';
|
|
180
|
+
const passedGates = gates.filter(g => g.success).length;
|
|
162
181
|
return `
|
|
163
182
|
<!DOCTYPE html>
|
|
164
183
|
<html>
|
|
165
184
|
<head>
|
|
166
185
|
<meta charset="UTF-8">
|
|
167
|
-
<title>QA360
|
|
186
|
+
<title>QA360 Report - ${pack.name}</title>
|
|
168
187
|
<style>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.
|
|
187
|
-
.
|
|
188
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
189
|
+
body {
|
|
190
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
191
|
+
background: #f8fafc;
|
|
192
|
+
color: #1e293b;
|
|
193
|
+
line-height: 1.6;
|
|
194
|
+
}
|
|
195
|
+
.container { max-width: 900px; margin: 0 auto; padding: 40px 20px; }
|
|
196
|
+
|
|
197
|
+
.header {
|
|
198
|
+
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
|
199
|
+
color: white;
|
|
200
|
+
padding: 40px;
|
|
201
|
+
border-radius: 16px;
|
|
202
|
+
text-align: center;
|
|
203
|
+
margin-bottom: 30px;
|
|
204
|
+
}
|
|
205
|
+
.logo { font-size: 28px; font-weight: 800; margin-bottom: 8px; }
|
|
206
|
+
.subtitle { opacity: 0.9; font-size: 14px; }
|
|
207
|
+
|
|
208
|
+
.trust-card {
|
|
209
|
+
background: white;
|
|
210
|
+
border-radius: 16px;
|
|
211
|
+
padding: 30px;
|
|
212
|
+
text-align: center;
|
|
213
|
+
margin-bottom: 30px;
|
|
214
|
+
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
|
|
215
|
+
}
|
|
216
|
+
.trust-score {
|
|
217
|
+
font-size: 72px;
|
|
218
|
+
font-weight: 800;
|
|
219
|
+
color: ${trustColor};
|
|
220
|
+
line-height: 1;
|
|
221
|
+
}
|
|
222
|
+
.trust-label {
|
|
223
|
+
display: inline-block;
|
|
224
|
+
background: ${trustColor};
|
|
225
|
+
color: white;
|
|
226
|
+
padding: 6px 16px;
|
|
227
|
+
border-radius: 20px;
|
|
228
|
+
font-size: 12px;
|
|
229
|
+
font-weight: 600;
|
|
230
|
+
margin-top: 10px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.info-grid {
|
|
234
|
+
display: grid;
|
|
235
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
236
|
+
gap: 20px;
|
|
237
|
+
margin-bottom: 30px;
|
|
238
|
+
}
|
|
239
|
+
.info-card {
|
|
240
|
+
background: white;
|
|
241
|
+
border-radius: 12px;
|
|
242
|
+
padding: 20px;
|
|
243
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
244
|
+
}
|
|
245
|
+
.info-label { font-size: 12px; color: #64748b; text-transform: uppercase; font-weight: 600; }
|
|
246
|
+
.info-value { font-size: 18px; font-weight: 600; margin-top: 4px; }
|
|
247
|
+
|
|
248
|
+
.section {
|
|
249
|
+
background: white;
|
|
250
|
+
border-radius: 12px;
|
|
251
|
+
padding: 24px;
|
|
252
|
+
margin-bottom: 20px;
|
|
253
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
254
|
+
}
|
|
255
|
+
.section-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; }
|
|
256
|
+
|
|
257
|
+
.gate {
|
|
258
|
+
display: flex;
|
|
259
|
+
justify-content: space-between;
|
|
260
|
+
align-items: center;
|
|
261
|
+
padding: 12px 0;
|
|
262
|
+
border-bottom: 1px solid #f1f5f9;
|
|
263
|
+
}
|
|
264
|
+
.gate:last-child { border-bottom: none; }
|
|
265
|
+
.gate-name { font-weight: 500; }
|
|
266
|
+
.gate-meta { font-size: 12px; color: #64748b; }
|
|
267
|
+
.gate-status {
|
|
268
|
+
padding: 4px 12px;
|
|
269
|
+
border-radius: 6px;
|
|
270
|
+
font-size: 12px;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
}
|
|
273
|
+
.status-pass { background: #dcfce7; color: #166534; }
|
|
274
|
+
.status-fail { background: #fecaca; color: #991b1b; }
|
|
275
|
+
|
|
276
|
+
.signature-box {
|
|
277
|
+
background: #f8fafc;
|
|
278
|
+
border: 1px solid #e2e8f0;
|
|
279
|
+
border-radius: 8px;
|
|
280
|
+
padding: 16px;
|
|
281
|
+
font-family: monospace;
|
|
282
|
+
font-size: 12px;
|
|
283
|
+
word-break: break-all;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.footer {
|
|
287
|
+
text-align: center;
|
|
288
|
+
padding: 20px;
|
|
289
|
+
color: #64748b;
|
|
290
|
+
font-size: 12px;
|
|
291
|
+
}
|
|
188
292
|
</style>
|
|
189
293
|
</head>
|
|
190
294
|
<body>
|
|
191
|
-
<div class="
|
|
192
|
-
<div class="
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<div class="trust-label">CONFIANCE ${trustLabel}</div>
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
<div class="pack-info">
|
|
199
|
-
<h2>${pack.name}</h2>
|
|
200
|
-
<p><strong>Version:</strong> ${pack.version}</p>
|
|
201
|
-
<p><strong>Description:</strong> ${pack.description}</p>
|
|
202
|
-
<p><strong>Exécuté le:</strong> ${new Date(timestamp).toLocaleString('fr-FR')}</p>
|
|
203
|
-
<p><strong>Durée:</strong> ${Math.round(duration / 1000)}s</p>
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
<div class="summary">
|
|
207
|
-
<div class="metric">
|
|
208
|
-
<div class="metric-value" style="color: #3b82f6;">${summary.total}</div>
|
|
209
|
-
<div class="metric-label">Total Tests</div>
|
|
210
|
-
</div>
|
|
211
|
-
<div class="metric">
|
|
212
|
-
<div class="metric-value" style="color: #22c55e;">${summary.passed}</div>
|
|
213
|
-
<div class="metric-label">Succès</div>
|
|
295
|
+
<div class="container">
|
|
296
|
+
<div class="header">
|
|
297
|
+
<div class="logo">QA360</div>
|
|
298
|
+
<div class="subtitle">Preuve de Qualité Cryptographique</div>
|
|
214
299
|
</div>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<div class="
|
|
300
|
+
|
|
301
|
+
<div class="trust-card">
|
|
302
|
+
<div class="trust-score">${trustScore}%</div>
|
|
303
|
+
<div class="trust-label">CONFIANCE ${trustLabel}</div>
|
|
218
304
|
</div>
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<div class="
|
|
305
|
+
|
|
306
|
+
<div class="info-grid">
|
|
307
|
+
<div class="info-card">
|
|
308
|
+
<div class="info-label">Pack</div>
|
|
309
|
+
<div class="info-value">${pack.name}</div>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="info-card">
|
|
312
|
+
<div class="info-label">Version</div>
|
|
313
|
+
<div class="info-value">${pack.version || '1.0.0'}</div>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="info-card">
|
|
316
|
+
<div class="info-label">Durée</div>
|
|
317
|
+
<div class="info-value">${Math.round(execution.duration / 1000)}s</div>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="info-card">
|
|
320
|
+
<div class="info-label">Statut</div>
|
|
321
|
+
<div class="info-value">${execution.success ? '✅ Succès' : '❌ Échec'}</div>
|
|
322
|
+
</div>
|
|
222
323
|
</div>
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
324
|
+
|
|
325
|
+
<div class="section">
|
|
326
|
+
<div class="section-title">Quality Gates (${passedGates}/${gates.length})</div>
|
|
327
|
+
${gates.map(gate => `
|
|
328
|
+
<div class="gate">
|
|
329
|
+
<div>
|
|
330
|
+
<div class="gate-name">${gate.gate}</div>
|
|
331
|
+
<div class="gate-meta">${gate.adapter} • ${gate.duration}ms</div>
|
|
332
|
+
</div>
|
|
333
|
+
<div class="gate-status ${gate.success ? 'status-pass' : 'status-fail'}">
|
|
334
|
+
${gate.success ? 'PASS' : 'FAIL'}
|
|
335
|
+
</div>
|
|
232
336
|
</div>
|
|
233
|
-
|
|
337
|
+
`).join('')}
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<div class="section">
|
|
341
|
+
<div class="section-title">Signature Cryptographique</div>
|
|
342
|
+
<p style="margin-bottom: 12px; color: #64748b;">
|
|
343
|
+
${signatureStatus} • ${proof.timestamp}
|
|
344
|
+
</p>
|
|
345
|
+
<div class="signature-box">
|
|
346
|
+
<strong>Algorithm:</strong> ${signature.algorithm}<br>
|
|
347
|
+
<strong>Run ID:</strong> ${proof.runId}<br>
|
|
348
|
+
<strong>Signature:</strong> ${signature.value.substring(0, 64)}...
|
|
234
349
|
</div>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(Phase 2)
|
|
242
|
-
</div>
|
|
243
|
-
|
|
244
|
-
<div class="footer">
|
|
245
|
-
<p><strong>Signature:</strong> [Signature Ed25519 - Phase 1 implémentation]</p>
|
|
246
|
-
<p><strong>Généré par:</strong> QA360 Core v0.9.0-core</p>
|
|
247
|
-
<p><strong>Plateforme:</strong> ${process.platform} ${process.arch}</p>
|
|
248
|
-
<p><strong>Hash:</strong> ${this.generateContentHash(runResult)}</p>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div class="footer">
|
|
353
|
+
<p>Généré par QA360 v1.0.1 • ${new Date().toLocaleString('fr-FR')}</p>
|
|
354
|
+
<p>Vérifiez cette preuve avec: <code>qa360 verify ${proof.runId}</code></p>
|
|
355
|
+
</div>
|
|
249
356
|
</div>
|
|
250
357
|
</body>
|
|
251
358
|
</html>`;
|
|
252
359
|
}
|
|
253
|
-
getStatusLabel(status) {
|
|
254
|
-
switch (status) {
|
|
255
|
-
case 'passed': return 'SUCCÈS';
|
|
256
|
-
case 'failed': return 'ÉCHEC';
|
|
257
|
-
case 'skipped': return 'IGNORÉ';
|
|
258
|
-
default: return status.toUpperCase();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
generateContentHash(runResult) {
|
|
262
|
-
const content = JSON.stringify(runResult, null, 0);
|
|
263
|
-
return createHash('sha256').update(content).digest('hex').substring(0, 16);
|
|
264
|
-
}
|
|
265
360
|
generateReportId() {
|
|
266
361
|
return 'qa360-' + Date.now().toString(36) + '-' + Math.random().toString(36).substring(2, 8);
|
|
267
362
|
}
|
|
268
|
-
getLatestRunDir() {
|
|
269
|
-
const runs = readdirSync(this.runsDir).sort().reverse();
|
|
270
|
-
return join(this.runsDir, runs[0]);
|
|
271
|
-
}
|
|
272
|
-
async signReports(reportPaths, runResult, trustScore) {
|
|
273
|
-
const spinner = ora('Signature cryptographique...').start();
|
|
274
|
-
try {
|
|
275
|
-
// Ensure keys exist
|
|
276
|
-
await this.ensureKeys();
|
|
277
|
-
// Read private key
|
|
278
|
-
const privateKey = readFileSync(this.privateKeyPath, 'utf8');
|
|
279
|
-
// Create signature payload
|
|
280
|
-
const payload = {
|
|
281
|
-
reports: reportPaths.map(path => ({
|
|
282
|
-
path: path.split('/').pop(),
|
|
283
|
-
hash: this.hashFile(path)
|
|
284
|
-
})),
|
|
285
|
-
trustScore,
|
|
286
|
-
timestamp: new Date().toISOString(),
|
|
287
|
-
pack: runResult.pack.name
|
|
288
|
-
};
|
|
289
|
-
const payloadString = JSON.stringify(payload, null, 0);
|
|
290
|
-
const signature = sign('sha256', Buffer.from(payloadString), privateKey).toString('base64');
|
|
291
|
-
// Save signature
|
|
292
|
-
const signaturePath = join(this.getLatestRunDir(), 'signature.json');
|
|
293
|
-
writeFileSync(signaturePath, JSON.stringify({
|
|
294
|
-
payload,
|
|
295
|
-
signature,
|
|
296
|
-
algorithm: 'Ed25519',
|
|
297
|
-
qa360Version: '0.9.0-core'
|
|
298
|
-
}, null, 2));
|
|
299
|
-
spinner.succeed(`Signature générée: ${signaturePath}`);
|
|
300
|
-
console.log(chalk.green(`🔐 Preuve signée cryptographiquement`));
|
|
301
|
-
}
|
|
302
|
-
catch (error) {
|
|
303
|
-
spinner.fail('Échec signature');
|
|
304
|
-
throw error;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
async ensureKeys() {
|
|
308
|
-
if (existsSync(this.privateKeyPath) && existsSync(this.publicKeyPath)) {
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
console.log(chalk.yellow('🔑 Génération de la paire de clés Ed25519...'));
|
|
312
|
-
return new Promise((resolve, reject) => {
|
|
313
|
-
generateKeyPair('ed25519', {
|
|
314
|
-
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
315
|
-
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
316
|
-
}, (err, publicKey, privateKey) => {
|
|
317
|
-
if (err) {
|
|
318
|
-
reject(err);
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
writeFileSync(this.publicKeyPath, publicKey);
|
|
322
|
-
writeFileSync(this.privateKeyPath, privateKey);
|
|
323
|
-
console.log(chalk.green('✅ Clés générées'));
|
|
324
|
-
resolve();
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
hashFile(filePath) {
|
|
329
|
-
const content = readFileSync(filePath);
|
|
330
|
-
return createHash('sha256').update(content).digest('hex');
|
|
331
|
-
}
|
|
332
363
|
}
|
|
333
364
|
export async function reportCommand(options = {}) {
|
|
334
365
|
const reporter = new QA360Reporter();
|
|
@@ -338,10 +369,7 @@ export async function reportCommand(options = {}) {
|
|
|
338
369
|
outputs.forEach(path => {
|
|
339
370
|
console.log(chalk.blue(` 📄 ${path}`));
|
|
340
371
|
});
|
|
341
|
-
|
|
342
|
-
console.log(chalk.green('\n🔐 Preuve signée et vérifiable'));
|
|
343
|
-
console.log(chalk.blue('💡 Utilisez "qa360 verify" pour vérifier la signature'));
|
|
344
|
-
}
|
|
372
|
+
console.log(chalk.gray('\n💡 Vérifiez la preuve avec: qa360 verify'));
|
|
345
373
|
}
|
|
346
374
|
catch (error) {
|
|
347
375
|
console.error(chalk.red('❌ Échec génération rapport:'), error);
|