@odavl/guardian 0.1.0-rc1
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 +20 -0
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/bin/guardian.js +690 -0
- package/flows/example-login-flow.json +36 -0
- package/flows/example-signup-flow.json +44 -0
- package/guardian-contract-v1.md +149 -0
- package/guardian.config.json +54 -0
- package/guardian.policy.json +12 -0
- package/guardian.profile.docs.yaml +18 -0
- package/guardian.profile.ecommerce.yaml +17 -0
- package/guardian.profile.marketing.yaml +18 -0
- package/guardian.profile.saas.yaml +21 -0
- package/package.json +69 -0
- package/policies/enterprise.json +12 -0
- package/policies/saas.json +12 -0
- package/policies/startup.json +12 -0
- package/src/guardian/attempt-engine.js +454 -0
- package/src/guardian/attempt-registry.js +227 -0
- package/src/guardian/attempt-reporter.js +507 -0
- package/src/guardian/attempt.js +227 -0
- package/src/guardian/auto-attempt-builder.js +283 -0
- package/src/guardian/baseline-reporter.js +143 -0
- package/src/guardian/baseline-storage.js +285 -0
- package/src/guardian/baseline.js +492 -0
- package/src/guardian/behavioral-signals.js +261 -0
- package/src/guardian/breakage-intelligence.js +223 -0
- package/src/guardian/browser.js +92 -0
- package/src/guardian/cli-summary.js +141 -0
- package/src/guardian/crawler.js +142 -0
- package/src/guardian/discovery-engine.js +661 -0
- package/src/guardian/enhanced-html-reporter.js +305 -0
- package/src/guardian/failure-taxonomy.js +169 -0
- package/src/guardian/flow-executor.js +374 -0
- package/src/guardian/flow-registry.js +67 -0
- package/src/guardian/html-reporter.js +414 -0
- package/src/guardian/index.js +218 -0
- package/src/guardian/init-command.js +139 -0
- package/src/guardian/junit-reporter.js +264 -0
- package/src/guardian/market-criticality.js +335 -0
- package/src/guardian/market-reporter.js +305 -0
- package/src/guardian/network-trace.js +178 -0
- package/src/guardian/policy.js +357 -0
- package/src/guardian/preset-loader.js +148 -0
- package/src/guardian/reality.js +547 -0
- package/src/guardian/reporter.js +181 -0
- package/src/guardian/root-cause-analysis.js +171 -0
- package/src/guardian/safety.js +248 -0
- package/src/guardian/scan-presets.js +60 -0
- package/src/guardian/screenshot.js +152 -0
- package/src/guardian/sitemap.js +225 -0
- package/src/guardian/snapshot-schema.js +266 -0
- package/src/guardian/snapshot.js +327 -0
- package/src/guardian/validators.js +323 -0
- package/src/guardian/visual-diff.js +247 -0
- package/src/guardian/webhook.js +206 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Summary Module
|
|
3
|
+
*
|
|
4
|
+
* Generate human-friendly CLI summaries at the end of Guardian runs.
|
|
5
|
+
* Shows critical info, top risks, and actionable next steps.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate final CLI summary
|
|
10
|
+
* @param {object} snapshot - Guardian snapshot
|
|
11
|
+
* @param {object} policyEval - Policy evaluation result
|
|
12
|
+
* @returns {string} Formatted CLI summary
|
|
13
|
+
*/
|
|
14
|
+
function generateCliSummary(snapshot, policyEval) {
|
|
15
|
+
if (!snapshot) {
|
|
16
|
+
return 'No snapshot data available.';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const meta = snapshot.meta || {};
|
|
20
|
+
const marketImpact = snapshot.marketImpactSummary || {};
|
|
21
|
+
const counts = marketImpact.countsBySeverity || { CRITICAL: 0, WARNING: 0, INFO: 0 };
|
|
22
|
+
const topRisks = marketImpact.topRisks || [];
|
|
23
|
+
const attempts = snapshot.attempts || [];
|
|
24
|
+
const discovery = snapshot.discovery || {};
|
|
25
|
+
|
|
26
|
+
let output = '\n';
|
|
27
|
+
output += 'â'.repeat(70) + '\n';
|
|
28
|
+
output += 'đĄī¸ Guardian Reality Summary\n';
|
|
29
|
+
output += 'â'.repeat(70) + '\n\n';
|
|
30
|
+
|
|
31
|
+
// Target URL
|
|
32
|
+
output += `Target: ${meta.url || 'unknown'}\n`;
|
|
33
|
+
output += `Run ID: ${meta.runId || 'unknown'}\n\n`;
|
|
34
|
+
|
|
35
|
+
// Risk Counts
|
|
36
|
+
output += 'đ Risk Summary:\n';
|
|
37
|
+
output += ` đ¨ CRITICAL: ${counts.CRITICAL}`;
|
|
38
|
+
if (counts.CRITICAL > 0) output += ' (Revenue impact)';
|
|
39
|
+
output += '\n';
|
|
40
|
+
output += ` â ī¸ WARNING: ${counts.WARNING}`;
|
|
41
|
+
if (counts.WARNING > 0) output += ' (User experience)';
|
|
42
|
+
output += '\n';
|
|
43
|
+
output += ` âšī¸ INFO: ${counts.INFO}`;
|
|
44
|
+
if (counts.INFO > 0) output += ' (Minor issues)';
|
|
45
|
+
output += '\n\n';
|
|
46
|
+
|
|
47
|
+
// Top Risks (up to 3)
|
|
48
|
+
if (topRisks.length > 0) {
|
|
49
|
+
// Preserve legacy label for backward compatibility
|
|
50
|
+
output += 'đĨ Top Risk:\n';
|
|
51
|
+
// New: compact list of top 3 issues
|
|
52
|
+
output += ' (Top Issues)\n';
|
|
53
|
+
topRisks.slice(0, 3).forEach((risk, idx) => {
|
|
54
|
+
output += ` ${idx + 1}. ${risk.humanReadableReason || 'Unknown issue'}\n`;
|
|
55
|
+
output += ` Impact: ${risk.impactScore || 0} (${risk.category || 'UNKNOWN'}) | Severity: ${risk.severity || 'INFO'}\n`;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Link evidence if available for the top-most
|
|
59
|
+
const topRisk = topRisks[0];
|
|
60
|
+
const relatedAttempt = attempts.find(a =>
|
|
61
|
+
a.attemptId === topRisk.attemptId ||
|
|
62
|
+
(topRisk.humanReadableReason || '').toLowerCase().includes(a.attemptName?.toLowerCase() || '')
|
|
63
|
+
);
|
|
64
|
+
if (relatedAttempt && relatedAttempt.evidence) {
|
|
65
|
+
output += ` Evidence: ${relatedAttempt.evidence.screenshotPath || 'See report'}\n`;
|
|
66
|
+
}
|
|
67
|
+
output += '\n';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Attempt Summary
|
|
71
|
+
const successfulAttempts = attempts.filter(a => a.outcome === 'SUCCESS').length;
|
|
72
|
+
const totalAttempts = attempts.length;
|
|
73
|
+
if (totalAttempts > 0) {
|
|
74
|
+
output += 'đ¯ Attempts:\n';
|
|
75
|
+
output += ` ${successfulAttempts}/${totalAttempts} successful`;
|
|
76
|
+
if (successfulAttempts < totalAttempts) {
|
|
77
|
+
output += ` (${totalAttempts - successfulAttempts} failed)`;
|
|
78
|
+
}
|
|
79
|
+
output += '\n\n';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Discovery Summary
|
|
83
|
+
if (discovery.pagesVisitedCount > 0) {
|
|
84
|
+
output += 'đ Discovery:\n';
|
|
85
|
+
output += ` Pages visited: ${discovery.pagesVisitedCount || 0}\n`;
|
|
86
|
+
output += ` Interactions discovered: ${discovery.interactionsDiscovered || 0}\n`;
|
|
87
|
+
output += ` Interactions executed: ${discovery.interactionsExecuted || 0}\n\n`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Policy Evaluation
|
|
91
|
+
if (policyEval) {
|
|
92
|
+
output += 'đĄī¸ Policy:\n';
|
|
93
|
+
if (policyEval.passed) {
|
|
94
|
+
output += ' â
PASSED - All checks satisfied\n';
|
|
95
|
+
} else {
|
|
96
|
+
output += ' â FAILED - Policy violations detected\n';
|
|
97
|
+
if (policyEval.reasons && policyEval.reasons.length > 0) {
|
|
98
|
+
output += ' Reasons:\n';
|
|
99
|
+
policyEval.reasons.slice(0, 3).forEach(reason => {
|
|
100
|
+
output += ` âĸ ${reason}\n`;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
output += ` Exit code: ${policyEval.exitCode || 0}\n\n`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Next Action
|
|
108
|
+
output += 'đ Next Action:\n';
|
|
109
|
+
if (counts.CRITICAL > 0) {
|
|
110
|
+
output += ' â ī¸ Fix the CRITICAL issue(s) before deploying.\n';
|
|
111
|
+
output += ' Review the top risk above and check evidence screenshots.\n';
|
|
112
|
+
} else if (counts.WARNING > 0) {
|
|
113
|
+
output += ' â ī¸ Review WARNING issues - they may impact user experience.\n';
|
|
114
|
+
output += ' Consider fixing before next release.\n';
|
|
115
|
+
} else if (policyEval && !policyEval.passed) {
|
|
116
|
+
output += ' â ī¸ Policy check failed. Review policy violations above.\n';
|
|
117
|
+
} else {
|
|
118
|
+
output += ' â
All checks passed! Site is ready for deployment.\n';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
output += '\n';
|
|
122
|
+
output += 'đ Full report: ' + (meta.runId ? `artifacts/${meta.runId}/` : 'See artifacts/\n');
|
|
123
|
+
output += '\n';
|
|
124
|
+
|
|
125
|
+
output += 'â'.repeat(70) + '\n';
|
|
126
|
+
|
|
127
|
+
return output;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Print summary to console
|
|
132
|
+
*/
|
|
133
|
+
function printCliSummary(snapshot, policyEval) {
|
|
134
|
+
const summary = generateCliSummary(snapshot, policyEval);
|
|
135
|
+
console.log(summary);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
generateCliSummary,
|
|
140
|
+
printCliSummary
|
|
141
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
class GuardianCrawler {
|
|
2
|
+
constructor(baseUrl, maxPages = 25, maxDepth = 3) {
|
|
3
|
+
this.baseUrl = new URL(baseUrl);
|
|
4
|
+
this.maxPages = maxPages;
|
|
5
|
+
this.maxDepth = maxDepth;
|
|
6
|
+
|
|
7
|
+
this.visited = new Set();
|
|
8
|
+
this.discovered = new Set();
|
|
9
|
+
this.toVisit = ['/'];
|
|
10
|
+
this.pages = [];
|
|
11
|
+
|
|
12
|
+
// Phase 2 features
|
|
13
|
+
this.screenshot = null; // Will be set by main engine
|
|
14
|
+
this.safety = null; // Will be set by main engine
|
|
15
|
+
this.artifactsDir = null; // Will be set during crawl
|
|
16
|
+
this.safetyStats = { urlsBlocked: 0 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
isSameOrigin(url) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = new URL(url, this.baseUrl);
|
|
22
|
+
return parsed.origin === this.baseUrl.origin;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getPathname(url) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(url, this.baseUrl);
|
|
31
|
+
return parsed.pathname;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async crawl(browser, artifactsDir = null) {
|
|
38
|
+
this.artifactsDir = artifactsDir;
|
|
39
|
+
let depth = 0;
|
|
40
|
+
let currentDepthUrls = ['/'];
|
|
41
|
+
|
|
42
|
+
while (currentDepthUrls.length > 0 && depth < this.maxDepth && this.visited.size < this.maxPages) {
|
|
43
|
+
const nextDepthUrls = [];
|
|
44
|
+
|
|
45
|
+
for (const pathname of currentDepthUrls) {
|
|
46
|
+
if (this.visited.size >= this.maxPages) break;
|
|
47
|
+
if (this.visited.has(pathname)) continue;
|
|
48
|
+
|
|
49
|
+
// Safety check
|
|
50
|
+
const fullUrl = new URL(pathname, this.baseUrl).toString();
|
|
51
|
+
if (this.safety) {
|
|
52
|
+
const safetyCheck = this.safety.isUrlSafe(fullUrl);
|
|
53
|
+
if (!safetyCheck.safe) {
|
|
54
|
+
console.log(`đĄī¸ Blocked: ${pathname} (${safetyCheck.reason})`);
|
|
55
|
+
this.safetyStats.urlsBlocked++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.visited.add(pathname);
|
|
61
|
+
this.discovered.add(pathname);
|
|
62
|
+
|
|
63
|
+
const result = await browser.navigate(fullUrl);
|
|
64
|
+
|
|
65
|
+
if (result.success) {
|
|
66
|
+
const links = await browser.getLinks();
|
|
67
|
+
|
|
68
|
+
// Capture screenshot if enabled
|
|
69
|
+
let screenshotFile = null;
|
|
70
|
+
if (this.screenshot && this.artifactsDir) {
|
|
71
|
+
screenshotFile = await this.screenshot.captureForCrawl(
|
|
72
|
+
browser.page,
|
|
73
|
+
fullUrl,
|
|
74
|
+
this.pages.length + 1,
|
|
75
|
+
this.artifactsDir
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const pageRecord = {
|
|
80
|
+
index: this.pages.length + 1,
|
|
81
|
+
url: fullUrl,
|
|
82
|
+
pathname: pathname,
|
|
83
|
+
status: result.status,
|
|
84
|
+
links: links.length,
|
|
85
|
+
linkCount: links.length, // Deprecated, use 'links'
|
|
86
|
+
depth: depth,
|
|
87
|
+
screenshot: screenshotFile,
|
|
88
|
+
timestamp: new Date().toISOString()
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.pages.push(pageRecord);
|
|
92
|
+
|
|
93
|
+
// Extract unique new links
|
|
94
|
+
for (const link of links) {
|
|
95
|
+
if (!this.isSameOrigin(link.href)) continue;
|
|
96
|
+
|
|
97
|
+
const newPathname = this.getPathname(link.href);
|
|
98
|
+
if (!newPathname || this.discovered.has(newPathname)) continue;
|
|
99
|
+
|
|
100
|
+
this.discovered.add(newPathname);
|
|
101
|
+
if (!this.visited.has(newPathname)) {
|
|
102
|
+
nextDepthUrls.push(newPathname);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
// Still record failed visits
|
|
107
|
+
this.pages.push({
|
|
108
|
+
index: this.pages.length + 1,
|
|
109
|
+
url: fullUrl,
|
|
110
|
+
pathname: pathname,
|
|
111
|
+
status: null,
|
|
112
|
+
links: 0,
|
|
113
|
+
linkCount: 0,
|
|
114
|
+
depth: depth,
|
|
115
|
+
error: result.error,
|
|
116
|
+
screenshot: null,
|
|
117
|
+
timestamp: new Date().toISOString()
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
currentDepthUrls = nextDepthUrls;
|
|
123
|
+
depth++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add discovered but not visited pages
|
|
127
|
+
for (const pathname of this.discovered) {
|
|
128
|
+
if (!this.visited.has(pathname)) {
|
|
129
|
+
// Do nothing, we'll handle in report
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
visited: this.pages,
|
|
135
|
+
totalDiscovered: this.discovered.size,
|
|
136
|
+
totalVisited: this.visited.size,
|
|
137
|
+
safetyStats: this.safetyStats
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { GuardianCrawler };
|