@arcadialdev/arcality 2.4.24 → 2.4.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,7 +43,12 @@ class ArcalityReporter implements Reporter {
43
43
 
44
44
  // Handle attachments: copy to report dir
45
45
  const processedAttachments = result.attachments.map(att => {
46
- const fileName = att.path ? path.basename(att.path) : `${att.name || 'attachment'}-${Date.now()}.png`;
46
+ let extension = '.png';
47
+ if (att.contentType === 'application/json') extension = '.json';
48
+ else if (att.contentType === 'text/plain') extension = '.txt';
49
+ else if (att.contentType === 'text/yaml') extension = '.yaml';
50
+
51
+ const fileName = att.path ? path.basename(att.path) : `${att.name || 'attachment'}-${Date.now()}${extension}`;
47
52
  const attachmentsDir = path.join(this.outputDir, 'attachments');
48
53
  const destPath = path.join(attachmentsDir, fileName);
49
54
 
@@ -67,8 +72,10 @@ class ArcalityReporter implements Reporter {
67
72
  });
68
73
 
69
74
  if (result.status === 'failed' || result.error) {
70
- console.log(`\n❌ [TEST ENGINE FAILURE] ${test.title} failed:`);
71
- console.log(`${result.error?.message || result.error?.stack || 'Unknown error'}\n`);
75
+ console.log(`
76
+ [TEST ENGINE FAILURE] ${test.title} failed:`);
77
+ console.log(`${result.error?.message || result.error?.stack || 'Unknown error'}
78
+ `);
72
79
  if (result.error?.stack) {
73
80
  console.log(result.error.stack);
74
81
  }
@@ -361,6 +368,92 @@ class ArcalityReporter implements Reporter {
361
368
  .tag { padding: 4px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 800; text-transform: uppercase; }
362
369
  .tag-project { background: rgba(192, 132, 252, 0.2); color: var(--accent); }
363
370
  .tag-retry { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
371
+
372
+ /* SECURITY REPORT SECTION */
373
+ .security-section {
374
+ margin: 2rem 0;
375
+ background: var(--bg-card);
376
+ border-radius: 16px;
377
+ overflow: hidden;
378
+ border: 1px solid #4a4a4a
379
+ }
380
+ .security-header {
381
+ padding: 1rem 1.5rem;
382
+ cursor: pointer;
383
+ display: flex;
384
+ align-items: center;
385
+ gap: 12px;
386
+ background: linear-gradient(90deg, rgba(245, 158, 11, 0.1), rgba(245, 158, 11, 0.0) 70%);
387
+ }
388
+ .security-header:hover {
389
+ background: linear-gradient(90deg, rgba(245, 158, 11, 0.15), rgba(245, 158, 11, 0.05) 70%);
390
+ }
391
+ .security-header h3 {
392
+ margin: 0;
393
+ font-size: 1.2rem;
394
+ color: var(--warning);
395
+ font-weight: 700;
396
+ }
397
+ .security-risk-badge {
398
+ padding: 4px 10px;
399
+ border-radius: 6px;
400
+ font-size: 0.8rem;
401
+ font-weight: 800;
402
+ color: white;
403
+ margin-left: auto;
404
+ }
405
+ .security-details {
406
+ padding: 1.5rem;
407
+ background: #0c1322;
408
+ }
409
+ .security-summary {
410
+ display: grid;
411
+ grid-template-columns: repeat(4, 1fr);
412
+ gap: 1rem;
413
+ margin-bottom: 2rem;
414
+ padding-bottom: 1.5rem;
415
+ border-bottom: 1px solid var(--border);
416
+ text-align: center;
417
+ }
418
+ .summary-item .count {
419
+ font-size: 1.5rem;
420
+ font-weight: 800;
421
+ }
422
+ .summary-item .label {
423
+ font-size: 0.7rem;
424
+ text-transform: uppercase;
425
+ color: var(--text-muted);
426
+ }
427
+ .count-Critical { color: var(--error); }
428
+ .count-High { color: #fb923c; }
429
+ .count-Medium { color: var(--warning); }
430
+ .count-Low { color: #38bdf8; }
431
+
432
+ .vuln-list { display: flex; flex-direction: column; gap: 1rem; }
433
+ .vuln-card {
434
+ background: var(--bg-deep);
435
+ border: 1px solid var(--border);
436
+ border-left-width: 4px;
437
+ border-radius: 8px;
438
+ padding: 1.5rem;
439
+ }
440
+ .vuln-card-title { font-weight: 700; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 10px;}
441
+ .vuln-card-desc { color: var(--text-muted); font-size: 0.9rem; margin-bottom: 1rem; }
442
+ .vuln-card-evidence {
443
+ background: #020617;
444
+ padding: 0.75rem;
445
+ border-radius: 4px;
446
+ font-family: 'JetBrains Mono', monospace;
447
+ font-size: 0.8rem;
448
+ white-space: pre-wrap;
449
+ word-break: break-all;
450
+ margin-bottom: 1rem;
451
+ border: 1px solid var(--border);
452
+ max-height: 200px;
453
+ overflow-y: auto;
454
+ }
455
+ .vuln-card-remediation { font-size: 0.9rem; color: var(--success); }
456
+ pre { margin: 0; }
364
457
  </style>
365
458
  </head>
366
459
  <body>
@@ -389,12 +482,10 @@ class ArcalityReporter implements Reporter {
389
482
  ${this.results.map((r, i) => {
390
483
  const statusClass = `status-${r.result.status}`;
391
484
  const projectName = r.test.parent.project()?.name || 'default';
392
- const hasTrace = r.attachments.some(a => a.name === 'trace');
393
- const traceFile = r.attachments.find(a => a.name === 'trace')?.path;
394
-
485
+
395
486
  return `
396
487
  <div class="test-entry ${statusClass}">
397
- <div class="test-header" onclick="toggleDetails(${i})">
488
+ <div class="test-header" onclick="toggleDetails('details-${i}')">
398
489
  <div class="test-info">
399
490
  <span class="status-dot"></span>
400
491
  <div>
@@ -411,14 +502,12 @@ class ArcalityReporter implements Reporter {
411
502
  </div>
412
503
  </div>
413
504
 
414
- <div class="test-details" id="details-${i}">
505
+ <div class="test-details" id="details-${i}" style="display: none;">
415
506
  ${r.result.status === 'passed' ? (() => {
416
- const successSummary = r.attachments.find(a => a.name === 'success_summary');
417
- if (successSummary && successSummary.body) {
418
- return `<div class="success-block">${this.escapeHtml(successSummary.body.toString())}</div>`;
419
- }
420
- return '';
421
- })() : ''}
507
+ const successSummary = r.attachments.find(a => a.name === 'success_summary');
508
+ const text = this.getAttachmentText(successSummary);
509
+ return text ? `<div class="success-block">${this.escapeHtml(text)}</div>` : '';
510
+ })() : ''}
422
511
 
423
512
  ${r.result.error ? `
424
513
  <div class="steps-section">
@@ -426,6 +515,8 @@ class ArcalityReporter implements Reporter {
426
515
  <div class="error-block">${this.escapeHtml(r.result.error.message || 'Unknown Failure')}</div>
427
516
  </div>
428
517
  ` : ''}
518
+
519
+ ${this._renderSecuritySection(r.attachments, i)}
429
520
 
430
521
  <div class="steps-section">
431
522
  <span class="section-title">🧠 Agent Cognitive Process</span>
@@ -439,31 +530,31 @@ class ArcalityReporter implements Reporter {
439
530
  <span class="section-title">Visual Evidence</span>
440
531
  <div class="attachments-grid">
441
532
  ${r.attachments.map(att => {
442
- if (att.contentType.startsWith('image/')) {
443
- return `
533
+ if (att.contentType.startsWith('image/')) {
534
+ return `
444
535
  <div class="attachment-item">
445
536
  <img src="${att.path}" alt="${att.name}" onclick="window.open('${att.path}')">
446
537
  <a href="${att.path}" target="_blank" class="att-label">${att.name}</a>
447
538
  </div>
448
539
  `;
449
- }
450
- if (att.contentType.startsWith('video/')) {
451
- return `
540
+ }
541
+ if (att.contentType.startsWith('video/')) {
542
+ return `
452
543
  <div class="attachment-item">
453
544
  <video controls src="${att.path}"></video>
454
545
  <a href="${att.path}" target="_blank" class="att-label">${att.name}</a>
455
546
  </div>
456
547
  `;
457
- }
458
- return '';
459
- }).join('')}
548
+ }
549
+ return '';
550
+ }).join('')}
460
551
  </div>
461
552
  </div>
462
553
  ` : ''}
463
554
  </div>
464
555
  </div>
465
556
  `;
466
- }).join('')}
557
+ }).join('')}
467
558
  </div>
468
559
 
469
560
  <footer>
@@ -473,17 +564,161 @@ class ArcalityReporter implements Reporter {
473
564
 
474
565
  <script>
475
566
  function toggleDetails(id) {
476
- const el = document.getElementById('details-' + id);
477
- el.style.display = (el.style.display === 'none' || el.style.display === '') ? 'block' : 'none';
567
+ const el = document.getElementById(id);
568
+ if (el) {
569
+ const isSecurityDetails = id.startsWith('security-details');
570
+ if (isSecurityDetails) {
571
+ // Independent toggle for security section
572
+ el.style.display = (el.style.display === 'none') ? 'block' : 'none';
573
+ } else {
574
+ // Main test details toggle
575
+ el.style.display = (el.style.display === 'none' || el.style.display === '') ? 'block' : 'none';
576
+ }
577
+ }
478
578
  }
479
- // Collapse all by default
480
- document.querySelectorAll('.test-details').forEach(d => d.style.display = 'none');
481
579
  </script>
482
580
  </body>
483
581
  </html>
484
582
  `;
485
583
  }
486
584
 
585
+ private getAttachmentText(att: { body?: Buffer, path?: string } | undefined): string | null {
586
+ if (!att) return null;
587
+ if (att.body) return att.body.toString();
588
+ if (att.path) {
589
+ const actualPath = path.isAbsolute(att.path) ? att.path : path.join(this.outputDir, att.path);
590
+ if (fs.existsSync(actualPath)) {
591
+ return fs.readFileSync(actualPath, 'utf8');
592
+ }
593
+ }
594
+ return null;
595
+ }
596
+
597
+ private _renderSecuritySection(attachments: any[], index: number): string {
598
+ let vulnerabilities: any[] = [];
599
+
600
+ const reportAtt = attachments.find(a => a.name === 'security_report');
601
+ if (reportAtt) {
602
+ const text = this.getAttachmentText(reportAtt);
603
+ if (text) {
604
+ try {
605
+ const parsed = JSON.parse(text);
606
+ if (Array.isArray(parsed)) vulnerabilities.push(...parsed);
607
+ } catch (e) { }
608
+ }
609
+ }
610
+
611
+ const findingAtts = attachments.filter(a => a.name === 'security_finding');
612
+ for (const findingAtt of findingAtts) {
613
+ const text = this.getAttachmentText(findingAtt);
614
+ if (text) {
615
+ try {
616
+ const parsed = JSON.parse(text);
617
+ vulnerabilities.push(parsed);
618
+ } catch (e) { }
619
+ }
620
+ }
621
+
622
+ if (vulnerabilities.length === 0) return '';
623
+
624
+ const severityOrder = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3, 'Info': 4 };
625
+ type Severity = keyof typeof severityOrder;
626
+
627
+ const severityCounts: Record<Severity, number> = { 'Critical': 0, 'High': 0, 'Medium': 0, 'Low': 0, 'Info': 0 };
628
+ let worstSeverity: Severity = 'Info';
629
+
630
+ vulnerabilities.forEach(v => {
631
+ const severity = v.severity as Severity;
632
+ if (severityCounts[severity] !== undefined) {
633
+ severityCounts[severity]++;
634
+ }
635
+ if (severityOrder[severity] < severityOrder[worstSeverity]) {
636
+ worstSeverity = severity;
637
+ }
638
+ });
639
+
640
+ const riskColor = worstSeverity === 'Critical' ? 'var(--error)' :
641
+ worstSeverity === 'High' ? '#fb923c' :
642
+ worstSeverity === 'Medium' ? 'var(--warning)' :
643
+ '#38bdf8';
644
+
645
+ return `
646
+ <div class="security-section">
647
+ <div class="security-header" onclick="toggleDetails('security-details-${index}')">
648
+ <h3>🛡️ Security Assessment</h3>
649
+ <span class="security-risk-badge" style="background-color: ${riskColor};">${worstSeverity} Risk</span>
650
+ </div>
651
+ <div class="security-details" id="security-details-${index}" style="display: block;">
652
+ <div class="security-summary">
653
+ <div class="summary-item">
654
+ <div class="count count-Critical">${severityCounts.Critical}</div>
655
+ <div class="label">Critical</div>
656
+ </div>
657
+ <div class="summary-item">
658
+ <div class="count count-High">${severityCounts.High}</div>
659
+ <div class="label">High</div>
660
+ </div>
661
+ <div class="summary-item">
662
+ <div class="count count-Medium">${severityCounts.Medium}</div>
663
+ <div class="label">Medium</div>
664
+ </div>
665
+ <div class="summary-item">
666
+ <div class="count count-Low">${severityCounts.Low}</div>
667
+ <div class="label">Low</div>
668
+ </div>
669
+ </div>
670
+ <div class="vuln-list">
671
+ ${vulnerabilities.map(v => {
672
+ const sev = v.severity as Severity;
673
+ const cardColor = sev === 'Critical' ? 'var(--error)' :
674
+ sev === 'High' ? '#fb923c' :
675
+ sev === 'Medium' ? 'var(--warning)' :
676
+ '#38bdf8';
677
+ return `
678
+ <div class="vuln-card" style="border-left-color: ${cardColor};">
679
+ <div class="vuln-card-title">
680
+ <span class="tag" style="background-color: ${cardColor}20; color: ${cardColor};">${v.severity}</span>
681
+ <span>${this.escapeHtml(v.category)} ${v.subcategory ? `<span style="opacity:0.7">— ${this.escapeHtml(v.subcategory)}</span>` : ''}</span>
682
+ </div>
683
+ <div style="font-size: 0.8rem; opacity: 0.8; margin-top: 5px; margin-bottom: 10px; display: flex; flex-wrap: wrap; gap: 12px;">
684
+ ${v.type ? `<span><strong style="color:var(--accent);">Type:</strong> ${this.escapeHtml(v.type)}</span>` : ''}
685
+ ${v.confidence ? `<span><strong>Confidence:</strong> ${this.escapeHtml(v.confidence)}</span>` : ''}
686
+ ${v.exploitability ? `<span><strong>Exploitability:</strong> ${this.escapeHtml(v.exploitability)}</span>` : ''}
687
+ </div>
688
+ <p class="vuln-card-desc">${this.escapeHtml(v.description)}</p>
689
+ ${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>` : ''}
690
+
691
+ <div class="vuln-card-evidence">
692
+ <strong>Evidence:</strong>
693
+ <pre>${this.escapeHtml(JSON.stringify(v.evidence, null, 2))}</pre>
694
+ </div>
695
+
696
+ ${v.reproductionSteps && v.reproductionSteps.length > 0 ? `
697
+ <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;">
698
+ <strong style="font-size: 0.85rem; color: var(--text-muted);">Reproduction Steps:</strong>
699
+ <ol style="margin: 5px 0 0 20px; font-size: 0.85rem; color: var(--text-main);">
700
+ ${v.reproductionSteps.map((s: string) => `<li>${this.escapeHtml(s)}</li>`).join('')}
701
+ </ol>
702
+ </div>` : ''}
703
+
704
+ <div class="vuln-card-remediation">
705
+ <strong>→ Remediation:</strong> ${this.escapeHtml(v.remediation)}
706
+ </div>
707
+
708
+ ${v.classification ? `
709
+ <div style="font-size: 0.75rem; opacity: 0.5; margin-top: 12px; display: flex; gap: 15px; font-family: 'JetBrains Mono', monospace;">
710
+ ${v.classification.owaspTop10 ? `<span>[OWASP: ${this.escapeHtml(v.classification.owaspTop10)}]</span>` : ''}
711
+ ${v.classification.cwe ? `<span>[CWE: ${this.escapeHtml(v.classification.cwe)}]</span>` : ''}
712
+ </div>` : ''}
713
+ </div>
714
+ `;
715
+ }).join('')}
716
+ </div>
717
+ </div>
718
+ </div>
719
+ `;
720
+ }
721
+
487
722
  private escapeHtml(unsafe: string | undefined): string {
488
723
  if (!unsafe) return '';
489
724
  return unsafe