@arcadialdev/arcality 2.6.7 โ 3.0.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/bin/arcality.mjs +11 -0
- package/package.json +1 -1
- package/playwright.config.js +24 -5
- package/scripts/gen-and-run.mjs +1045 -249
- package/scripts/init.mjs +221 -178
- package/src/arcalityClient.mjs +128 -1
- package/src/configLoader.mjs +6 -2
- package/src/configManager.mjs +1 -1
- package/tests/_helpers/ArcalityReporter.js +234 -251
- package/tests/_helpers/agentic-runner.bundle.spec.js +796 -109
|
@@ -1,122 +1,104 @@
|
|
|
1
|
-
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
4
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
-
mod
|
|
26
|
-
));
|
|
27
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
-
var ArcalityReporter_exports = {};
|
|
29
|
-
__export(ArcalityReporter_exports, {
|
|
30
|
-
default: () => ArcalityReporter_default
|
|
31
|
-
});
|
|
32
|
-
module.exports = __toCommonJS(ArcalityReporter_exports);
|
|
33
|
-
var import_fs = __toESM(require("fs"));
|
|
34
|
-
var import_path = __toESM(require("path"));
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
35
8
|
class ArcalityReporter {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.outputDir = options.outputDir || "arcality-report";
|
|
42
|
-
}
|
|
43
|
-
onBegin(config, suite) {
|
|
44
|
-
if (import_fs.default.existsSync(this.outputDir)) {
|
|
45
|
-
try {
|
|
46
|
-
import_fs.default.rmSync(this.outputDir, { recursive: true, force: true });
|
|
47
|
-
console.log(`\u{1F9F9} Cleaned old report data at: ${this.outputDir}`);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.warn(`\u26A0\uFE0F Could not fully clean report dir: ${err.message}`);
|
|
50
|
-
}
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.results = [];
|
|
11
|
+
this.totalDuration = 0;
|
|
12
|
+
this.startTime = new Date();
|
|
13
|
+
this.outputDir = options.outputDir || 'arcality-report';
|
|
51
14
|
}
|
|
52
|
-
|
|
53
|
-
|
|
15
|
+
onBegin(config, suite) {
|
|
16
|
+
// Clean output directory to avoid stale reports/attachments
|
|
17
|
+
if (fs_1.default.existsSync(this.outputDir)) {
|
|
18
|
+
try {
|
|
19
|
+
fs_1.default.rmSync(this.outputDir, { recursive: true, force: true });
|
|
20
|
+
console.log(`๐งน Cleaned old report data at: ${this.outputDir}`);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
console.warn(`โ ๏ธ Could not fully clean report dir: ${err.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!fs_1.default.existsSync(this.outputDir)) {
|
|
27
|
+
fs_1.default.mkdirSync(this.outputDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
this.startTime = new Date();
|
|
30
|
+
console.log(`๐ Arcality Mission started at ${this.startTime.toLocaleTimeString()}`);
|
|
54
31
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
32
|
+
onTestEnd(test, result) {
|
|
33
|
+
// Collect steps with recursion
|
|
34
|
+
const stepsHtml = result.steps.map(step => this.renderStep(step, 0)).join('');
|
|
35
|
+
// Handle attachments: copy to report dir
|
|
36
|
+
const processedAttachments = result.attachments.map(att => {
|
|
37
|
+
let extension = '.png';
|
|
38
|
+
if (att.contentType === 'application/json')
|
|
39
|
+
extension = '.json';
|
|
40
|
+
else if (att.contentType === 'text/plain')
|
|
41
|
+
extension = '.txt';
|
|
42
|
+
else if (att.contentType === 'text/yaml')
|
|
43
|
+
extension = '.yaml';
|
|
44
|
+
const fileName = att.path ? path_1.default.basename(att.path) : `${att.name || 'attachment'}-${Date.now()}${extension}`;
|
|
45
|
+
const attachmentsDir = path_1.default.join(this.outputDir, 'attachments');
|
|
46
|
+
const destPath = path_1.default.join(attachmentsDir, fileName);
|
|
47
|
+
if (!fs_1.default.existsSync(attachmentsDir)) {
|
|
48
|
+
fs_1.default.mkdirSync(attachmentsDir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
if (att.path && fs_1.default.existsSync(att.path)) {
|
|
52
|
+
// Si ya existe el archivo fรญsico, lo copiamos
|
|
53
|
+
fs_1.default.copyFileSync(att.path, destPath);
|
|
54
|
+
}
|
|
55
|
+
else if (att.body) {
|
|
56
|
+
// Si viene como Buffer (evidencia del agente), lo guardamos
|
|
57
|
+
fs_1.default.writeFileSync(destPath, att.body);
|
|
58
|
+
}
|
|
59
|
+
return { ...att, path: `attachments/${fileName}` };
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
console.error(`Failed to process attachment ${att.name}:`, e);
|
|
63
|
+
return att;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
if (result.status === 'failed' || result.error) {
|
|
67
|
+
const isAppBug = result.error?.message?.includes('APPLICATION BUG');
|
|
68
|
+
const prefix = isAppBug ? '๐ [APPLICATION BUG FOUND]' : 'โ [TEST ENGINE FAILURE]';
|
|
69
|
+
console.log(`\n${prefix} ${test.title} failed:`);
|
|
70
|
+
console.log(`${result.error?.message || result.error?.stack || 'Unknown error'}\n`);
|
|
71
|
+
if (result.error?.stack && !isAppBug) {
|
|
72
|
+
console.log(result.error.stack);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.results.push({ test, result, stepsHtml, attachments: processedAttachments });
|
|
94
76
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
import_fs.default.writeFileSync(reportPath, htmlContent);
|
|
102
|
-
console.log(`\u2728 Arcality Mission Report generated at: ${reportPath}`);
|
|
103
|
-
}
|
|
104
|
-
renderStep(step, depth) {
|
|
105
|
-
if (step.category === "hook" || step.category === "fixture")
|
|
106
|
-
return "";
|
|
107
|
-
const padding = depth * 16;
|
|
108
|
-
const statusClass = step.error ? "step-error" : "step-passed";
|
|
109
|
-
const icon = step.error ? "\u2715" : "\u2713";
|
|
110
|
-
const duration = step.duration >= 0 ? `<span class="step-duration">${step.duration}ms</span>` : "";
|
|
111
|
-
let title = this.escapeHtml(step.title);
|
|
112
|
-
title = title.replace(/^(click|fill|navigate|wait|expect|goto|press)/i, '<span class="step-action">$1</span>');
|
|
113
|
-
let children = "";
|
|
114
|
-
if (step.steps && step.steps.length > 0) {
|
|
115
|
-
children = `<div class="step-children">${step.steps.map((s) => this.renderStep(s, depth + 1)).join("")}</div>`;
|
|
77
|
+
async onEnd(result) {
|
|
78
|
+
this.totalDuration = result.duration;
|
|
79
|
+
const htmlContent = this.generateHtml();
|
|
80
|
+
const reportPath = path_1.default.join(this.outputDir, 'index.html');
|
|
81
|
+
fs_1.default.writeFileSync(reportPath, htmlContent);
|
|
82
|
+
console.log(`โจ Arcality Mission Report generated at: ${reportPath}`);
|
|
116
83
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
84
|
+
renderStep(step, depth) {
|
|
85
|
+
// Humanize: Hide internal Playwright hooks to only show Agent decisions
|
|
86
|
+
if (step.category === 'hook' || step.category === 'fixture')
|
|
87
|
+
return '';
|
|
88
|
+
const padding = depth * 16;
|
|
89
|
+
const statusClass = step.error ? 'step-error' : 'step-passed';
|
|
90
|
+
const icon = step.error ? 'โ' : 'โ';
|
|
91
|
+
const duration = step.duration >= 0 ? `<span class="step-duration">${step.duration}ms</span>` : '';
|
|
92
|
+
let title = this.escapeHtml(step.title);
|
|
93
|
+
// Highlight common playwright actions
|
|
94
|
+
title = title.replace(/^(click|fill|navigate|wait|expect|goto|press)/i, '<span class="step-action">$1</span>');
|
|
95
|
+
let children = '';
|
|
96
|
+
if (step.steps && step.steps.length > 0) {
|
|
97
|
+
children = `<div class="step-children">${step.steps.map(s => this.renderStep(s, depth + 1)).join('')}</div>`;
|
|
98
|
+
}
|
|
99
|
+
const isExpandable = step.steps && step.steps.length > 0;
|
|
100
|
+
const expandableAttr = isExpandable ? 'data-expandable="true"' : '';
|
|
101
|
+
return `
|
|
120
102
|
<div class="step-wrapper ${statusClass}" style="margin-left: ${padding}px" ${expandableAttr}>
|
|
121
103
|
<div class="step-header">
|
|
122
104
|
<span class="step-icon">${icon}</span>
|
|
@@ -126,28 +108,29 @@ class ArcalityReporter {
|
|
|
126
108
|
${children}
|
|
127
109
|
</div>
|
|
128
110
|
`;
|
|
129
|
-
}
|
|
130
|
-
getEngineVersion() {
|
|
131
|
-
try {
|
|
132
|
-
const packageJsonPath = import_path.default.join(process.cwd(), "package.json");
|
|
133
|
-
if (import_fs.default.existsSync(packageJsonPath)) {
|
|
134
|
-
const pkg = JSON.parse(import_fs.default.readFileSync(packageJsonPath, "utf8"));
|
|
135
|
-
return pkg.version || "2.2.0";
|
|
136
|
-
}
|
|
137
|
-
} catch (e) {
|
|
138
|
-
console.error("Error reading package.json version:", e);
|
|
139
111
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
112
|
+
getEngineVersion() {
|
|
113
|
+
try {
|
|
114
|
+
const packageJsonPath = path_1.default.join(process.cwd(), 'package.json');
|
|
115
|
+
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
116
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
|
|
117
|
+
return pkg.version || '2.2.0';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
console.error('Error reading package.json version:', e);
|
|
122
|
+
}
|
|
123
|
+
return '2.2.0';
|
|
124
|
+
}
|
|
125
|
+
generateHtml() {
|
|
126
|
+
const stats = {
|
|
127
|
+
total: this.results.length,
|
|
128
|
+
passed: this.results.filter(r => r.result.status === 'passed').length,
|
|
129
|
+
failed: this.results.filter(r => r.result.status === 'failed' || r.result.status === 'timedOut').length,
|
|
130
|
+
skipped: this.results.filter(r => r.result.status === 'skipped').length,
|
|
131
|
+
flaky: this.results.filter(r => r.result.status === 'passed' && r.result.retry > 0).length
|
|
132
|
+
};
|
|
133
|
+
return `
|
|
151
134
|
<!DOCTYPE html>
|
|
152
135
|
<html lang="en">
|
|
153
136
|
<head>
|
|
@@ -290,7 +273,7 @@ class ArcalityReporter {
|
|
|
290
273
|
gap: 12px;
|
|
291
274
|
}
|
|
292
275
|
.success-block::before {
|
|
293
|
-
content: "
|
|
276
|
+
content: "โจ";
|
|
294
277
|
font-size: 1.4rem;
|
|
295
278
|
}
|
|
296
279
|
|
|
@@ -475,14 +458,14 @@ class ArcalityReporter {
|
|
|
475
458
|
<div class="stat-card"><span class="stat-value val-passed">${stats.passed}</span><span class="stat-label">Successful</span></div>
|
|
476
459
|
<div class="stat-card"><span class="stat-value val-failed">${stats.failed}</span><span class="stat-label">Fatal Errors</span></div>
|
|
477
460
|
<div class="stat-card"><span class="stat-value val-flaky">${stats.flaky}</span><span class="stat-label">Flaky Tests</span></div>
|
|
478
|
-
<div class="stat-card"><span class="stat-value val-total">${(this.totalDuration /
|
|
461
|
+
<div class="stat-card"><span class="stat-value val-total">${(this.totalDuration / 1000).toFixed(2)}s</span><span class="stat-label">Total Duration</span></div>
|
|
479
462
|
</div>
|
|
480
463
|
|
|
481
464
|
<div class="test-list">
|
|
482
465
|
${this.results.map((r, i) => {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
466
|
+
const statusClass = `status-${r.result.status}`;
|
|
467
|
+
const projectName = r.test.parent.project()?.name || 'default';
|
|
468
|
+
return `
|
|
486
469
|
<div class="test-entry ${statusClass}">
|
|
487
470
|
<div class="test-header" onclick="toggleDetails('details-${i}')">
|
|
488
471
|
<div class="test-info">
|
|
@@ -491,34 +474,34 @@ class ArcalityReporter {
|
|
|
491
474
|
<div class="test-name">${this.escapeHtml(r.test.title)}</div>
|
|
492
475
|
<div class="test-meta">
|
|
493
476
|
<span class="tag tag-project">${projectName}</span>
|
|
494
|
-
${r.result.retry > 0 ? `<span class="tag tag-retry">Retry #${r.result.retry}</span>` :
|
|
495
|
-
<span>${(r.result.duration /
|
|
477
|
+
${r.result.retry > 0 ? `<span class="tag tag-retry">Retry #${r.result.retry}</span>` : ''}
|
|
478
|
+
<span>${(r.result.duration / 1000).toFixed(2)}s</span>
|
|
496
479
|
</div>
|
|
497
480
|
</div>
|
|
498
481
|
</div>
|
|
499
482
|
<div style="display:flex; gap: 10px; align-items: center;">
|
|
500
|
-
<span style="opacity:0.5; font-size: 1.2rem"
|
|
483
|
+
<span style="opacity:0.5; font-size: 1.2rem">โผ</span>
|
|
501
484
|
</div>
|
|
502
485
|
</div>
|
|
503
486
|
|
|
504
487
|
<div class="test-details" id="details-${i}" style="display: none;">
|
|
505
|
-
${r.result.status ===
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
488
|
+
${r.result.status === 'passed' ? (() => {
|
|
489
|
+
const successSummary = r.attachments.find(a => a.name === 'success_summary');
|
|
490
|
+
const text = this.getAttachmentText(successSummary);
|
|
491
|
+
return text ? `<div class="success-block">${this.escapeHtml(text)}</div>` : '';
|
|
492
|
+
})() : ''}
|
|
510
493
|
|
|
511
494
|
${r.result.error ? `
|
|
512
495
|
<div class="steps-section">
|
|
513
496
|
<span class="section-title">Analysis of the Obstacle</span>
|
|
514
|
-
<div class="error-block">${this.escapeHtml(r.result.error.message ||
|
|
497
|
+
<div class="error-block">${this.escapeHtml(r.result.error.message || 'Unknown Failure')}</div>
|
|
515
498
|
</div>
|
|
516
|
-
` :
|
|
499
|
+
` : ''}
|
|
517
500
|
|
|
518
501
|
${this._renderSecuritySection(r.attachments, i)}
|
|
519
502
|
|
|
520
503
|
<div class="steps-section">
|
|
521
|
-
<span class="section-title"
|
|
504
|
+
<span class="section-title">๐ง Agent Cognitive Process</span>
|
|
522
505
|
<div class="steps-container">
|
|
523
506
|
${r.stepsHtml || '<p style="opacity:0.5">No steps recorded.</p>'}
|
|
524
507
|
</div>
|
|
@@ -528,36 +511,36 @@ class ArcalityReporter {
|
|
|
528
511
|
<div class="steps-section">
|
|
529
512
|
<span class="section-title">Visual Evidence</span>
|
|
530
513
|
<div class="attachments-grid">
|
|
531
|
-
${r.attachments.map(
|
|
532
|
-
|
|
533
|
-
|
|
514
|
+
${r.attachments.map(att => {
|
|
515
|
+
if (att.contentType.startsWith('image/')) {
|
|
516
|
+
return `
|
|
534
517
|
<div class="attachment-item">
|
|
535
518
|
<img src="${att.path}" alt="${att.name}" onclick="window.open('${att.path}')">
|
|
536
519
|
<a href="${att.path}" target="_blank" class="att-label">${att.name}</a>
|
|
537
520
|
</div>
|
|
538
521
|
`;
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
522
|
+
}
|
|
523
|
+
if (att.contentType.startsWith('video/')) {
|
|
524
|
+
return `
|
|
542
525
|
<div class="attachment-item">
|
|
543
526
|
<video controls src="${att.path}"></video>
|
|
544
527
|
<a href="${att.path}" target="_blank" class="att-label">${att.name}</a>
|
|
545
528
|
</div>
|
|
546
529
|
`;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
530
|
+
}
|
|
531
|
+
return '';
|
|
532
|
+
}).join('')}
|
|
550
533
|
</div>
|
|
551
534
|
</div>
|
|
552
|
-
` :
|
|
535
|
+
` : ''}
|
|
553
536
|
</div>
|
|
554
537
|
</div>
|
|
555
538
|
`;
|
|
556
|
-
|
|
539
|
+
}).join('')}
|
|
557
540
|
</div>
|
|
558
541
|
|
|
559
542
|
<footer>
|
|
560
|
-
ARCALITY ENGINE v${this.getEngineVersion()}
|
|
543
|
+
ARCALITY ENGINE v${this.getEngineVersion()} โข SYSTEM TIME [${new Date().toISOString()}] โข GENERATED FOR ARCADIAL
|
|
561
544
|
</footer>
|
|
562
545
|
</div>
|
|
563
546
|
|
|
@@ -579,78 +562,73 @@ class ArcalityReporter {
|
|
|
579
562
|
</body>
|
|
580
563
|
</html>
|
|
581
564
|
`;
|
|
582
|
-
}
|
|
583
|
-
getAttachmentText(att) {
|
|
584
|
-
if (!att)
|
|
585
|
-
return null;
|
|
586
|
-
if (att.body)
|
|
587
|
-
return att.body.toString();
|
|
588
|
-
if (att.path) {
|
|
589
|
-
const actualPath = import_path.default.isAbsolute(att.path) ? att.path : import_path.default.join(this.outputDir, att.path);
|
|
590
|
-
if (import_fs.default.existsSync(actualPath)) {
|
|
591
|
-
return import_fs.default.readFileSync(actualPath, "utf8");
|
|
592
|
-
}
|
|
593
565
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (Array.isArray(parsed))
|
|
605
|
-
vulnerabilities.push(...parsed);
|
|
606
|
-
} catch (e) {
|
|
566
|
+
getAttachmentText(att) {
|
|
567
|
+
if (!att)
|
|
568
|
+
return null;
|
|
569
|
+
if (att.body)
|
|
570
|
+
return att.body.toString();
|
|
571
|
+
if (att.path) {
|
|
572
|
+
const actualPath = path_1.default.isAbsolute(att.path) ? att.path : path_1.default.join(this.outputDir, att.path);
|
|
573
|
+
if (fs_1.default.existsSync(actualPath)) {
|
|
574
|
+
return fs_1.default.readFileSync(actualPath, 'utf8');
|
|
575
|
+
}
|
|
607
576
|
}
|
|
608
|
-
|
|
577
|
+
return null;
|
|
609
578
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
579
|
+
_renderSecuritySection(attachments, index) {
|
|
580
|
+
let vulnerabilities = [];
|
|
581
|
+
const reportAtt = attachments.find(a => a.name === 'security_report');
|
|
582
|
+
if (reportAtt) {
|
|
583
|
+
const text = this.getAttachmentText(reportAtt);
|
|
584
|
+
if (text) {
|
|
585
|
+
try {
|
|
586
|
+
const parsed = JSON.parse(text);
|
|
587
|
+
if (Array.isArray(parsed))
|
|
588
|
+
vulnerabilities.push(...parsed);
|
|
589
|
+
}
|
|
590
|
+
catch (e) { }
|
|
591
|
+
}
|
|
618
592
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
593
|
+
const findingAtts = attachments.filter(a => a.name === 'security_finding');
|
|
594
|
+
for (const findingAtt of findingAtts) {
|
|
595
|
+
const text = this.getAttachmentText(findingAtt);
|
|
596
|
+
if (text) {
|
|
597
|
+
try {
|
|
598
|
+
const parsed = JSON.parse(text);
|
|
599
|
+
vulnerabilities.push(parsed);
|
|
600
|
+
}
|
|
601
|
+
catch (e) { }
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (vulnerabilities.length === 0)
|
|
605
|
+
return '';
|
|
606
|
+
const severityOrder = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3, 'Info': 4 };
|
|
607
|
+
const severityCounts = { 'Critical': 0, 'High': 0, 'Medium': 0, 'Low': 0, 'Info': 0 };
|
|
608
|
+
let worstSeverity = 'Info';
|
|
609
|
+
vulnerabilities.forEach(v => {
|
|
610
|
+
const severity = v.severity;
|
|
611
|
+
if (severityCounts[severity] !== undefined) {
|
|
612
|
+
severityCounts[severity]++;
|
|
613
|
+
}
|
|
614
|
+
if (severityOrder[severity] < severityOrder[worstSeverity]) {
|
|
615
|
+
worstSeverity = severity;
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
const getSeverityColor = (sev) => {
|
|
619
|
+
switch (sev) {
|
|
620
|
+
case 'Critical': return 'var(--error)';
|
|
621
|
+
case 'High': return '#fb923c';
|
|
622
|
+
case 'Medium': return 'var(--warning)';
|
|
623
|
+
case 'Low': return '#38bdf8';
|
|
624
|
+
case 'Info': return '#94a3b8';
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
const riskColor = getSeverityColor(worstSeverity);
|
|
628
|
+
return `
|
|
651
629
|
<div class="security-section">
|
|
652
630
|
<div class="security-header" onclick="toggleDetails('security-details-${index}')">
|
|
653
|
-
<h3
|
|
631
|
+
<h3>๐ก๏ธ Security Assessment</h3>
|
|
654
632
|
<span class="security-risk-badge" style="background-color: ${riskColor};">${worstSeverity} Risk</span>
|
|
655
633
|
</div>
|
|
656
634
|
<div class="security-details" id="security-details-${index}" style="display: block;">
|
|
@@ -673,22 +651,22 @@ class ArcalityReporter {
|
|
|
673
651
|
</div>
|
|
674
652
|
</div>
|
|
675
653
|
<div class="vuln-list">
|
|
676
|
-
${vulnerabilities.map(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
654
|
+
${vulnerabilities.map(v => {
|
|
655
|
+
const sev = v.severity;
|
|
656
|
+
const cardColor = getSeverityColor(sev);
|
|
657
|
+
return `
|
|
680
658
|
<div class="vuln-card" style="border-left-color: ${cardColor};">
|
|
681
659
|
<div class="vuln-card-title">
|
|
682
660
|
<span class="tag" style="background-color: ${cardColor}20; color: ${cardColor};">${v.severity}</span>
|
|
683
|
-
<span>${this.escapeHtml(v.category)} ${v.subcategory ? `<span style="opacity:0.7"
|
|
661
|
+
<span>${this.escapeHtml(v.category)} ${v.subcategory ? `<span style="opacity:0.7">โ ${this.escapeHtml(v.subcategory)}</span>` : ''}</span>
|
|
684
662
|
</div>
|
|
685
663
|
<div style="font-size: 0.8rem; opacity: 0.8; margin-top: 5px; margin-bottom: 10px; display: flex; flex-wrap: wrap; gap: 12px;">
|
|
686
|
-
${v.type ? `<span><strong style="color:var(--accent);">Type:</strong> ${this.escapeHtml(v.type)}</span>` :
|
|
687
|
-
${v.confidence ? `<span><strong>Confidence:</strong> ${this.escapeHtml(v.confidence)}</span>` :
|
|
688
|
-
${v.exploitability ? `<span><strong>Exploitability:</strong> ${this.escapeHtml(v.exploitability)}</span>` :
|
|
664
|
+
${v.type ? `<span><strong style="color:var(--accent);">Type:</strong> ${this.escapeHtml(v.type)}</span>` : ''}
|
|
665
|
+
${v.confidence ? `<span><strong>Confidence:</strong> ${this.escapeHtml(v.confidence)}</span>` : ''}
|
|
666
|
+
${v.exploitability ? `<span><strong>Exploitability:</strong> ${this.escapeHtml(v.exploitability)}</span>` : ''}
|
|
689
667
|
</div>
|
|
690
668
|
<p class="vuln-card-desc">${this.escapeHtml(v.description)}</p>
|
|
691
|
-
${v.impact ? `<p class="vuln-card-impact" style="font-size: 0.85rem; color: #fb923c; margin-bottom: 10px;"><strong>Potential Impact:</strong> ${this.escapeHtml(v.impact)}</p>` :
|
|
669
|
+
${v.impact ? `<p class="vuln-card-impact" style="font-size: 0.85rem; color: #fb923c; margin-bottom: 10px;"><strong>Potential Impact:</strong> ${this.escapeHtml(v.impact)}</p>` : ''}
|
|
692
670
|
|
|
693
671
|
<div class="vuln-card-evidence">
|
|
694
672
|
<strong>Evidence:</strong>
|
|
@@ -699,31 +677,36 @@ class ArcalityReporter {
|
|
|
699
677
|
<div class="vuln-card-repro" style="margin: 10px 0; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 6px; border-left: 2px solid ${cardColor}80;">
|
|
700
678
|
<strong style="font-size: 0.85rem; color: var(--text-muted);">Reproduction Steps:</strong>
|
|
701
679
|
<ol style="margin: 5px 0 0 20px; font-size: 0.85rem; color: var(--text-main);">
|
|
702
|
-
${v.reproductionSteps.map((s) => `<li>${this.escapeHtml(s)}</li>`).join(
|
|
680
|
+
${v.reproductionSteps.map((s) => `<li>${this.escapeHtml(s)}</li>`).join('')}
|
|
703
681
|
</ol>
|
|
704
|
-
</div>` :
|
|
682
|
+
</div>` : ''}
|
|
705
683
|
|
|
706
684
|
<div class="vuln-card-remediation">
|
|
707
|
-
<strong
|
|
685
|
+
<strong>โ Remediation:</strong> ${this.escapeHtml(v.remediation)}
|
|
708
686
|
</div>
|
|
709
687
|
|
|
710
688
|
${v.classification ? `
|
|
711
689
|
<div style="font-size: 0.75rem; opacity: 0.5; margin-top: 12px; display: flex; gap: 15px; font-family: 'JetBrains Mono', monospace;">
|
|
712
|
-
${v.classification.owaspTop10 ? `<span>[OWASP: ${this.escapeHtml(v.classification.owaspTop10)}]</span>` :
|
|
713
|
-
${v.classification.cwe ? `<span>[CWE: ${this.escapeHtml(v.classification.cwe)}]</span>` :
|
|
714
|
-
</div>` :
|
|
690
|
+
${v.classification.owaspTop10 ? `<span>[OWASP: ${this.escapeHtml(v.classification.owaspTop10)}]</span>` : ''}
|
|
691
|
+
${v.classification.cwe ? `<span>[CWE: ${this.escapeHtml(v.classification.cwe)}]</span>` : ''}
|
|
692
|
+
</div>` : ''}
|
|
715
693
|
</div>
|
|
716
694
|
`;
|
|
717
|
-
|
|
695
|
+
}).join('')}
|
|
718
696
|
</div>
|
|
719
697
|
</div>
|
|
720
698
|
</div>
|
|
721
699
|
`;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
700
|
+
}
|
|
701
|
+
escapeHtml(unsafe) {
|
|
702
|
+
if (!unsafe)
|
|
703
|
+
return '';
|
|
704
|
+
return unsafe
|
|
705
|
+
.replace(/&/g, "&")
|
|
706
|
+
.replace(/</g, "<")
|
|
707
|
+
.replace(/>/g, ">")
|
|
708
|
+
.replace(/"/g, """)
|
|
709
|
+
.replace(/'/g, "'");
|
|
710
|
+
}
|
|
728
711
|
}
|
|
729
|
-
|
|
712
|
+
exports.default = ArcalityReporter;
|