@odavl/guardian 0.2.0 → 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.
- package/CHANGELOG.md +86 -2
- package/README.md +155 -97
- package/bin/guardian.js +1345 -60
- package/config/README.md +59 -0
- package/config/profiles/landing-demo.yaml +16 -0
- package/package.json +21 -11
- package/policies/landing-demo.json +22 -0
- package/src/enterprise/audit-logger.js +166 -0
- package/src/enterprise/pdf-exporter.js +267 -0
- package/src/enterprise/rbac-gate.js +142 -0
- package/src/enterprise/rbac.js +239 -0
- package/src/enterprise/site-manager.js +180 -0
- package/src/founder/feedback-system.js +156 -0
- package/src/founder/founder-tracker.js +213 -0
- package/src/founder/usage-signals.js +141 -0
- package/src/guardian/alert-ledger.js +121 -0
- package/src/guardian/attempt-engine.js +568 -7
- package/src/guardian/attempt-registry.js +42 -1
- package/src/guardian/attempt-relevance.js +106 -0
- package/src/guardian/attempt.js +24 -0
- package/src/guardian/baseline.js +12 -4
- package/src/guardian/breakage-intelligence.js +1 -0
- package/src/guardian/ci-cli.js +121 -0
- package/src/guardian/ci-output.js +4 -3
- package/src/guardian/cli-summary.js +79 -92
- package/src/guardian/config-loader.js +162 -0
- package/src/guardian/drift-detector.js +100 -0
- package/src/guardian/enhanced-html-reporter.js +221 -4
- package/src/guardian/env-guard.js +127 -0
- package/src/guardian/failure-intelligence.js +173 -0
- package/src/guardian/first-run-profile.js +89 -0
- package/src/guardian/first-run.js +6 -1
- package/src/guardian/flag-validator.js +17 -3
- package/src/guardian/html-reporter.js +2 -0
- package/src/guardian/human-reporter.js +431 -0
- package/src/guardian/index.js +22 -19
- package/src/guardian/init-command.js +9 -5
- package/src/guardian/intent-detector.js +146 -0
- package/src/guardian/journey-definitions.js +132 -0
- package/src/guardian/journey-scan-cli.js +145 -0
- package/src/guardian/journey-scanner.js +583 -0
- package/src/guardian/junit-reporter.js +18 -1
- package/src/guardian/live-cli.js +95 -0
- package/src/guardian/live-scheduler-runner.js +137 -0
- package/src/guardian/live-scheduler.js +146 -0
- package/src/guardian/market-reporter.js +341 -81
- package/src/guardian/pattern-analyzer.js +348 -0
- package/src/guardian/policy.js +80 -3
- package/src/guardian/preset-loader.js +9 -6
- package/src/guardian/reality.js +1278 -117
- package/src/guardian/reporter.js +27 -41
- package/src/guardian/run-artifacts.js +212 -0
- package/src/guardian/run-cleanup.js +207 -0
- package/src/guardian/run-latest.js +90 -0
- package/src/guardian/run-list.js +211 -0
- package/src/guardian/scan-presets.js +100 -11
- package/src/guardian/selector-fallbacks.js +394 -0
- package/src/guardian/semantic-contact-finder.js +2 -1
- package/src/guardian/site-introspection.js +257 -0
- package/src/guardian/smoke.js +2 -2
- package/src/guardian/snapshot-schema.js +25 -1
- package/src/guardian/snapshot.js +46 -2
- package/src/guardian/stability-scorer.js +169 -0
- package/src/guardian/template-command.js +184 -0
- package/src/guardian/text-formatters.js +426 -0
- package/src/guardian/verdict.js +320 -0
- package/src/guardian/verdicts.js +74 -0
- package/src/guardian/watch-runner.js +3 -7
- package/src/payments/stripe-checkout.js +169 -0
- package/src/plans/plan-definitions.js +148 -0
- package/src/plans/plan-manager.js +211 -0
- package/src/plans/usage-tracker.js +210 -0
- package/src/recipes/recipe-engine.js +188 -0
- package/src/recipes/recipe-failure-analysis.js +159 -0
- package/src/recipes/recipe-registry.js +134 -0
- package/src/recipes/recipe-runtime.js +507 -0
- package/src/recipes/recipe-store.js +410 -0
- package/guardian-contract-v1.md +0 -149
- /package/{guardian.config.json → config/guardian.config.json} +0 -0
- /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
- /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
- /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
- /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
- /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
package/config/README.md
ADDED
|
@@ -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.
|
|
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",
|
|
@@ -30,19 +30,21 @@
|
|
|
30
30
|
"src/",
|
|
31
31
|
"flows/",
|
|
32
32
|
"policies/",
|
|
33
|
-
"
|
|
34
|
-
"guardian.policy.json",
|
|
35
|
-
"guardian.profile.docs.yaml",
|
|
36
|
-
"guardian.profile.ecommerce.yaml",
|
|
37
|
-
"guardian.profile.marketing.yaml",
|
|
38
|
-
"guardian.profile.saas.yaml",
|
|
33
|
+
"config/",
|
|
39
34
|
"guardian-contract-v1.md",
|
|
40
35
|
"README.md",
|
|
41
36
|
"LICENSE",
|
|
42
|
-
"CHANGELOG.md"
|
|
37
|
+
"CHANGELOG.md",
|
|
38
|
+
"SECURITY.md",
|
|
39
|
+
"SUPPORT.md",
|
|
40
|
+
"MAINTAINERS.md",
|
|
41
|
+
"VERSIONING.md"
|
|
43
42
|
],
|
|
44
43
|
"scripts": {
|
|
45
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",
|
|
46
48
|
"test:phase0": "node test/phase0-reality-lock.test.js",
|
|
47
49
|
"test:phase1": "node test/phase1-baseline-expansion.test.js",
|
|
48
50
|
"test:phase2": "node test/phase2-auto-attempts.test.js",
|
|
@@ -60,15 +62,23 @@
|
|
|
60
62
|
"test:phase5:evidence": "node test/phase5-evidence-run.test.js",
|
|
61
63
|
"test:phase5:all": "node test/phase5-visual.test.js && node test/phase5-evidence-run.test.js",
|
|
62
64
|
"test:phase6": "node test/phase6.test.js && node test/phase6-product.test.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",
|
|
63
68
|
"test:wave1-3": "npx mocha test/success-evaluator.unit.test.js test/wave1-3-success-e2e.test.js --timeout 30000",
|
|
64
|
-
"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",
|
|
65
|
-
"
|
|
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"
|
|
66
75
|
},
|
|
67
76
|
"dependencies": {
|
|
68
77
|
"express": "^5.2.1",
|
|
69
78
|
"playwright": "^1.48.2"
|
|
70
79
|
},
|
|
71
80
|
"devDependencies": {
|
|
81
|
+
"archiver": "^6.0.1",
|
|
72
82
|
"mocha": "^11.7.5"
|
|
73
83
|
}
|
|
74
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
|
+
};
|