@odavl/guardian 0.1.0-rc1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1544 -55
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +26 -11
  7. package/policies/landing-demo.json +22 -0
  8. package/src/enterprise/audit-logger.js +166 -0
  9. package/src/enterprise/pdf-exporter.js +267 -0
  10. package/src/enterprise/rbac-gate.js +142 -0
  11. package/src/enterprise/rbac.js +239 -0
  12. package/src/enterprise/site-manager.js +180 -0
  13. package/src/founder/feedback-system.js +156 -0
  14. package/src/founder/founder-tracker.js +213 -0
  15. package/src/founder/usage-signals.js +141 -0
  16. package/src/guardian/alert-ledger.js +121 -0
  17. package/src/guardian/attempt-engine.js +587 -12
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +85 -39
  21. package/src/guardian/attempts-filter.js +63 -0
  22. package/src/guardian/baseline.js +50 -8
  23. package/src/guardian/breakage-intelligence.js +1 -0
  24. package/src/guardian/browser-pool.js +131 -0
  25. package/src/guardian/browser.js +28 -1
  26. package/src/guardian/ci-cli.js +121 -0
  27. package/src/guardian/ci-mode.js +15 -0
  28. package/src/guardian/ci-output.js +38 -0
  29. package/src/guardian/cli-summary.js +167 -67
  30. package/src/guardian/config-loader.js +162 -0
  31. package/src/guardian/data-guardian-detector.js +189 -0
  32. package/src/guardian/detection-layers.js +271 -0
  33. package/src/guardian/drift-detector.js +100 -0
  34. package/src/guardian/enhanced-html-reporter.js +221 -4
  35. package/src/guardian/env-guard.js +127 -0
  36. package/src/guardian/failure-intelligence.js +173 -0
  37. package/src/guardian/first-run-profile.js +89 -0
  38. package/src/guardian/first-run.js +54 -0
  39. package/src/guardian/flag-validator.js +111 -0
  40. package/src/guardian/flow-executor.js +309 -44
  41. package/src/guardian/html-reporter.js +2 -0
  42. package/src/guardian/human-reporter.js +431 -0
  43. package/src/guardian/index.js +22 -19
  44. package/src/guardian/init-command.js +9 -5
  45. package/src/guardian/intent-detector.js +146 -0
  46. package/src/guardian/journey-definitions.js +132 -0
  47. package/src/guardian/journey-scan-cli.js +145 -0
  48. package/src/guardian/journey-scanner.js +583 -0
  49. package/src/guardian/junit-reporter.js +18 -1
  50. package/src/guardian/language-detection.js +99 -0
  51. package/src/guardian/live-cli.js +95 -0
  52. package/src/guardian/live-scheduler-runner.js +137 -0
  53. package/src/guardian/live-scheduler.js +146 -0
  54. package/src/guardian/market-reporter.js +357 -82
  55. package/src/guardian/parallel-executor.js +116 -0
  56. package/src/guardian/pattern-analyzer.js +348 -0
  57. package/src/guardian/policy.js +80 -3
  58. package/src/guardian/prerequisite-checker.js +101 -0
  59. package/src/guardian/preset-loader.js +27 -18
  60. package/src/guardian/profile-loader.js +96 -0
  61. package/src/guardian/reality.js +1612 -115
  62. package/src/guardian/reporter.js +27 -41
  63. package/src/guardian/run-artifacts.js +212 -0
  64. package/src/guardian/run-cleanup.js +207 -0
  65. package/src/guardian/run-latest.js +90 -0
  66. package/src/guardian/run-list.js +211 -0
  67. package/src/guardian/run-summary.js +20 -0
  68. package/src/guardian/scan-presets.js +100 -11
  69. package/src/guardian/selector-fallbacks.js +394 -0
  70. package/src/guardian/semantic-contact-detection.js +255 -0
  71. package/src/guardian/semantic-contact-finder.js +201 -0
  72. package/src/guardian/semantic-targets.js +234 -0
  73. package/src/guardian/site-introspection.js +257 -0
  74. package/src/guardian/smoke.js +258 -0
  75. package/src/guardian/snapshot-schema.js +25 -1
  76. package/src/guardian/snapshot.js +69 -3
  77. package/src/guardian/stability-scorer.js +169 -0
  78. package/src/guardian/success-evaluator.js +214 -0
  79. package/src/guardian/template-command.js +184 -0
  80. package/src/guardian/text-formatters.js +426 -0
  81. package/src/guardian/timeout-profiles.js +57 -0
  82. package/src/guardian/verdict.js +320 -0
  83. package/src/guardian/verdicts.js +74 -0
  84. package/src/guardian/wait-for-outcome.js +120 -0
  85. package/src/guardian/watch-runner.js +181 -0
  86. package/src/payments/stripe-checkout.js +169 -0
  87. package/src/plans/plan-definitions.js +148 -0
  88. package/src/plans/plan-manager.js +211 -0
  89. package/src/plans/usage-tracker.js +210 -0
  90. package/src/recipes/recipe-engine.js +188 -0
  91. package/src/recipes/recipe-failure-analysis.js +159 -0
  92. package/src/recipes/recipe-registry.js +134 -0
  93. package/src/recipes/recipe-runtime.js +507 -0
  94. package/src/recipes/recipe-store.js +410 -0
  95. package/guardian-contract-v1.md +0 -149
  96. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  97. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  98. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  99. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  100. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  101. /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
@@ -0,0 +1,59 @@
1
+ # Guardian Configuration
2
+
3
+ This directory contains all Guardian configuration files.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ config/
9
+ ├── guardian.config.json # Main Guardian configuration
10
+ ├── guardian.policy.json # Testing policy and failure thresholds
11
+ └── profiles/ # Pre-configured profiles for different use cases
12
+ ├── docs.yaml # Documentation site profile
13
+ ├── ecommerce.yaml # E-commerce/Shop profile
14
+ ├── landing-demo.yaml # Landing page profile
15
+ ├── marketing.yaml # Marketing site profile
16
+ └── saas.yaml # SaaS application profile
17
+ ```
18
+
19
+ ## Files
20
+
21
+ ### guardian.config.json
22
+ Main configuration file that controls:
23
+ - Base URL for testing
24
+ - Test mode (SAFE/NORMAL)
25
+ - Page crawling limits (maxPages, maxDepth)
26
+ - Performance thresholds
27
+ - Safety rules (URL patterns, selectors to avoid)
28
+ - Artifact output directory
29
+
30
+ ### guardian.policy.json
31
+ Defines testing policies:
32
+ - Failure severity thresholds
33
+ - Maximum allowed warnings/errors
34
+ - Regression detection settings
35
+ - Baseline requirements
36
+
37
+ ### Profiles (profiles/*.yaml)
38
+ Pre-configured profiles for specific website types:
39
+ - **docs.yaml**: For documentation sites (40 pages, 5 depth)
40
+ - **ecommerce.yaml**: For e-commerce (35 pages, 4 depth, with revenue tracking)
41
+ - **landing-demo.yaml**: For landing pages (20 pages, 2 depth)
42
+ - **marketing.yaml**: For marketing sites (20 pages, 3 depth)
43
+ - **saas.yaml**: For SaaS apps (25 pages, 3 depth, with auth)
44
+
45
+ ## Usage
46
+
47
+ Guardian automatically discovers these files in the following order:
48
+ 1. `config/guardian.policy.json` (preferred)
49
+ 2. `guardian.policy.json` (legacy)
50
+ 3. `.odavl-guardian/guardian.policy.json` (CI/CD)
51
+
52
+ ## Customization
53
+
54
+ Edit these files to:
55
+ - Adjust safety rules and URL patterns
56
+ - Change policy severity thresholds
57
+ - Modify performance baselines
58
+ - Configure auth requirements
59
+ - Set revenue tracking paths
@@ -0,0 +1,16 @@
1
+ # Guardian profile template: Landing/Demo
2
+ baseUrl: https://example.com
3
+ profile: landing-demo
4
+ mode: SAFE
5
+ maxPages: 20
6
+ maxDepth: 2
7
+ slowThresholdMs: 8000
8
+ traceEnabled: false
9
+ harEnabled: false
10
+ auth:
11
+ enabled: false
12
+ revenue:
13
+ enabled: false
14
+ regression:
15
+ enabled: false
16
+ policyPack: landing-demo
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@odavl/guardian",
3
- "version": "0.1.0-rc1",
3
+ "version": "1.0.0",
4
4
  "description": "ODAVL Guardian — Market Reality Testing Engine with Visual Diffs, Behavioral Signals, Auto-Discovery, Intelligence, and CI/CD Integration",
5
5
  "license": "MIT",
6
6
  "author": "ODAVL",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/odavlstudio/odavlguardian"
9
+ "url": "git+https://github.com/odavlstudio/odavlguardian.git"
10
10
  },
11
11
  "keywords": [
12
12
  "testing",
@@ -24,24 +24,27 @@
24
24
  "bin": {
25
25
  "guardian": "bin/guardian.js"
26
26
  },
27
+ "main": "src/guardian/index.js",
27
28
  "files": [
28
29
  "bin/",
29
30
  "src/",
30
31
  "flows/",
31
32
  "policies/",
32
- "guardian.config.json",
33
- "guardian.policy.json",
34
- "guardian.profile.docs.yaml",
35
- "guardian.profile.ecommerce.yaml",
36
- "guardian.profile.marketing.yaml",
37
- "guardian.profile.saas.yaml",
33
+ "config/",
38
34
  "guardian-contract-v1.md",
39
35
  "README.md",
40
36
  "LICENSE",
41
- "CHANGELOG.md"
37
+ "CHANGELOG.md",
38
+ "SECURITY.md",
39
+ "SUPPORT.md",
40
+ "MAINTAINERS.md",
41
+ "VERSIONING.md"
42
42
  ],
43
43
  "scripts": {
44
44
  "test": "node test/mvp.test.js",
45
+ "test:journey:unit": "node test/journey-scanner.unit.test.js",
46
+ "test:journey:integration": "node test/journey-scanner.integration.test.js",
47
+ "test:journey:all": "node test/journey-scanner.unit.test.js && node test/journey-scanner.integration.test.js",
45
48
  "test:phase0": "node test/phase0-reality-lock.test.js",
46
49
  "test:phase1": "node test/phase1-baseline-expansion.test.js",
47
50
  "test:phase2": "node test/phase2-auto-attempts.test.js",
@@ -59,11 +62,23 @@
59
62
  "test:phase5:evidence": "node test/phase5-evidence-run.test.js",
60
63
  "test:phase5:all": "node test/phase5-visual.test.js && node test/phase5-evidence-run.test.js",
61
64
  "test:phase6": "node test/phase6.test.js && node test/phase6-product.test.js",
62
- "test:all": "node test/phase0-reality-lock.test.js && node test/mvp.test.js && node test/phase2.test.js && node test/attempt.test.js && node test/reality.test.js && node test/baseline.test.js && node test/baseline-junit.test.js && node test/snapshot.test.js && node test/soft-failures.test.js && node test/market-criticality.test.js && node test/discovery.test.js && node test/phase5.test.js && node test/phase5-visual.test.js && node test/phase5-evidence-run.test.js && node test/phase6.test.js",
63
- "start": "node bin/guardian.js"
65
+ "test:trust-seal": "node test/trust-seal.test.js",
66
+ "test:narrative-run": "node test/narrative-run.test.js",
67
+ "test:hardening": "node test/trust-seal.test.js && node test/narrative-run.test.js",
68
+ "test:wave1-3": "npx mocha test/success-evaluator.unit.test.js test/wave1-3-success-e2e.test.js --timeout 30000",
69
+ "test:all": "node test/phase0-reality-lock.test.js && node test/mvp.test.js && node test/phase2.test.js && node test/attempt.test.js && node test/reality.test.js && node test/baseline.test.js && node test/baseline-junit.test.js && node test/snapshot.test.js && node test/soft-failures.test.js && node test/market-criticality.test.js && node test/discovery.test.js && node test/phase5.test.js && node test/phase5-visual.test.js && node test/phase5-evidence-run.test.js && node test/phase6.test.js && node test/trust-seal.test.js && node test/narrative-run.test.js",
70
+ "release:dry": "npm run test:guardian && node bin/guardian.js --help && node bin/guardian.js --version",
71
+ "test:guardian": "npx jest test/guardian/stability-scorer.unit.test.js test/guardian/stability.integration.test.js test/guardian/alert-ledger.unit.test.js test/guardian/severity-classification.unit.test.js --testTimeout=50000",
72
+ "start": "node bin/guardian.js",
73
+ "sample:generate": "node scripts/generate-sample.js",
74
+ "template:verify": "node scripts/verify-templates.js"
64
75
  },
65
76
  "dependencies": {
66
77
  "express": "^5.2.1",
67
78
  "playwright": "^1.48.2"
79
+ },
80
+ "devDependencies": {
81
+ "archiver": "^6.0.1",
82
+ "mocha": "^11.7.5"
68
83
  }
69
84
  }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "Landing/Demo Policy",
3
+ "description": "Focused policy for landing pages, demo sites, and pre-launch products. Disables account and revenue flows, emphasizes navigation and CTA integrity.",
4
+ "failOnSeverity": "CRITICAL",
5
+ "maxWarnings": 5,
6
+ "maxInfo": 999,
7
+ "maxTotalRisk": 999,
8
+ "failOnNewRegression": false,
9
+ "failOnSoftFailures": false,
10
+ "softFailureThreshold": 999,
11
+ "requireBaseline": false,
12
+ "disabledAttempts": ["signup", "login", "checkout", "newsletter_signup"],
13
+ "focusAreas": {
14
+ "broken_pages": "CRITICAL",
15
+ "dead_links": "CRITICAL",
16
+ "seo_fundamentals": "WARNING",
17
+ "trust_signals": "WARNING",
18
+ "cta_integrity": "CRITICAL",
19
+ "performance": "WARNING"
20
+ },
21
+ "notes": "Landing/Demo sites should emphasize navigation integrity and CTA validity. Revenue-related issues are deprioritized. Account signup/login and checkout are marked NOT_APPLICABLE."
22
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Phase 11: Audit Logging System
3
+ * Immutable, append-only logs for compliance and accountability
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const AUDIT_DIR = path.join(os.homedir(), '.odavl-guardian', 'audit');
11
+
12
+ // Ensure audit directory exists
13
+ function ensureAuditDir() {
14
+ if (!fs.existsSync(AUDIT_DIR)) {
15
+ fs.mkdirSync(AUDIT_DIR, { recursive: true });
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Get current audit log file path (monthly rotation)
21
+ */
22
+ function getAuditLogPath() {
23
+ ensureAuditDir();
24
+
25
+ const now = new Date();
26
+ const year = now.getFullYear();
27
+ const month = String(now.getMonth() + 1).padStart(2, '0');
28
+ const filename = `audit-${year}-${month}.jsonl`;
29
+
30
+ return path.join(AUDIT_DIR, filename);
31
+ }
32
+
33
+ /**
34
+ * Write an audit log entry
35
+ */
36
+ function logAudit(action, details = {}) {
37
+ const entry = {
38
+ timestamp: new Date().toISOString(),
39
+ user: os.userInfo().username || 'unknown',
40
+ action,
41
+ details,
42
+ hostname: os.hostname(),
43
+ };
44
+
45
+ const logPath = getAuditLogPath();
46
+ const logLine = JSON.stringify(entry) + '\n';
47
+
48
+ // Append-only write (immutable)
49
+ fs.appendFileSync(logPath, logLine, 'utf-8');
50
+
51
+ return entry;
52
+ }
53
+
54
+ /**
55
+ * Read audit logs (optionally filtered)
56
+ */
57
+ function readAuditLogs(options = {}) {
58
+ ensureAuditDir();
59
+
60
+ const {
61
+ action = null,
62
+ user = null,
63
+ startDate = null,
64
+ endDate = null,
65
+ limit = 1000,
66
+ } = options;
67
+
68
+ const logs = [];
69
+ const files = fs.readdirSync(AUDIT_DIR)
70
+ .filter(f => f.startsWith('audit-') && f.endsWith('.jsonl'))
71
+ .sort()
72
+ .reverse(); // Most recent first
73
+
74
+ for (const file of files) {
75
+ const filePath = path.join(AUDIT_DIR, file);
76
+ const content = fs.readFileSync(filePath, 'utf-8');
77
+ const lines = content.split('\n').filter(l => l.trim());
78
+
79
+ for (const line of lines) {
80
+ try {
81
+ const entry = JSON.parse(line);
82
+
83
+ // Apply filters
84
+ if (action && entry.action !== action) continue;
85
+ if (user && entry.user !== user) continue;
86
+ if (startDate && new Date(entry.timestamp) < new Date(startDate)) continue;
87
+ if (endDate && new Date(entry.timestamp) > new Date(endDate)) continue;
88
+
89
+ logs.push(entry);
90
+
91
+ if (logs.length >= limit) {
92
+ return logs;
93
+ }
94
+ } catch (error) {
95
+ // Skip invalid lines
96
+ continue;
97
+ }
98
+ }
99
+ }
100
+
101
+ return logs;
102
+ }
103
+
104
+ /**
105
+ * Get audit summary
106
+ */
107
+ function getAuditSummary() {
108
+ const logs = readAuditLogs({ limit: 10000 });
109
+
110
+ const actionCounts = {};
111
+ const userCounts = {};
112
+
113
+ for (const log of logs) {
114
+ actionCounts[log.action] = (actionCounts[log.action] || 0) + 1;
115
+ userCounts[log.user] = (userCounts[log.user] || 0) + 1;
116
+ }
117
+
118
+ return {
119
+ totalLogs: logs.length,
120
+ actionCounts,
121
+ userCounts,
122
+ firstLog: logs[logs.length - 1]?.timestamp || null,
123
+ lastLog: logs[0]?.timestamp || null,
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Common audit actions (constants)
129
+ */
130
+ const AUDIT_ACTIONS = {
131
+ SCAN_RUN: 'scan:run',
132
+ SCAN_VIEW: 'scan:view',
133
+ LIVE_START: 'live:start',
134
+ LIVE_STOP: 'live:stop',
135
+ SITE_ADD: 'site:add',
136
+ SITE_REMOVE: 'site:remove',
137
+ PLAN_UPGRADE: 'plan:upgrade',
138
+ USER_ADD: 'user:add',
139
+ USER_REMOVE: 'user:remove',
140
+ EXPORT_PDF: 'export:pdf',
141
+ RECIPE_IMPORT: 'recipe:import',
142
+ RECIPE_EXPORT: 'recipe:export',
143
+ RECIPE_REMOVE: 'recipe:remove',
144
+ };
145
+
146
+ /**
147
+ * Reset audit logs (for testing only)
148
+ */
149
+ function resetAuditLogs() {
150
+ if (fs.existsSync(AUDIT_DIR)) {
151
+ const files = fs.readdirSync(AUDIT_DIR);
152
+ for (const file of files) {
153
+ if (file.startsWith('audit-') && file.endsWith('.jsonl')) {
154
+ fs.unlinkSync(path.join(AUDIT_DIR, file));
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ module.exports = {
161
+ logAudit,
162
+ readAuditLogs,
163
+ getAuditSummary,
164
+ AUDIT_ACTIONS,
165
+ resetAuditLogs,
166
+ };
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Phase 11: PDF Export for Reports
3
+ * Generate executive-ready PDF reports from Guardian scans
4
+ * Phase C: Real PDF rendering using Playwright
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { GuardianBrowser } = require('../guardian/browser');
10
+
11
+ /**
12
+ * Convert HTML report to real PDF using Playwright
13
+ * Produces binary PDF with valid %PDF header
14
+ */
15
+ async function generatePDFReal(reportPath, outputPath) {
16
+ // Read the HTML report
17
+ if (!fs.existsSync(reportPath)) {
18
+ throw new Error(`Report not found: ${reportPath}`);
19
+ }
20
+
21
+ const html = fs.readFileSync(reportPath, 'utf-8');
22
+ const metadata = extractReportMetadata(html);
23
+
24
+ // Use Playwright to render to PDF
25
+ const browser = new GuardianBrowser();
26
+
27
+ try {
28
+ await browser.launch(30000, { headless: true });
29
+
30
+ // Set content as HTML
31
+ await browser.page.setContent(html, { waitUntil: 'networkidle' });
32
+
33
+ // Render to PDF (binary format with %PDF header)
34
+ const pdfBuffer = await browser.page.pdf({
35
+ format: 'A4',
36
+ margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
37
+ });
38
+
39
+ // Write binary PDF
40
+ fs.writeFileSync(outputPath, pdfBuffer);
41
+
42
+ await browser.close();
43
+
44
+ return {
45
+ outputPath,
46
+ size: fs.statSync(outputPath).size,
47
+ metadata,
48
+ format: 'application/pdf',
49
+ isRealPDF: true,
50
+ };
51
+ } catch (error) {
52
+ await browser.close().catch(() => {});
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Convert HTML report to PDF
59
+ * Wrapper that uses real PDF rendering
60
+ */
61
+ function generatePDF(reportPath, outputPath) {
62
+ // For synchronous API compatibility, we use generatePDFReal async version
63
+ // This function should be called via async wrapper
64
+ throw new Error('Use generatePDFAsync for real PDF rendering');
65
+ }
66
+
67
+ /**
68
+ * Async version - generates real PDF
69
+ */
70
+ async function generatePDFAsync(reportPath, outputPath) {
71
+ return generatePDFReal(reportPath, outputPath);
72
+ }
73
+
74
+ /**
75
+ * Extract metadata from HTML report
76
+ */
77
+ function extractReportMetadata(html) {
78
+ const metadata = {
79
+ url: extractBetween(html, '<h2>URL:', '</h2>') || 'Unknown',
80
+ timestamp: extractBetween(html, '<p>Scanned:', '</p>') || 'Unknown',
81
+ verdict: extractVerdict(html),
82
+ riskLevel: extractRiskLevel(html),
83
+ flags: extractFlags(html),
84
+ summary: extractBetween(html, '<h3>Executive Summary</h3>', '<h3>') || '',
85
+ };
86
+
87
+ return metadata;
88
+ }
89
+
90
+ /**
91
+ * Extract text between two markers
92
+ */
93
+ function extractBetween(text, start, end) {
94
+ const startIndex = text.indexOf(start);
95
+ if (startIndex === -1) return null;
96
+
97
+ const searchStart = startIndex + start.length;
98
+ const endIndex = text.indexOf(end, searchStart);
99
+ if (endIndex === -1) return null;
100
+
101
+ return text.substring(searchStart, endIndex).trim().replace(/<[^>]*>/g, '');
102
+ }
103
+
104
+ /**
105
+ * Extract verdict from HTML
106
+ */
107
+ function extractVerdict(html) {
108
+ if (html.includes('PASSED')) return 'PASSED';
109
+ if (html.includes('FAILED')) return 'FAILED';
110
+ if (html.includes('WARN')) return 'WARN';
111
+ return 'UNKNOWN';
112
+ }
113
+
114
+ /**
115
+ * Extract risk level
116
+ */
117
+ function extractRiskLevel(html) {
118
+ if (html.includes('HIGH RISK')) return 'HIGH';
119
+ if (html.includes('MEDIUM RISK')) return 'MEDIUM';
120
+ if (html.includes('LOW RISK')) return 'LOW';
121
+ return 'UNKNOWN';
122
+ }
123
+
124
+ /**
125
+ * Extract flags from report
126
+ */
127
+ function extractFlags(html) {
128
+ const flags = [];
129
+
130
+ const flagSection = extractBetween(html, '<h3>Flags Detected</h3>', '<h3>');
131
+ if (!flagSection) return flags;
132
+
133
+ const lines = flagSection.split('\n');
134
+ for (const line of lines) {
135
+ const match = line.match(/([A-Z_]+):/);
136
+ if (match) {
137
+ flags.push(match[1]);
138
+ }
139
+ }
140
+
141
+ return flags;
142
+ }
143
+
144
+ /**
145
+ * Generate simplified PDF (text format)
146
+ * In production, use a proper PDF library
147
+ */
148
+ function generateSimplifiedPDF(metadata) {
149
+ const lines = [
150
+ '='.repeat(80),
151
+ 'GUARDIAN SECURITY REPORT',
152
+ '='.repeat(80),
153
+ '',
154
+ `URL: ${metadata.url}`,
155
+ `Scanned: ${metadata.timestamp}`,
156
+ `Verdict: ${metadata.verdict}`,
157
+ `Risk Level: ${metadata.riskLevel}`,
158
+ '',
159
+ '-'.repeat(80),
160
+ 'EXECUTIVE SUMMARY',
161
+ '-'.repeat(80),
162
+ '',
163
+ metadata.summary || 'No summary available',
164
+ '',
165
+ '-'.repeat(80),
166
+ 'FLAGS DETECTED',
167
+ '-'.repeat(80),
168
+ '',
169
+ ];
170
+
171
+ if (metadata.flags.length === 0) {
172
+ lines.push('No flags detected');
173
+ } else {
174
+ for (const flag of metadata.flags) {
175
+ lines.push(`- ${flag}`);
176
+ }
177
+ }
178
+
179
+ lines.push('');
180
+ lines.push('='.repeat(80));
181
+ lines.push('Generated by Guardian - Enterprise Edition');
182
+ lines.push('='.repeat(80));
183
+
184
+ return lines.join('\n');
185
+ }
186
+
187
+ /**
188
+ * Export report to PDF (async)
189
+ * Main entry point - uses real PDF rendering
190
+ */
191
+ async function exportReportToPDFAsync(reportId, outputDir = null) {
192
+ // Find report file
193
+ const artifactsDir = path.join(process.cwd(), 'artifacts');
194
+
195
+ let reportPath = null;
196
+
197
+ // Try direct path first
198
+ if (fs.existsSync(reportId)) {
199
+ reportPath = reportId;
200
+ } else {
201
+ // Search in artifacts
202
+ const reportFile = path.join(artifactsDir, reportId, 'index.html');
203
+ if (fs.existsSync(reportFile)) {
204
+ reportPath = reportFile;
205
+ }
206
+ }
207
+
208
+ if (!reportPath) {
209
+ throw new Error(`Report not found: ${reportId}`);
210
+ }
211
+
212
+ // Generate output path
213
+ const basename = path.basename(reportPath, '.html');
214
+ const outputFilename = `${basename}-report.pdf`;
215
+ const outputPath = outputDir
216
+ ? path.join(outputDir, outputFilename)
217
+ : path.join(path.dirname(reportPath), outputFilename);
218
+
219
+ return generatePDFAsync(reportPath, outputPath);
220
+ }
221
+
222
+ /**
223
+ * Legacy synchronous version (for backward compatibility)
224
+ */
225
+ function exportReportToPDF(reportId, outputDir = null) {
226
+ throw new Error('Use exportReportToPDFAsync for PDF export');
227
+ }
228
+
229
+ /**
230
+ * List available reports for export
231
+ */
232
+ function listAvailableReports() {
233
+ const artifactsDir = path.join(process.cwd(), 'artifacts');
234
+
235
+ if (!fs.existsSync(artifactsDir)) {
236
+ return [];
237
+ }
238
+
239
+ const reports = [];
240
+ const entries = fs.readdirSync(artifactsDir, { withFileTypes: true });
241
+
242
+ for (const entry of entries) {
243
+ if (entry.isDirectory()) {
244
+ const reportFile = path.join(artifactsDir, entry.name, 'index.html');
245
+ if (fs.existsSync(reportFile)) {
246
+ const stats = fs.statSync(reportFile);
247
+ reports.push({
248
+ id: entry.name,
249
+ path: reportFile,
250
+ modifiedAt: stats.mtime.toISOString(),
251
+ });
252
+ }
253
+ }
254
+ }
255
+
256
+ return reports.sort((a, b) =>
257
+ new Date(b.modifiedAt) - new Date(a.modifiedAt)
258
+ );
259
+ }
260
+
261
+ module.exports = {
262
+ exportReportToPDF,
263
+ exportReportToPDFAsync,
264
+ listAvailableReports,
265
+ generatePDF: generatePDFAsync,
266
+ generatePDFAsync,
267
+ };