hackmyagent 0.3.2 → 0.3.4

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/index.js CHANGED
@@ -30,7 +30,7 @@ HackMyAgent helps you secure AI agent deployments with 100+ security checks
30
30
  across credential exposure, MCP configurations, prompt injection defenses,
31
31
  and infrastructure hardening.
32
32
 
33
- Documentation: https://github.com/ecolibria/hackmyagent
33
+ Documentation: https://github.com/opena2a-org/hackmyagent
34
34
 
35
35
  Examples:
36
36
  $ hackmyagent check @anthropic/claude-mcp Verify skill before installing
@@ -401,116 +401,697 @@ function generateHtmlReport(result) {
401
401
  'Passing': '#eab308',
402
402
  'Needs Improvement': '#f97316',
403
403
  'Failing': '#ef4444',
404
- }[result.rating];
405
- const categoryRows = result.categories.map(cat => {
406
- const statusIcon = cat.failed === 0 ? '' : cat.passed > 0 ? '⚠️' : '❌';
407
- const barWidth = cat.compliance;
404
+ }[result.rating] || '#94a3b8';
405
+ const ratingBg = {
406
+ 'Certified': 'rgba(34, 197, 94, 0.15)',
407
+ 'Compliant': 'rgba(34, 197, 94, 0.15)',
408
+ 'Passing': 'rgba(234, 179, 8, 0.15)',
409
+ 'Needs Improvement': 'rgba(249, 115, 22, 0.15)',
410
+ 'Failing': 'rgba(239, 68, 68, 0.15)',
411
+ }[result.rating] || 'rgba(148, 163, 184, 0.15)';
412
+ // Generate donut chart SVG
413
+ const donutRadius = 70;
414
+ const donutStroke = 14;
415
+ const donutCircumference = 2 * Math.PI * donutRadius;
416
+ const donutOffset = donutCircumference * (1 - result.compliance / 100);
417
+ const complianceColor = result.compliance >= 90 ? '#22c55e' : result.compliance >= 70 ? '#eab308' : '#ef4444';
418
+ // Generate radar chart data points
419
+ const radarCategories = result.categories.slice(0, 10); // Max 10 for radar
420
+ const radarPoints = [];
421
+ const radarLabels = [];
422
+ const radarCenter = 120;
423
+ const radarRadius = 90;
424
+ // Category name abbreviations for radar chart labels
425
+ const categoryAbbreviations = {
426
+ 'Identity & Provenance': 'Identity',
427
+ 'Capability & Authorization': 'Capability',
428
+ 'Input Security': 'Input',
429
+ 'Output Security': 'Output',
430
+ 'Credential Protection': 'Credentials',
431
+ 'Supply Chain Integrity': 'Supply Chain',
432
+ 'Agent-to-Agent Security': 'A2A Security',
433
+ 'Memory & Context Integrity': 'Memory',
434
+ 'Operational Security': 'Operations',
435
+ 'Monitoring & Response': 'Monitoring',
436
+ };
437
+ radarCategories.forEach((cat, i) => {
438
+ const angle = (Math.PI * 2 * i) / radarCategories.length - Math.PI / 2;
439
+ // Use minimum 5% so 0% categories still show on the chart edge (not at center)
440
+ const value = Math.max(0.05, cat.compliance / 100);
441
+ const x = radarCenter + Math.cos(angle) * radarRadius * value;
442
+ const y = radarCenter + Math.sin(angle) * radarRadius * value;
443
+ radarPoints.push(`${x},${y}`);
444
+ // Label position (slightly outside)
445
+ const labelX = radarCenter + Math.cos(angle) * (radarRadius + 20);
446
+ const labelY = radarCenter + Math.sin(angle) * (radarRadius + 20);
447
+ const shortName = categoryAbbreviations[cat.category] || cat.category.split(' ')[0];
448
+ radarLabels.push(`<text x="${labelX}" y="${labelY}" text-anchor="middle" dominant-baseline="middle" fill="#94a3b8" font-size="10" font-weight="500">${escapeHtml(shortName)}</text>`);
449
+ });
450
+ // Generate radar grid lines
451
+ const radarGrid = [0.25, 0.5, 0.75, 1].map(scale => {
452
+ const points = radarCategories.map((_, i) => {
453
+ const angle = (Math.PI * 2 * i) / radarCategories.length - Math.PI / 2;
454
+ const x = radarCenter + Math.cos(angle) * radarRadius * scale;
455
+ const y = radarCenter + Math.sin(angle) * radarRadius * scale;
456
+ return `${x},${y}`;
457
+ }).join(' ');
458
+ return `<polygon points="${points}" fill="none" stroke="#334155" stroke-width="1"/>`;
459
+ }).join('');
460
+ // Radar axis lines
461
+ const radarAxes = radarCategories.map((_, i) => {
462
+ const angle = (Math.PI * 2 * i) / radarCategories.length - Math.PI / 2;
463
+ const x = radarCenter + Math.cos(angle) * radarRadius;
464
+ const y = radarCenter + Math.sin(angle) * radarRadius;
465
+ return `<line x1="${radarCenter}" y1="${radarCenter}" x2="${x}" y2="${y}" stroke="#334155" stroke-width="1"/>`;
466
+ }).join('');
467
+ // Collect all controls for statistics
468
+ const allControls = result.categories.flatMap(cat => cat.controls);
469
+ const failedControls = allControls.filter(ctrl => ctrl.status === 'failed');
470
+ const passedControls = allControls.filter(ctrl => ctrl.status === 'passed');
471
+ const unverifiedControls = allControls.filter(ctrl => ctrl.status === 'unverified');
472
+ // Level breakdown stats
473
+ const levelStats = {
474
+ L1: { passed: 0, failed: 0, total: 0 },
475
+ L2: { passed: 0, failed: 0, total: 0 },
476
+ L3: { passed: 0, failed: 0, total: 0 },
477
+ };
478
+ allControls.forEach(ctrl => {
479
+ const lvl = ctrl.level;
480
+ if (levelStats[lvl]) {
481
+ levelStats[lvl].total++;
482
+ if (ctrl.status === 'passed')
483
+ levelStats[lvl].passed++;
484
+ if (ctrl.status === 'failed')
485
+ levelStats[lvl].failed++;
486
+ }
487
+ });
488
+ // Find worst category
489
+ const worstCategory = result.categories
490
+ .filter(cat => cat.passed + cat.failed > 0)
491
+ .sort((a, b) => a.compliance - b.compliance)[0];
492
+ // Security grade based on compliance
493
+ const getGrade = (pct) => {
494
+ if (pct >= 95)
495
+ return { letter: 'A+', color: '#22c55e' };
496
+ if (pct >= 90)
497
+ return { letter: 'A', color: '#22c55e' };
498
+ if (pct >= 85)
499
+ return { letter: 'B+', color: '#84cc16' };
500
+ if (pct >= 80)
501
+ return { letter: 'B', color: '#84cc16' };
502
+ if (pct >= 75)
503
+ return { letter: 'C+', color: '#eab308' };
504
+ if (pct >= 70)
505
+ return { letter: 'C', color: '#eab308' };
506
+ if (pct >= 60)
507
+ return { letter: 'D', color: '#f97316' };
508
+ return { letter: 'F', color: '#ef4444' };
509
+ };
510
+ const grade = getGrade(result.compliance);
511
+ // Generate executive summary items
512
+ const executiveSummary = failedControls.length === 0
513
+ ? '<div class="exec-item success"><span class="exec-icon">✓</span><span>All controls passing at this level</span></div>'
514
+ : failedControls.slice(0, 5).map(ctrl => `<div class="exec-item critical"><span class="exec-icon">!</span><span><strong>${ctrl.controlId}</strong>: ${escapeHtml(ctrl.name)}</span></div>`).join('') + (failedControls.length > 5 ? `<div class="exec-item warning"><span class="exec-icon">+</span><span>${failedControls.length - 5} more issues not shown</span></div>` : '');
515
+ // SVG icons for professional look (no emojis)
516
+ const icons = {
517
+ check: '<svg class="icon icon-check" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>',
518
+ x: '<svg class="icon icon-x" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>',
519
+ warning: '<svg class="icon icon-warning" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>',
520
+ circle: '<svg class="icon icon-circle" viewBox="0 0 20 20" fill="currentColor"><circle cx="10" cy="10" r="4"/></svg>',
521
+ shield: '<svg class="icon icon-shield" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>',
522
+ print: '<svg class="icon icon-print" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5 4v3H4a2 2 0 00-2 2v3a2 2 0 002 2h1v2a2 2 0 002 2h6a2 2 0 002-2v-2h1a2 2 0 002-2V9a2 2 0 00-2-2h-1V4a2 2 0 00-2-2H7a2 2 0 00-2 2zm8 0H7v3h6V4zm0 8H7v4h6v-4z" clip-rule="evenodd"/></svg>',
523
+ };
524
+ // Category rows with collapsible sections
525
+ const categoryRows = result.categories.map((cat, catIndex) => {
526
+ const statusIcon = cat.failed === 0 ? icons.check : cat.passed > 0 ? icons.warning : icons.x;
527
+ const statusClass = cat.failed === 0 ? 'status-pass' : cat.passed > 0 ? 'status-warn' : 'status-fail';
408
528
  const barColor = cat.compliance >= 90 ? '#22c55e' : cat.compliance >= 70 ? '#eab308' : '#ef4444';
409
529
  const controlRows = cat.controls.map(ctrl => {
410
- const statusEmoji = ctrl.status === 'passed' ? '✅' : ctrl.status === 'failed' ? '❌' : '⚪';
530
+ const statusSvg = ctrl.status === 'passed' ? icons.check : ctrl.status === 'failed' ? icons.x : icons.circle;
531
+ const ctrlStatusClass = ctrl.status === 'passed' ? 'status-pass' : ctrl.status === 'failed' ? 'status-fail' : 'status-unverified';
411
532
  const findingsList = ctrl.findings.length > 0
412
533
  ? `<ul class="findings">${ctrl.findings.map(f => `<li>${escapeHtml(f)}</li>`).join('')}</ul>`
413
534
  : '';
414
535
  const remediation = ctrl.remediation
415
- ? `<div class="remediation"><strong>Fix:</strong> ${escapeHtml(ctrl.remediation)}</div>`
536
+ ? `<div class="remediation"><strong>Remediation:</strong> ${escapeHtml(ctrl.remediation)}</div>`
416
537
  : '';
417
538
  return `
418
539
  <tr class="control-row ${ctrl.status}">
419
- <td>${statusEmoji}</td>
420
- <td>${ctrl.controlId}</td>
421
- <td>${escapeHtml(ctrl.name)}</td>
422
- <td><span class="level-badge level-${ctrl.level.toLowerCase()}">${ctrl.level}</span></td>
423
- <td>${findingsList}${remediation}</td>
540
+ <td class="status-cell"><span class="${ctrlStatusClass}">${statusSvg}</span></td>
541
+ <td class="id-cell"><code>${ctrl.controlId}</code></td>
542
+ <td class="name-cell">${escapeHtml(ctrl.name)}</td>
543
+ <td class="level-cell"><span class="level-badge level-${ctrl.level.toLowerCase()}">${ctrl.level}</span></td>
544
+ <td class="details-cell">${findingsList}${remediation}</td>
424
545
  </tr>`;
425
546
  }).join('');
426
547
  return `
427
- <div class="category">
428
- <div class="category-header">
429
- <span class="category-icon">${statusIcon}</span>
548
+ <div class="category" id="cat-${catIndex}">
549
+ <div class="category-header" onclick="toggleCategory(${catIndex})">
550
+ <span class="category-icon ${statusClass}">${statusIcon}</span>
430
551
  <span class="category-name">${escapeHtml(cat.category)}</span>
431
- <span class="category-score">${cat.passed}/${cat.passed + cat.failed} (${cat.compliance}%)</span>
552
+ <div class="category-meta">
553
+ <span class="category-score">${cat.passed}/${cat.passed + cat.failed}</span>
554
+ <div class="mini-bar"><div class="mini-fill" style="width: ${cat.compliance}%; background: ${barColor};"></div></div>
555
+ <span class="category-percent">${cat.compliance}%</span>
556
+ <span class="chevron">▼</span>
557
+ </div>
432
558
  </div>
433
- <div class="progress-bar">
434
- <div class="progress-fill" style="width: ${barWidth}%; background: ${barColor};"></div>
559
+ <div class="category-content">
560
+ <table class="controls-table">
561
+ <thead><tr><th></th><th>Control ID</th><th>Control Name</th><th>Level</th><th>Details</th></tr></thead>
562
+ <tbody>${controlRows}</tbody>
563
+ </table>
435
564
  </div>
436
- <table class="controls-table">
437
- <thead><tr><th></th><th>ID</th><th>Control</th><th>Level</th><th>Details</th></tr></thead>
438
- <tbody>${controlRows}</tbody>
439
- </table>
440
565
  </div>`;
441
566
  }).join('');
567
+ // Level description
568
+ const levelDesc = {
569
+ 'L1': 'Essential baseline security every agent should implement',
570
+ 'L2': 'Defense-in-depth for production systems',
571
+ 'L3': 'Maximum security for high-risk or regulated environments'
572
+ }[result.level] || '';
442
573
  return `<!DOCTYPE html>
443
574
  <html lang="en">
444
575
  <head>
445
576
  <meta charset="UTF-8">
446
577
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
447
- <title>OASB-1 Compliance Report</title>
578
+ <title>OASB-1 Compliance Report | ${result.rating}</title>
448
579
  <style>
580
+ :root {
581
+ --bg-primary: #0a0f1a;
582
+ --bg-secondary: #111827;
583
+ --bg-tertiary: #1f2937;
584
+ --text-primary: #f1f5f9;
585
+ --text-secondary: #94a3b8;
586
+ --text-muted: #64748b;
587
+ --border: #334155;
588
+ --accent: #3b82f6;
589
+ --success: #22c55e;
590
+ --warning: #eab308;
591
+ --danger: #ef4444;
592
+ }
449
593
  * { box-sizing: border-box; margin: 0; padding: 0; }
450
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; line-height: 1.6; padding: 2rem; }
451
- .container { max-width: 1200px; margin: 0 auto; }
452
- .header { text-align: center; margin-bottom: 2rem; padding: 2rem; background: #1e293b; border-radius: 12px; }
453
- .header h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
454
- .header .version { color: #94a3b8; font-size: 0.875rem; }
455
- .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
456
- .summary-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; text-align: center; }
457
- .summary-card .value { font-size: 2rem; font-weight: bold; }
458
- .summary-card .label { color: #94a3b8; font-size: 0.875rem; }
459
- .rating { color: ${ratingColor}; }
460
- .category { background: #1e293b; border-radius: 8px; margin-bottom: 1rem; overflow: hidden; }
461
- .category-header { display: flex; align-items: center; gap: 0.75rem; padding: 1rem; font-weight: 600; }
462
- .category-icon { font-size: 1.25rem; }
463
- .category-name { flex: 1; }
464
- .category-score { color: #94a3b8; }
465
- .progress-bar { height: 4px; background: #334155; }
466
- .progress-fill { height: 100%; transition: width 0.3s; }
467
- .controls-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
468
- .controls-table th, .controls-table td { padding: 0.75rem 1rem; text-align: left; border-top: 1px solid #334155; }
469
- .controls-table th { background: #0f172a; color: #94a3b8; font-weight: 500; }
470
- .control-row.failed { background: rgba(239, 68, 68, 0.1); }
471
- .control-row.unverified { opacity: 0.6; }
472
- .level-badge { padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; }
594
+ body {
595
+ font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
596
+ background: var(--bg-primary);
597
+ color: var(--text-primary);
598
+ line-height: 1.6;
599
+ padding: 2rem;
600
+ font-size: 14px;
601
+ }
602
+ .container { max-width: 1400px; margin: 0 auto; }
603
+
604
+ /* Header */
605
+ .header {
606
+ display: flex;
607
+ justify-content: space-between;
608
+ align-items: center;
609
+ margin-bottom: 2rem;
610
+ padding: 1.5rem 2rem;
611
+ background: var(--bg-secondary);
612
+ border-radius: 12px;
613
+ border: 1px solid var(--border);
614
+ }
615
+ .header-left h1 {
616
+ font-size: 1.5rem;
617
+ font-weight: 700;
618
+ display: flex;
619
+ align-items: center;
620
+ gap: 0.75rem;
621
+ }
622
+ .header-left .meta { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.25rem; }
623
+ .header-icon { display: inline-flex; margin-right: 0.5rem; }
624
+ .header-icon .icon { width: 24px; height: 24px; color: var(--accent); }
625
+ .header-right { display: flex; align-items: center; gap: 1rem; }
626
+ .rating-badge {
627
+ display: inline-block;
628
+ padding: 0.375rem 1rem;
629
+ border-radius: 6px;
630
+ font-weight: 600;
631
+ font-size: 0.875rem;
632
+ background: ${ratingBg};
633
+ color: ${ratingColor};
634
+ border: 1px solid ${ratingColor}40;
635
+ }
636
+ .level-tag {
637
+ display: inline-block;
638
+ padding: 0.375rem 1rem;
639
+ background: var(--accent);
640
+ color: white;
641
+ border-radius: 6px;
642
+ font-size: 0.875rem;
643
+ font-weight: 600;
644
+ }
645
+
646
+ /* SVG Icons */
647
+ .icon { width: 16px; height: 16px; display: inline-block; vertical-align: middle; }
648
+ .status-pass { color: var(--success); }
649
+ .status-fail { color: var(--danger); }
650
+ .status-warn { color: var(--warning); }
651
+ .status-unverified { color: var(--text-muted); }
652
+ .category-icon { display: flex; align-items: center; }
653
+ .category-icon .icon { width: 18px; height: 18px; }
654
+ .footer-btn .icon { width: 14px; height: 14px; margin-right: 0.375rem; }
655
+
656
+ /* Dashboard grid */
657
+ .dashboard {
658
+ display: grid;
659
+ grid-template-columns: 280px 1fr 300px;
660
+ gap: 1.5rem;
661
+ margin-bottom: 2rem;
662
+ }
663
+ @media (max-width: 1200px) {
664
+ .dashboard { grid-template-columns: 1fr 1fr; }
665
+ .radar-section { grid-column: span 2; }
666
+ }
667
+ @media (max-width: 768px) {
668
+ .dashboard { grid-template-columns: 1fr; }
669
+ .radar-section { grid-column: span 1; }
670
+ }
671
+
672
+ /* Score card - Prowler style */
673
+ .score-card {
674
+ background: var(--bg-secondary);
675
+ border-radius: 12px;
676
+ padding: 1.25rem;
677
+ border: 1px solid var(--border);
678
+ }
679
+ .score-header {
680
+ display: flex;
681
+ align-items: center;
682
+ gap: 1rem;
683
+ margin-bottom: 1.25rem;
684
+ padding-bottom: 1rem;
685
+ border-bottom: 1px solid var(--border);
686
+ }
687
+ .score-grade {
688
+ width: 56px;
689
+ height: 56px;
690
+ border-radius: 12px;
691
+ border: 2px solid;
692
+ display: flex;
693
+ align-items: center;
694
+ justify-content: center;
695
+ }
696
+ .grade-letter { font-size: 1.75rem; font-weight: 800; }
697
+ .score-main { flex: 1; }
698
+ .score-pct { font-size: 2rem; font-weight: 700; color: var(--text-primary); line-height: 1; }
699
+ .score-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.25rem; }
700
+
701
+ .score-bars { margin-bottom: 1rem; }
702
+ .score-bar-row {
703
+ display: flex;
704
+ align-items: center;
705
+ gap: 0.75rem;
706
+ margin-bottom: 0.5rem;
707
+ }
708
+ .bar-label { width: 50px; font-size: 0.75rem; color: var(--text-secondary); }
709
+ .bar-track { flex: 1; height: 8px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden; }
710
+ .bar-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
711
+ .bar-pass { background: var(--success); }
712
+ .bar-fail { background: var(--danger); }
713
+ .bar-manual { background: var(--text-muted); }
714
+ .bar-count { width: 24px; font-size: 0.8rem; font-weight: 600; text-align: right; color: var(--text-primary); }
715
+
716
+ .level-breakdown {
717
+ display: flex;
718
+ gap: 0.75rem;
719
+ padding: 0.75rem;
720
+ background: var(--bg-tertiary);
721
+ border-radius: 8px;
722
+ margin-bottom: 1rem;
723
+ }
724
+ .level-row { display: flex; align-items: center; gap: 0.375rem; }
725
+ .level-stat { font-size: 0.8rem; color: var(--text-secondary); }
726
+
727
+ .worst-category {
728
+ display: flex;
729
+ align-items: center;
730
+ gap: 0.5rem;
731
+ padding: 0.625rem 0.75rem;
732
+ background: rgba(239, 68, 68, 0.1);
733
+ border-radius: 6px;
734
+ border-left: 3px solid var(--danger);
735
+ }
736
+ .worst-label { font-size: 0.7rem; color: var(--danger); text-transform: uppercase; font-weight: 600; }
737
+ .worst-name { flex: 1; font-size: 0.8rem; color: var(--text-primary); }
738
+ .worst-pct { font-size: 0.85rem; font-weight: 700; }
739
+
740
+ /* Radar chart */
741
+ .radar-section {
742
+ background: var(--bg-secondary);
743
+ border-radius: 12px;
744
+ padding: 1.5rem;
745
+ border: 1px solid var(--border);
746
+ }
747
+ .radar-section h3 {
748
+ font-size: 0.85rem;
749
+ color: var(--text-secondary);
750
+ text-transform: uppercase;
751
+ letter-spacing: 0.05em;
752
+ margin-bottom: 1rem;
753
+ }
754
+ .radar-container { display: flex; justify-content: center; }
755
+
756
+ /* Executive summary */
757
+ .exec-section {
758
+ background: var(--bg-secondary);
759
+ border-radius: 12px;
760
+ padding: 1.5rem;
761
+ border: 1px solid var(--border);
762
+ }
763
+ .exec-section h3 {
764
+ font-size: 0.85rem;
765
+ color: var(--text-secondary);
766
+ text-transform: uppercase;
767
+ letter-spacing: 0.05em;
768
+ margin-bottom: 1rem;
769
+ }
770
+ .exec-item {
771
+ display: flex;
772
+ align-items: flex-start;
773
+ gap: 0.75rem;
774
+ padding: 0.75rem;
775
+ margin-bottom: 0.5rem;
776
+ border-radius: 6px;
777
+ font-size: 0.85rem;
778
+ }
779
+ .exec-item.critical { background: rgba(239, 68, 68, 0.1); border-left: 3px solid var(--danger); }
780
+ .exec-item.warning { background: rgba(234, 179, 8, 0.1); border-left: 3px solid var(--warning); }
781
+ .exec-item.success { background: rgba(34, 197, 94, 0.1); border-left: 3px solid var(--success); }
782
+ .exec-icon {
783
+ width: 20px;
784
+ height: 20px;
785
+ border-radius: 50%;
786
+ display: flex;
787
+ align-items: center;
788
+ justify-content: center;
789
+ font-weight: 700;
790
+ font-size: 0.75rem;
791
+ flex-shrink: 0;
792
+ }
793
+ .exec-item.critical .exec-icon { background: var(--danger); color: white; }
794
+ .exec-item.warning .exec-icon { background: var(--warning); color: black; }
795
+ .exec-item.success .exec-icon { background: var(--success); color: white; }
796
+
797
+ /* Categories */
798
+ .categories-header {
799
+ display: flex;
800
+ justify-content: space-between;
801
+ align-items: center;
802
+ margin-bottom: 1rem;
803
+ }
804
+ .categories-header h2 { font-size: 1.1rem; }
805
+ .expand-all {
806
+ background: var(--bg-tertiary);
807
+ border: 1px solid var(--border);
808
+ color: var(--text-secondary);
809
+ padding: 0.5rem 1rem;
810
+ border-radius: 6px;
811
+ cursor: pointer;
812
+ font-size: 0.8rem;
813
+ }
814
+ .expand-all:hover { background: var(--border); }
815
+
816
+ .category {
817
+ background: var(--bg-secondary);
818
+ border-radius: 8px;
819
+ margin-bottom: 0.75rem;
820
+ border: 1px solid var(--border);
821
+ overflow: hidden;
822
+ }
823
+ .category-header {
824
+ display: flex;
825
+ align-items: center;
826
+ gap: 0.75rem;
827
+ padding: 1rem 1.25rem;
828
+ cursor: pointer;
829
+ transition: background 0.15s;
830
+ }
831
+ .category-header:hover { background: var(--bg-tertiary); }
832
+ .category-icon { font-size: 1.1rem; }
833
+ .category-name { flex: 1; font-weight: 500; }
834
+ .category-meta { display: flex; align-items: center; gap: 0.75rem; }
835
+ .category-score { color: var(--text-secondary); font-size: 0.85rem; font-weight: 500; }
836
+ .mini-bar { width: 60px; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
837
+ .mini-fill { height: 100%; border-radius: 3px; }
838
+ .category-percent { color: var(--text-muted); font-size: 0.85rem; width: 40px; text-align: right; }
839
+ .chevron {
840
+ color: var(--text-muted);
841
+ font-size: 0.7rem;
842
+ transition: transform 0.2s;
843
+ margin-left: 0.5rem;
844
+ }
845
+ .category.collapsed .chevron { transform: rotate(-90deg); }
846
+ .category.collapsed .category-content { display: none; }
847
+
848
+ .category-content { border-top: 1px solid var(--border); }
849
+ .controls-table { width: 100%; border-collapse: collapse; }
850
+ .controls-table th {
851
+ padding: 0.75rem 1rem;
852
+ text-align: left;
853
+ background: var(--bg-primary);
854
+ color: var(--text-muted);
855
+ font-weight: 500;
856
+ font-size: 0.75rem;
857
+ text-transform: uppercase;
858
+ letter-spacing: 0.03em;
859
+ }
860
+ .controls-table td {
861
+ padding: 0.875rem 1rem;
862
+ border-top: 1px solid var(--border);
863
+ vertical-align: top;
864
+ }
865
+ .status-cell { width: 40px; text-align: center; }
866
+ .id-cell { width: 100px; }
867
+ .id-cell code {
868
+ background: var(--bg-tertiary);
869
+ padding: 0.2rem 0.5rem;
870
+ border-radius: 4px;
871
+ font-size: 0.8rem;
872
+ color: var(--accent);
873
+ }
874
+ .name-cell { width: 30%; }
875
+ .level-cell { width: 60px; }
876
+ .details-cell { color: var(--text-secondary); font-size: 0.85rem; }
877
+ .control-row.failed { background: rgba(239, 68, 68, 0.05); }
878
+ .control-row.unverified { opacity: 0.5; }
879
+
880
+ .level-badge {
881
+ padding: 0.2rem 0.6rem;
882
+ border-radius: 4px;
883
+ font-size: 0.7rem;
884
+ font-weight: 600;
885
+ text-transform: uppercase;
886
+ }
473
887
  .level-l1 { background: #7c3aed; color: white; }
474
888
  .level-l2 { background: #2563eb; color: white; }
475
889
  .level-l3 { background: #059669; color: white; }
476
- .findings { margin: 0.5rem 0; padding-left: 1.25rem; color: #f87171; }
477
- .remediation { margin-top: 0.5rem; padding: 0.5rem; background: #334155; border-radius: 4px; font-size: 0.8rem; }
478
- .footer { text-align: center; margin-top: 2rem; color: #64748b; font-size: 0.875rem; }
890
+
891
+ .findings {
892
+ margin: 0.25rem 0 0.5rem;
893
+ padding-left: 1.25rem;
894
+ color: #f87171;
895
+ list-style-type: disc;
896
+ }
897
+ .findings li { margin-bottom: 0.25rem; }
898
+ .remediation {
899
+ margin-top: 0.5rem;
900
+ padding: 0.625rem 0.875rem;
901
+ background: var(--bg-tertiary);
902
+ border-radius: 6px;
903
+ font-size: 0.8rem;
904
+ border-left: 3px solid var(--accent);
905
+ }
906
+
907
+ /* Footer */
908
+ .footer {
909
+ display: flex;
910
+ justify-content: space-between;
911
+ align-items: center;
912
+ margin-top: 2rem;
913
+ padding: 1.5rem;
914
+ background: var(--bg-secondary);
915
+ border-radius: 12px;
916
+ border: 1px solid var(--border);
917
+ color: var(--text-muted);
918
+ font-size: 0.85rem;
919
+ }
920
+ .footer a { color: var(--accent); text-decoration: none; }
921
+ .footer a:hover { text-decoration: underline; }
922
+ .footer-actions { display: flex; gap: 1rem; }
923
+ .footer-btn {
924
+ padding: 0.5rem 1rem;
925
+ background: var(--bg-tertiary);
926
+ border: 1px solid var(--border);
927
+ border-radius: 6px;
928
+ color: var(--text-primary);
929
+ cursor: pointer;
930
+ font-size: 0.8rem;
931
+ }
932
+ .footer-btn:hover { background: var(--border); }
933
+
934
+ /* Print styles */
935
+ @media print {
936
+ body { background: white; color: black; padding: 1rem; }
937
+ .container { max-width: 100%; }
938
+ .header, .donut-card, .radar-section, .exec-section, .category, .footer {
939
+ background: white;
940
+ border: 1px solid #ddd;
941
+ break-inside: avoid;
942
+ }
943
+ .category.collapsed .category-content { display: block !important; }
944
+ .chevron, .expand-all, .footer-actions { display: none; }
945
+ .category-header { cursor: default; }
946
+ .control-row.failed { background: #fff0f0; }
947
+ :root {
948
+ --bg-primary: white;
949
+ --bg-secondary: white;
950
+ --bg-tertiary: #f5f5f5;
951
+ --text-primary: black;
952
+ --text-secondary: #555;
953
+ --text-muted: #888;
954
+ --border: #ddd;
955
+ }
956
+ }
479
957
  </style>
480
958
  </head>
481
959
  <body>
482
960
  <div class="container">
483
- <div class="header">
484
- <h1>📋 ${escapeHtml(result.benchmark)}</h1>
485
- <div class="version">Version ${result.version} • Generated ${new Date(result.timestamp).toISOString()}</div>
486
- </div>
487
-
488
- <div class="summary">
489
- <div class="summary-card">
490
- <div class="value rating">${result.rating}</div>
491
- <div class="label">Rating</div>
961
+ <header class="header">
962
+ <div class="header-left">
963
+ <h1><span class="header-icon">${icons.shield}</span>${escapeHtml(result.benchmark)}</h1>
964
+ <div class="meta">Version ${result.version} • Generated ${new Date(result.timestamp).toLocaleString()}</div>
492
965
  </div>
493
- <div class="summary-card">
494
- <div class="value">${result.compliance}%</div>
495
- <div class="label">Compliance</div>
966
+ <div class="header-right">
967
+ <div class="rating-badge">${result.rating}</div>
968
+ <div class="level-tag">${result.level} — ${result.level === 'L1' ? 'Essential' : result.level === 'L2' ? 'Standard' : 'Hardened'}</div>
969
+ </div>
970
+ </header>
971
+
972
+ <div class="dashboard">
973
+ <div class="score-card">
974
+ <div class="score-header">
975
+ <div class="score-grade" style="background: ${grade.color}20; border-color: ${grade.color};">
976
+ <span class="grade-letter" style="color: ${grade.color};">${grade.letter}</span>
977
+ </div>
978
+ <div class="score-main">
979
+ <div class="score-pct">${result.compliance}%</div>
980
+ <div class="score-label">Security Score</div>
981
+ </div>
982
+ </div>
983
+
984
+ <div class="score-bars">
985
+ <div class="score-bar-row">
986
+ <span class="bar-label">Passed</span>
987
+ <div class="bar-track">
988
+ <div class="bar-fill bar-pass" style="width: ${allControls.length ? (passedControls.length / allControls.length * 100) : 0}%;"></div>
989
+ </div>
990
+ <span class="bar-count">${passedControls.length}</span>
991
+ </div>
992
+ <div class="score-bar-row">
993
+ <span class="bar-label">Failed</span>
994
+ <div class="bar-track">
995
+ <div class="bar-fill bar-fail" style="width: ${allControls.length ? (failedControls.length / allControls.length * 100) : 0}%;"></div>
996
+ </div>
997
+ <span class="bar-count">${failedControls.length}</span>
998
+ </div>
999
+ <div class="score-bar-row">
1000
+ <span class="bar-label">Manual</span>
1001
+ <div class="bar-track">
1002
+ <div class="bar-fill bar-manual" style="width: ${allControls.length ? (unverifiedControls.length / allControls.length * 100) : 0}%;"></div>
1003
+ </div>
1004
+ <span class="bar-count">${unverifiedControls.length}</span>
1005
+ </div>
1006
+ </div>
1007
+
1008
+ <div class="level-breakdown">
1009
+ <div class="level-row">
1010
+ <span class="level-badge level-l1">L1</span>
1011
+ <span class="level-stat">${levelStats.L1.passed}/${levelStats.L1.total}</span>
1012
+ </div>
1013
+ <div class="level-row">
1014
+ <span class="level-badge level-l2">L2</span>
1015
+ <span class="level-stat">${levelStats.L2.passed}/${levelStats.L2.total}</span>
1016
+ </div>
1017
+ <div class="level-row">
1018
+ <span class="level-badge level-l3">L3</span>
1019
+ <span class="level-stat">${levelStats.L3.passed}/${levelStats.L3.total}</span>
1020
+ </div>
1021
+ </div>
1022
+
1023
+ ${worstCategory && worstCategory.compliance < 100 ? `
1024
+ <div class="worst-category">
1025
+ <span class="worst-label">Needs Attention</span>
1026
+ <span class="worst-name">${escapeHtml(worstCategory.category)}</span>
1027
+ <span class="worst-pct" style="color: ${worstCategory.compliance < 50 ? '#ef4444' : '#eab308'};">${worstCategory.compliance}%</span>
1028
+ </div>` : ''}
496
1029
  </div>
497
- <div class="summary-card">
498
- <div class="value">${result.passedControls}/${result.passedControls + result.failedControls}</div>
499
- <div class="label">Controls Passed</div>
1030
+
1031
+ <div class="radar-section">
1032
+ <h3>Category Coverage</h3>
1033
+ <div class="radar-container">
1034
+ <svg width="240" height="240" viewBox="0 0 240 240">
1035
+ ${radarGrid}
1036
+ ${radarAxes}
1037
+ <polygon points="${radarPoints.join(' ')}" fill="${complianceColor}20" stroke="${complianceColor}" stroke-width="2"/>
1038
+ ${radarLabels.join('')}
1039
+ </svg>
1040
+ </div>
500
1041
  </div>
501
- <div class="summary-card">
502
- <div class="value">Level ${result.level.replace('L', '')}</div>
503
- <div class="label">${result.level === 'L1' ? 'Essential' : result.level === 'L2' ? 'Standard' : 'Hardened'}</div>
1042
+
1043
+ <div class="exec-section">
1044
+ <h3>Priority Issues</h3>
1045
+ ${executiveSummary}
1046
+ ${failedControls.length > 0 ? `<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.8rem; color: var(--text-muted);">
1047
+ ${levelDesc}
1048
+ </div>` : ''}
504
1049
  </div>
505
1050
  </div>
506
1051
 
1052
+ <div class="categories-header">
1053
+ <h2>Control Details by Category</h2>
1054
+ <button class="expand-all" onclick="toggleAll()">Expand All</button>
1055
+ </div>
1056
+
507
1057
  ${categoryRows}
508
1058
 
509
- <div class="footer">
510
- Generated by <a href="https://hackmyagent.com" style="color: #60a5fa;">HackMyAgent</a> •
511
- <a href="https://oasb.ai" style="color: #60a5fa;">OASB-1 Specification</a>
512
- </div>
1059
+ <footer class="footer">
1060
+ <div>
1061
+ Generated by <a href="https://hackmyagent.com">HackMyAgent</a>
1062
+ <a href="https://oasb.ai">OASB-1 Specification</a>
1063
+ </div>
1064
+ <div class="footer-actions">
1065
+ <button class="footer-btn" onclick="window.print()">${icons.print} Print / PDF</button>
1066
+ </div>
1067
+ </footer>
513
1068
  </div>
1069
+
1070
+ <script>
1071
+ function toggleCategory(index) {
1072
+ const cat = document.getElementById('cat-' + index);
1073
+ cat.classList.toggle('collapsed');
1074
+ }
1075
+
1076
+ function toggleAll() {
1077
+ const categories = document.querySelectorAll('.category');
1078
+ const btn = document.querySelector('.expand-all');
1079
+ const allCollapsed = Array.from(categories).every(c => c.classList.contains('collapsed'));
1080
+
1081
+ categories.forEach(cat => {
1082
+ if (allCollapsed) {
1083
+ cat.classList.remove('collapsed');
1084
+ } else {
1085
+ cat.classList.add('collapsed');
1086
+ }
1087
+ });
1088
+
1089
+ btn.textContent = allCollapsed ? 'Collapse All' : 'Expand All';
1090
+ }
1091
+
1092
+ // Start with categories collapsed
1093
+ document.querySelectorAll('.category').forEach(cat => cat.classList.add('collapsed'));
1094
+ </script>
514
1095
  </body>
515
1096
  </html>`;
516
1097
  }
@@ -726,6 +1307,11 @@ Examples:
726
1307
  .action(async (directory, options) => {
727
1308
  try {
728
1309
  const targetDir = directory.startsWith('/') ? directory : process.cwd() + '/' + directory;
1310
+ // Check if directory exists
1311
+ if (!require('fs').existsSync(targetDir)) {
1312
+ console.error(`Error: Directory '${targetDir}' does not exist.`);
1313
+ process.exit(1);
1314
+ }
729
1315
  // Parse ignore list
730
1316
  const ignoreList = options.ignore
731
1317
  ? options.ignore.split(',').map((s) => s.trim()).filter(Boolean)
@@ -1251,7 +1837,7 @@ Examples:
1251
1837
  .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
1252
1838
  .option('--delay <ms>', 'Delay between requests in milliseconds', '1000')
1253
1839
  .option('--stop-on-success', 'Stop after first successful attack')
1254
- .option('-f, --format <format>', 'Output format: text, json, sarif', 'text')
1840
+ .option('-f, --format <format>', 'Output format: text, json, sarif, html', 'text')
1255
1841
  .option('-o, --output <file>', 'Write output to file')
1256
1842
  .option('-v, --verbose', 'Show detailed output for each payload')
1257
1843
  .action(async (targetUrl, options) => {
@@ -1300,7 +1886,7 @@ Examples:
1300
1886
  systemPrompt: options.systemPrompt,
1301
1887
  };
1302
1888
  // Validate format
1303
- const validFormats = ['text', 'json', 'sarif'];
1889
+ const validFormats = ['text', 'json', 'sarif', 'html'];
1304
1890
  const format = options.format || 'text';
1305
1891
  if (!validFormats.includes(format)) {
1306
1892
  console.error(`Error: Invalid format '${format}'. Use: ${validFormats.join(', ')}`);
@@ -1334,6 +1920,9 @@ Examples:
1334
1920
  case 'sarif':
1335
1921
  output = generateAttackSarif(report);
1336
1922
  break;
1923
+ case 'html':
1924
+ output = generateAttackHtmlReport(report);
1925
+ break;
1337
1926
  default: // text
1338
1927
  printAttackReport(report, options.verbose ?? false);
1339
1928
  output = '';
@@ -1461,5 +2050,738 @@ function generateAttackSarif(report) {
1461
2050
  }],
1462
2051
  }, null, 2);
1463
2052
  }
2053
+ // Generate HTML report for attack results
2054
+ function generateAttackHtmlReport(report) {
2055
+ // Risk grade based on score
2056
+ const getGrade = (score) => {
2057
+ if (score <= 10)
2058
+ return { letter: 'A', color: '#22c55e' };
2059
+ if (score <= 25)
2060
+ return { letter: 'B', color: '#84cc16' };
2061
+ if (score <= 50)
2062
+ return { letter: 'C', color: '#eab308' };
2063
+ if (score <= 70)
2064
+ return { letter: 'D', color: '#f97316' };
2065
+ return { letter: 'F', color: '#ef4444' };
2066
+ };
2067
+ const grade = getGrade(report.riskScore);
2068
+ const ratingColor = {
2069
+ 'critical': '#ef4444',
2070
+ 'high': '#f97316',
2071
+ 'medium': '#eab308',
2072
+ 'low': '#22c55e',
2073
+ 'secure': '#22c55e',
2074
+ };
2075
+ const ratingBg = {
2076
+ 'critical': 'rgba(239, 68, 68, 0.15)',
2077
+ 'high': 'rgba(249, 115, 22, 0.15)',
2078
+ 'medium': 'rgba(234, 179, 8, 0.15)',
2079
+ 'low': 'rgba(34, 197, 94, 0.15)',
2080
+ 'secure': 'rgba(34, 197, 94, 0.15)',
2081
+ };
2082
+ // SVG icons
2083
+ const icons = {
2084
+ sword: '<svg class="icon icon-sword" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 17.5L3 6V3h3l11.5 11.5"/><path d="M13 19l6-6"/><path d="M16 16l4 4"/><path d="M19 21l2-2"/></svg>',
2085
+ shield: '<svg class="icon icon-shield" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>',
2086
+ check: '<svg class="icon icon-check" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>',
2087
+ x: '<svg class="icon icon-x" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>',
2088
+ warning: '<svg class="icon icon-warning" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>',
2089
+ print: '<svg class="icon icon-print" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5 4v3H4a2 2 0 00-2 2v3a2 2 0 002 2h1v2a2 2 0 002 2h6a2 2 0 002-2v-2h1a2 2 0 002-2V9a2 2 0 00-2-2h-1V4a2 2 0 00-2-2H7a2 2 0 00-2 2zm8 0H7v3h6V4zm0 8H7v4h6v-4z" clip-rule="evenodd"/></svg>',
2090
+ };
2091
+ // Category abbreviations
2092
+ const categoryAbbrev = {
2093
+ 'prompt-injection': 'PI',
2094
+ 'jailbreak': 'JB',
2095
+ 'data-exfiltration': 'DE',
2096
+ 'capability-abuse': 'CA',
2097
+ 'context-manipulation': 'CM',
2098
+ };
2099
+ // Donut chart for attack results
2100
+ const donutRadius = 60;
2101
+ const donutStroke = 12;
2102
+ const donutCircumference = 2 * Math.PI * donutRadius;
2103
+ const total = report.summary.total || 1;
2104
+ const successPct = report.summary.successful / total;
2105
+ const blockedPct = report.summary.blocked / total;
2106
+ const inconclusivePct = report.summary.inconclusive / total;
2107
+ const successDash = donutCircumference * successPct;
2108
+ const blockedDash = donutCircumference * blockedPct;
2109
+ const inconclusiveDash = donutCircumference * inconclusivePct;
2110
+ // Calculate offsets for each segment
2111
+ const successOffset = 0;
2112
+ const blockedOffset = successDash;
2113
+ const inconclusiveOffset = successDash + blockedDash;
2114
+ const donutSvg = `
2115
+ <svg width="160" height="160" viewBox="0 0 160 160">
2116
+ <!-- Background circle -->
2117
+ <circle cx="80" cy="80" r="${donutRadius}" fill="none" stroke="#334155" stroke-width="${donutStroke}"/>
2118
+ <!-- Inconclusive segment (gray) -->
2119
+ ${inconclusivePct > 0 ? `<circle cx="80" cy="80" r="${donutRadius}" fill="none"
2120
+ stroke="#64748b" stroke-width="${donutStroke}"
2121
+ stroke-dasharray="${inconclusiveDash} ${donutCircumference}"
2122
+ stroke-dashoffset="${-inconclusiveOffset}"
2123
+ transform="rotate(-90 80 80)"/>` : ''}
2124
+ <!-- Blocked segment (green) -->
2125
+ ${blockedPct > 0 ? `<circle cx="80" cy="80" r="${donutRadius}" fill="none"
2126
+ stroke="#22c55e" stroke-width="${donutStroke}"
2127
+ stroke-dasharray="${blockedDash} ${donutCircumference}"
2128
+ stroke-dashoffset="${-blockedOffset}"
2129
+ transform="rotate(-90 80 80)"/>` : ''}
2130
+ <!-- Successful segment (red) -->
2131
+ ${successPct > 0 ? `<circle cx="80" cy="80" r="${donutRadius}" fill="none"
2132
+ stroke="#ef4444" stroke-width="${donutStroke}"
2133
+ stroke-dasharray="${successDash} ${donutCircumference}"
2134
+ stroke-dashoffset="${-successOffset}"
2135
+ transform="rotate(-90 80 80)"/>` : ''}
2136
+ <!-- Center text -->
2137
+ <text x="80" y="75" text-anchor="middle" fill="#f1f5f9" font-size="24" font-weight="700">${report.summary.total}</text>
2138
+ <text x="80" y="95" text-anchor="middle" fill="#94a3b8" font-size="12">attacks</text>
2139
+ </svg>`;
2140
+ // Generate category breakdown rows
2141
+ const categoryRows = Object.entries(report.summary.byCategory)
2142
+ .filter(([_, stats]) => stats.total > 0)
2143
+ .map(([cat, stats]) => {
2144
+ const catInfo = hackmyagent_core_1.ATTACK_CATEGORIES[cat];
2145
+ const abbrev = categoryAbbrev[cat];
2146
+ const successRate = stats.total > 0 ? Math.round((stats.successful / stats.total) * 100) : 0;
2147
+ const barColor = stats.successful === 0 ? '#22c55e' : successRate > 50 ? '#ef4444' : '#eab308';
2148
+ const statusIcon = stats.successful === 0 ? icons.check : icons.x;
2149
+ const statusClass = stats.successful === 0 ? 'status-pass' : 'status-fail';
2150
+ // Get results for this category
2151
+ const catResults = report.results.filter(r => r.payload.category === cat);
2152
+ const resultRows = catResults.map(r => {
2153
+ const resultIcon = r.success ? icons.x : r.blocked ? icons.check : icons.warning;
2154
+ const resultClass = r.success ? 'status-fail' : r.blocked ? 'status-pass' : 'status-warn';
2155
+ const sevColor = r.payload.severity === 'critical' ? '#ef4444' :
2156
+ r.payload.severity === 'high' ? '#f97316' :
2157
+ r.payload.severity === 'medium' ? '#eab308' : '#22c55e';
2158
+ return `
2159
+ <tr class="attack-row ${r.success ? 'failed' : ''}">
2160
+ <td class="status-cell"><span class="${resultClass}">${resultIcon}</span></td>
2161
+ <td class="id-cell"><code>${r.payload.id}</code></td>
2162
+ <td class="name-cell">${escapeHtml(r.payload.name)}</td>
2163
+ <td class="severity-cell"><span class="severity-badge" style="color: ${sevColor}; background: ${sevColor}20;">${r.payload.severity.toUpperCase()}</span></td>
2164
+ <td class="result-cell">${r.success ? '<span class="result-tag fail">Succeeded</span>' : r.blocked ? '<span class="result-tag pass">Blocked</span>' : '<span class="result-tag warn">Inconclusive</span>'}</td>
2165
+ </tr>`;
2166
+ }).join('');
2167
+ return `
2168
+ <div class="category" id="cat-${abbrev}">
2169
+ <div class="category-header" onclick="toggleCategory('${abbrev}')">
2170
+ <span class="category-abbrev">[${abbrev}]</span>
2171
+ <span class="category-icon ${statusClass}">${statusIcon}</span>
2172
+ <span class="category-name">${escapeHtml(catInfo.name)}</span>
2173
+ <div class="category-meta">
2174
+ <span class="category-score">${stats.successful}/${stats.total} successful</span>
2175
+ <div class="mini-bar"><div class="mini-fill" style="width: ${successRate}%; background: ${barColor};"></div></div>
2176
+ <span class="chevron">▼</span>
2177
+ </div>
2178
+ </div>
2179
+ <div class="category-content">
2180
+ <table class="attacks-table">
2181
+ <thead><tr><th></th><th>ID</th><th>Attack</th><th>Severity</th><th>Result</th></tr></thead>
2182
+ <tbody>${resultRows}</tbody>
2183
+ </table>
2184
+ </div>
2185
+ </div>`;
2186
+ }).join('');
2187
+ // Successful attacks detail section
2188
+ const successfulAttacks = report.results.filter(r => r.success);
2189
+ const successfulDetailsHtml = successfulAttacks.length > 0 ? successfulAttacks.map(r => {
2190
+ const sevColor = r.payload.severity === 'critical' ? '#ef4444' :
2191
+ r.payload.severity === 'high' ? '#f97316' :
2192
+ r.payload.severity === 'medium' ? '#eab308' : '#22c55e';
2193
+ return `
2194
+ <div class="attack-detail">
2195
+ <div class="attack-detail-header">
2196
+ <code class="attack-id">${r.payload.id}</code>
2197
+ <span class="attack-name">${escapeHtml(r.payload.name)}</span>
2198
+ <span class="severity-badge" style="color: ${sevColor}; background: ${sevColor}20;">${r.payload.severity.toUpperCase()}</span>
2199
+ </div>
2200
+ <div class="attack-detail-meta">
2201
+ ${r.payload.oasbControl ? `<span class="meta-tag">OASB ${r.payload.oasbControl}</span>` : ''}
2202
+ ${r.payload.cwe ? `<span class="meta-tag">CWE-${r.payload.cwe}</span>` : ''}
2203
+ <span class="meta-tag">${hackmyagent_core_1.ATTACK_CATEGORIES[r.payload.category].name}</span>
2204
+ </div>
2205
+ <div class="attack-detail-body">
2206
+ <div class="detail-section">
2207
+ <strong>Description:</strong> ${escapeHtml(r.payload.description)}
2208
+ </div>
2209
+ <div class="detail-section evidence">
2210
+ <strong>Evidence:</strong> ${escapeHtml(r.evidence)}
2211
+ </div>
2212
+ <div class="detail-section remediation">
2213
+ <strong>Remediation:</strong> ${escapeHtml(r.payload.remediation)}
2214
+ </div>
2215
+ </div>
2216
+ </div>`;
2217
+ }).join('') : '<div class="no-attacks">No successful attacks detected.</div>';
2218
+ return `<!DOCTYPE html>
2219
+ <html lang="en">
2220
+ <head>
2221
+ <meta charset="UTF-8">
2222
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2223
+ <title>HackMyAgent Attack Report | ${report.riskRating.toUpperCase()}</title>
2224
+ <style>
2225
+ :root {
2226
+ --bg-primary: #0a0f1a;
2227
+ --bg-secondary: #111827;
2228
+ --bg-tertiary: #1f2937;
2229
+ --text-primary: #f1f5f9;
2230
+ --text-secondary: #94a3b8;
2231
+ --text-muted: #64748b;
2232
+ --border: #334155;
2233
+ --accent: #3b82f6;
2234
+ --success: #22c55e;
2235
+ --warning: #eab308;
2236
+ --danger: #ef4444;
2237
+ }
2238
+ * { box-sizing: border-box; margin: 0; padding: 0; }
2239
+ body {
2240
+ font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2241
+ background: var(--bg-primary);
2242
+ color: var(--text-primary);
2243
+ line-height: 1.6;
2244
+ padding: 2rem;
2245
+ font-size: 14px;
2246
+ }
2247
+ .container { max-width: 1400px; margin: 0 auto; }
2248
+
2249
+ /* Header */
2250
+ .header {
2251
+ display: flex;
2252
+ justify-content: space-between;
2253
+ align-items: center;
2254
+ margin-bottom: 2rem;
2255
+ padding: 1.5rem 2rem;
2256
+ background: var(--bg-secondary);
2257
+ border-radius: 12px;
2258
+ border: 1px solid var(--border);
2259
+ }
2260
+ .header-left h1 {
2261
+ font-size: 1.5rem;
2262
+ font-weight: 700;
2263
+ display: flex;
2264
+ align-items: center;
2265
+ gap: 0.75rem;
2266
+ }
2267
+ .header-left .meta { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.25rem; }
2268
+ .header-icon { display: inline-flex; margin-right: 0.5rem; }
2269
+ .header-icon .icon { width: 24px; height: 24px; color: var(--danger); }
2270
+ .header-right { display: flex; align-items: center; gap: 1rem; }
2271
+ .rating-badge {
2272
+ display: inline-block;
2273
+ padding: 0.375rem 1rem;
2274
+ border-radius: 6px;
2275
+ font-weight: 600;
2276
+ font-size: 0.875rem;
2277
+ background: ${ratingBg[report.riskRating]};
2278
+ color: ${ratingColor[report.riskRating]};
2279
+ border: 1px solid ${ratingColor[report.riskRating]}40;
2280
+ }
2281
+ .intensity-tag {
2282
+ display: inline-block;
2283
+ padding: 0.375rem 1rem;
2284
+ background: var(--accent);
2285
+ color: white;
2286
+ border-radius: 6px;
2287
+ font-size: 0.875rem;
2288
+ font-weight: 600;
2289
+ text-transform: capitalize;
2290
+ }
2291
+
2292
+ /* SVG Icons */
2293
+ .icon { width: 16px; height: 16px; display: inline-block; vertical-align: middle; }
2294
+ .status-pass { color: var(--success); }
2295
+ .status-fail { color: var(--danger); }
2296
+ .status-warn { color: var(--warning); }
2297
+ .category-icon { display: flex; align-items: center; }
2298
+ .category-icon .icon { width: 18px; height: 18px; }
2299
+ .footer-btn .icon { width: 14px; height: 14px; margin-right: 0.375rem; }
2300
+
2301
+ /* Dashboard grid */
2302
+ .dashboard {
2303
+ display: grid;
2304
+ grid-template-columns: 280px 200px 1fr;
2305
+ gap: 1.5rem;
2306
+ margin-bottom: 2rem;
2307
+ }
2308
+ @media (max-width: 1200px) {
2309
+ .dashboard { grid-template-columns: 1fr 1fr; }
2310
+ .summary-section { grid-column: span 2; }
2311
+ }
2312
+ @media (max-width: 768px) {
2313
+ .dashboard { grid-template-columns: 1fr; }
2314
+ .summary-section { grid-column: span 1; }
2315
+ }
2316
+
2317
+ /* Risk Score card */
2318
+ .score-card {
2319
+ background: var(--bg-secondary);
2320
+ border-radius: 12px;
2321
+ padding: 1.25rem;
2322
+ border: 1px solid var(--border);
2323
+ }
2324
+ .score-header {
2325
+ display: flex;
2326
+ align-items: center;
2327
+ gap: 1rem;
2328
+ margin-bottom: 1.25rem;
2329
+ padding-bottom: 1rem;
2330
+ border-bottom: 1px solid var(--border);
2331
+ }
2332
+ .score-grade {
2333
+ width: 56px;
2334
+ height: 56px;
2335
+ border-radius: 12px;
2336
+ border: 2px solid;
2337
+ display: flex;
2338
+ align-items: center;
2339
+ justify-content: center;
2340
+ }
2341
+ .grade-letter { font-size: 1.75rem; font-weight: 800; }
2342
+ .score-main { flex: 1; }
2343
+ .score-pct { font-size: 2rem; font-weight: 700; color: var(--text-primary); line-height: 1; }
2344
+ .score-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-top: 0.25rem; }
2345
+
2346
+ .score-stats { margin-top: 1rem; }
2347
+ .stat-row {
2348
+ display: flex;
2349
+ align-items: center;
2350
+ justify-content: space-between;
2351
+ padding: 0.5rem 0;
2352
+ border-bottom: 1px solid var(--border);
2353
+ }
2354
+ .stat-row:last-child { border-bottom: none; }
2355
+ .stat-label { color: var(--text-secondary); font-size: 0.85rem; }
2356
+ .stat-value { font-weight: 600; }
2357
+ .stat-value.danger { color: var(--danger); }
2358
+ .stat-value.success { color: var(--success); }
2359
+ .stat-value.muted { color: var(--text-muted); }
2360
+
2361
+ /* Donut chart section */
2362
+ .donut-section {
2363
+ background: var(--bg-secondary);
2364
+ border-radius: 12px;
2365
+ padding: 1.25rem;
2366
+ border: 1px solid var(--border);
2367
+ display: flex;
2368
+ flex-direction: column;
2369
+ align-items: center;
2370
+ }
2371
+ .donut-section h3 {
2372
+ font-size: 0.85rem;
2373
+ color: var(--text-secondary);
2374
+ text-transform: uppercase;
2375
+ letter-spacing: 0.05em;
2376
+ margin-bottom: 1rem;
2377
+ width: 100%;
2378
+ }
2379
+ .donut-legend {
2380
+ display: flex;
2381
+ flex-direction: column;
2382
+ gap: 0.5rem;
2383
+ margin-top: 1rem;
2384
+ width: 100%;
2385
+ }
2386
+ .legend-item {
2387
+ display: flex;
2388
+ align-items: center;
2389
+ gap: 0.5rem;
2390
+ font-size: 0.8rem;
2391
+ color: var(--text-secondary);
2392
+ }
2393
+ .legend-dot {
2394
+ width: 10px;
2395
+ height: 10px;
2396
+ border-radius: 50%;
2397
+ }
2398
+
2399
+ /* Summary section */
2400
+ .summary-section {
2401
+ background: var(--bg-secondary);
2402
+ border-radius: 12px;
2403
+ padding: 1.5rem;
2404
+ border: 1px solid var(--border);
2405
+ }
2406
+ .summary-section h3 {
2407
+ font-size: 0.85rem;
2408
+ color: var(--text-secondary);
2409
+ text-transform: uppercase;
2410
+ letter-spacing: 0.05em;
2411
+ margin-bottom: 1rem;
2412
+ }
2413
+ .severity-breakdown {
2414
+ display: flex;
2415
+ gap: 1rem;
2416
+ flex-wrap: wrap;
2417
+ }
2418
+ .severity-item {
2419
+ display: flex;
2420
+ align-items: center;
2421
+ gap: 0.5rem;
2422
+ padding: 0.5rem 1rem;
2423
+ background: var(--bg-tertiary);
2424
+ border-radius: 6px;
2425
+ }
2426
+ .severity-count { font-size: 1.25rem; font-weight: 700; }
2427
+ .severity-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; }
2428
+
2429
+ /* Categories */
2430
+ .categories-section {
2431
+ margin-bottom: 2rem;
2432
+ }
2433
+ .categories-header {
2434
+ display: flex;
2435
+ justify-content: space-between;
2436
+ align-items: center;
2437
+ margin-bottom: 1rem;
2438
+ }
2439
+ .categories-header h2 { font-size: 1.1rem; }
2440
+ .expand-all {
2441
+ background: var(--bg-tertiary);
2442
+ border: 1px solid var(--border);
2443
+ color: var(--text-secondary);
2444
+ padding: 0.5rem 1rem;
2445
+ border-radius: 6px;
2446
+ cursor: pointer;
2447
+ font-size: 0.8rem;
2448
+ }
2449
+ .expand-all:hover { background: var(--border); }
2450
+
2451
+ .category {
2452
+ background: var(--bg-secondary);
2453
+ border-radius: 8px;
2454
+ margin-bottom: 0.75rem;
2455
+ border: 1px solid var(--border);
2456
+ overflow: hidden;
2457
+ }
2458
+ .category-header {
2459
+ display: flex;
2460
+ align-items: center;
2461
+ gap: 0.75rem;
2462
+ padding: 1rem 1.25rem;
2463
+ cursor: pointer;
2464
+ transition: background 0.15s;
2465
+ }
2466
+ .category-header:hover { background: var(--bg-tertiary); }
2467
+ .category-abbrev {
2468
+ font-family: monospace;
2469
+ font-size: 0.85rem;
2470
+ color: var(--accent);
2471
+ font-weight: 600;
2472
+ }
2473
+ .category-icon { font-size: 1.1rem; }
2474
+ .category-name { flex: 1; font-weight: 500; }
2475
+ .category-meta { display: flex; align-items: center; gap: 0.75rem; }
2476
+ .category-score { color: var(--text-secondary); font-size: 0.85rem; font-weight: 500; }
2477
+ .mini-bar { width: 60px; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
2478
+ .mini-fill { height: 100%; border-radius: 3px; }
2479
+ .chevron {
2480
+ color: var(--text-muted);
2481
+ font-size: 0.7rem;
2482
+ transition: transform 0.2s;
2483
+ margin-left: 0.5rem;
2484
+ }
2485
+ .category.collapsed .chevron { transform: rotate(-90deg); }
2486
+ .category.collapsed .category-content { display: none; }
2487
+
2488
+ .category-content { border-top: 1px solid var(--border); }
2489
+ .attacks-table { width: 100%; border-collapse: collapse; }
2490
+ .attacks-table th {
2491
+ padding: 0.75rem 1rem;
2492
+ text-align: left;
2493
+ background: var(--bg-primary);
2494
+ color: var(--text-muted);
2495
+ font-weight: 500;
2496
+ font-size: 0.75rem;
2497
+ text-transform: uppercase;
2498
+ letter-spacing: 0.03em;
2499
+ }
2500
+ .attacks-table td {
2501
+ padding: 0.875rem 1rem;
2502
+ border-top: 1px solid var(--border);
2503
+ vertical-align: middle;
2504
+ }
2505
+ .status-cell { width: 40px; text-align: center; }
2506
+ .id-cell { width: 80px; }
2507
+ .id-cell code {
2508
+ background: var(--bg-tertiary);
2509
+ padding: 0.2rem 0.5rem;
2510
+ border-radius: 4px;
2511
+ font-size: 0.8rem;
2512
+ color: var(--accent);
2513
+ }
2514
+ .name-cell { width: 40%; }
2515
+ .severity-cell { width: 80px; }
2516
+ .result-cell { width: 100px; }
2517
+ .attack-row.failed { background: rgba(239, 68, 68, 0.05); }
2518
+
2519
+ .severity-badge {
2520
+ padding: 0.2rem 0.6rem;
2521
+ border-radius: 4px;
2522
+ font-size: 0.7rem;
2523
+ font-weight: 600;
2524
+ text-transform: uppercase;
2525
+ }
2526
+ .result-tag {
2527
+ padding: 0.2rem 0.6rem;
2528
+ border-radius: 4px;
2529
+ font-size: 0.7rem;
2530
+ font-weight: 600;
2531
+ }
2532
+ .result-tag.pass { background: rgba(34, 197, 94, 0.2); color: var(--success); }
2533
+ .result-tag.fail { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
2534
+ .result-tag.warn { background: rgba(234, 179, 8, 0.2); color: var(--warning); }
2535
+
2536
+ /* Successful attacks detail */
2537
+ .details-section {
2538
+ margin-bottom: 2rem;
2539
+ }
2540
+ .details-section h2 {
2541
+ font-size: 1.1rem;
2542
+ margin-bottom: 1rem;
2543
+ display: flex;
2544
+ align-items: center;
2545
+ gap: 0.5rem;
2546
+ }
2547
+ .details-section h2 .icon { color: var(--danger); }
2548
+ .attack-detail {
2549
+ background: var(--bg-secondary);
2550
+ border-radius: 8px;
2551
+ margin-bottom: 1rem;
2552
+ border: 1px solid var(--border);
2553
+ border-left: 3px solid var(--danger);
2554
+ overflow: hidden;
2555
+ }
2556
+ .attack-detail-header {
2557
+ display: flex;
2558
+ align-items: center;
2559
+ gap: 1rem;
2560
+ padding: 1rem 1.25rem;
2561
+ background: rgba(239, 68, 68, 0.05);
2562
+ }
2563
+ .attack-id {
2564
+ background: var(--bg-tertiary);
2565
+ padding: 0.2rem 0.5rem;
2566
+ border-radius: 4px;
2567
+ font-size: 0.85rem;
2568
+ color: var(--danger);
2569
+ }
2570
+ .attack-name { flex: 1; font-weight: 500; }
2571
+ .attack-detail-meta {
2572
+ display: flex;
2573
+ gap: 0.5rem;
2574
+ padding: 0.75rem 1.25rem;
2575
+ background: var(--bg-tertiary);
2576
+ border-bottom: 1px solid var(--border);
2577
+ }
2578
+ .meta-tag {
2579
+ padding: 0.2rem 0.5rem;
2580
+ background: var(--bg-secondary);
2581
+ border-radius: 4px;
2582
+ font-size: 0.75rem;
2583
+ color: var(--text-muted);
2584
+ }
2585
+ .attack-detail-body { padding: 1rem 1.25rem; }
2586
+ .detail-section {
2587
+ margin-bottom: 0.75rem;
2588
+ font-size: 0.9rem;
2589
+ color: var(--text-secondary);
2590
+ }
2591
+ .detail-section:last-child { margin-bottom: 0; }
2592
+ .detail-section strong { color: var(--text-primary); margin-right: 0.5rem; }
2593
+ .detail-section.evidence {
2594
+ padding: 0.75rem;
2595
+ background: rgba(239, 68, 68, 0.1);
2596
+ border-radius: 6px;
2597
+ border-left: 3px solid var(--danger);
2598
+ }
2599
+ .detail-section.remediation {
2600
+ padding: 0.75rem;
2601
+ background: var(--bg-tertiary);
2602
+ border-radius: 6px;
2603
+ border-left: 3px solid var(--accent);
2604
+ }
2605
+ .no-attacks {
2606
+ padding: 2rem;
2607
+ text-align: center;
2608
+ color: var(--success);
2609
+ background: var(--bg-secondary);
2610
+ border-radius: 8px;
2611
+ border: 1px solid var(--border);
2612
+ }
2613
+
2614
+ /* Footer */
2615
+ .footer {
2616
+ display: flex;
2617
+ justify-content: space-between;
2618
+ align-items: center;
2619
+ margin-top: 2rem;
2620
+ padding: 1.5rem;
2621
+ background: var(--bg-secondary);
2622
+ border-radius: 12px;
2623
+ border: 1px solid var(--border);
2624
+ color: var(--text-muted);
2625
+ font-size: 0.85rem;
2626
+ }
2627
+ .footer a { color: var(--accent); text-decoration: none; }
2628
+ .footer a:hover { text-decoration: underline; }
2629
+ .footer-actions { display: flex; gap: 1rem; }
2630
+ .footer-btn {
2631
+ display: flex;
2632
+ align-items: center;
2633
+ padding: 0.5rem 1rem;
2634
+ background: var(--bg-tertiary);
2635
+ border: 1px solid var(--border);
2636
+ border-radius: 6px;
2637
+ color: var(--text-primary);
2638
+ cursor: pointer;
2639
+ font-size: 0.8rem;
2640
+ }
2641
+ .footer-btn:hover { background: var(--border); }
2642
+
2643
+ /* Print styles */
2644
+ @media print {
2645
+ body { background: white; color: black; padding: 1rem; }
2646
+ .container { max-width: 100%; }
2647
+ .header, .score-card, .donut-section, .summary-section, .category, .attack-detail, .footer {
2648
+ background: white;
2649
+ border: 1px solid #ddd;
2650
+ break-inside: avoid;
2651
+ }
2652
+ .category.collapsed .category-content { display: block !important; }
2653
+ .chevron, .expand-all, .footer-actions { display: none; }
2654
+ .category-header { cursor: default; }
2655
+ .attack-row.failed { background: #fff0f0; }
2656
+ :root {
2657
+ --bg-primary: white;
2658
+ --bg-secondary: white;
2659
+ --bg-tertiary: #f5f5f5;
2660
+ --text-primary: black;
2661
+ --text-secondary: #555;
2662
+ --text-muted: #888;
2663
+ --border: #ddd;
2664
+ }
2665
+ }
2666
+ </style>
2667
+ </head>
2668
+ <body>
2669
+ <div class="container">
2670
+ <header class="header">
2671
+ <div class="header-left">
2672
+ <h1><span class="header-icon">${icons.sword}</span>HackMyAgent Attack Report</h1>
2673
+ <div class="meta">Target: ${escapeHtml(report.target || 'Local Simulation')} • ${new Date(report.endTime).toLocaleString()}</div>
2674
+ </div>
2675
+ <div class="header-right">
2676
+ <div class="rating-badge">${report.riskRating.toUpperCase()} RISK</div>
2677
+ <div class="intensity-tag">${report.intensity}</div>
2678
+ </div>
2679
+ </header>
2680
+
2681
+ <div class="dashboard">
2682
+ <div class="score-card">
2683
+ <div class="score-header">
2684
+ <div class="score-grade" style="background: ${grade.color}20; border-color: ${grade.color};">
2685
+ <span class="grade-letter" style="color: ${grade.color};">${grade.letter}</span>
2686
+ </div>
2687
+ <div class="score-main">
2688
+ <div class="score-pct">${report.riskScore}/100</div>
2689
+ <div class="score-label">Risk Score</div>
2690
+ </div>
2691
+ </div>
2692
+ <div class="score-stats">
2693
+ <div class="stat-row">
2694
+ <span class="stat-label">Successful Attacks</span>
2695
+ <span class="stat-value danger">${report.summary.successful}</span>
2696
+ </div>
2697
+ <div class="stat-row">
2698
+ <span class="stat-label">Blocked Attacks</span>
2699
+ <span class="stat-value success">${report.summary.blocked}</span>
2700
+ </div>
2701
+ <div class="stat-row">
2702
+ <span class="stat-label">Inconclusive</span>
2703
+ <span class="stat-value muted">${report.summary.inconclusive}</span>
2704
+ </div>
2705
+ <div class="stat-row">
2706
+ <span class="stat-label">Duration</span>
2707
+ <span class="stat-value">${report.duration}ms</span>
2708
+ </div>
2709
+ </div>
2710
+ </div>
2711
+
2712
+ <div class="donut-section">
2713
+ <h3>Attack Results</h3>
2714
+ ${donutSvg}
2715
+ <div class="donut-legend">
2716
+ <div class="legend-item"><span class="legend-dot" style="background: #ef4444;"></span> Successful (${report.summary.successful})</div>
2717
+ <div class="legend-item"><span class="legend-dot" style="background: #22c55e;"></span> Blocked (${report.summary.blocked})</div>
2718
+ <div class="legend-item"><span class="legend-dot" style="background: #64748b;"></span> Inconclusive (${report.summary.inconclusive})</div>
2719
+ </div>
2720
+ </div>
2721
+
2722
+ <div class="summary-section">
2723
+ <h3>Severity Breakdown (Successful Attacks)</h3>
2724
+ <div class="severity-breakdown">
2725
+ <div class="severity-item">
2726
+ <span class="severity-count" style="color: #ef4444;">${report.summary.bySeverity.critical || 0}</span>
2727
+ <span class="severity-label">Critical</span>
2728
+ </div>
2729
+ <div class="severity-item">
2730
+ <span class="severity-count" style="color: #f97316;">${report.summary.bySeverity.high || 0}</span>
2731
+ <span class="severity-label">High</span>
2732
+ </div>
2733
+ <div class="severity-item">
2734
+ <span class="severity-count" style="color: #eab308;">${report.summary.bySeverity.medium || 0}</span>
2735
+ <span class="severity-label">Medium</span>
2736
+ </div>
2737
+ <div class="severity-item">
2738
+ <span class="severity-count" style="color: #22c55e;">${report.summary.bySeverity.low || 0}</span>
2739
+ <span class="severity-label">Low</span>
2740
+ </div>
2741
+ </div>
2742
+ </div>
2743
+ </div>
2744
+
2745
+ <div class="categories-section">
2746
+ <div class="categories-header">
2747
+ <h2>Category Breakdown</h2>
2748
+ <button class="expand-all" onclick="toggleAll()">Expand/Collapse All</button>
2749
+ </div>
2750
+ ${categoryRows}
2751
+ </div>
2752
+
2753
+ <div class="details-section">
2754
+ <h2>${icons.x} Successful Attacks Detail</h2>
2755
+ ${successfulDetailsHtml}
2756
+ </div>
2757
+
2758
+ <footer class="footer">
2759
+ <div>Generated by <a href="https://hackmyagent.com">HackMyAgent</a> v${hackmyagent_core_1.VERSION} • <a href="https://oasb.ai/attacks">oasb.ai/attacks</a></div>
2760
+ <div class="footer-actions">
2761
+ <button class="footer-btn" onclick="window.print()">${icons.print} Print Report</button>
2762
+ </div>
2763
+ </footer>
2764
+ </div>
2765
+
2766
+ <script>
2767
+ function toggleCategory(id) {
2768
+ const cat = document.getElementById('cat-' + id);
2769
+ cat.classList.toggle('collapsed');
2770
+ }
2771
+ function toggleAll() {
2772
+ const cats = document.querySelectorAll('.category');
2773
+ const allCollapsed = Array.from(cats).every(c => c.classList.contains('collapsed'));
2774
+ cats.forEach(c => {
2775
+ if (allCollapsed) {
2776
+ c.classList.remove('collapsed');
2777
+ } else {
2778
+ c.classList.add('collapsed');
2779
+ }
2780
+ });
2781
+ }
2782
+ </script>
2783
+ </body>
2784
+ </html>`;
2785
+ }
1464
2786
  program.parse();
1465
2787
  //# sourceMappingURL=index.js.map