@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -0
  4. package/bin/guardian.js +690 -0
  5. package/flows/example-login-flow.json +36 -0
  6. package/flows/example-signup-flow.json +44 -0
  7. package/guardian-contract-v1.md +149 -0
  8. package/guardian.config.json +54 -0
  9. package/guardian.policy.json +12 -0
  10. package/guardian.profile.docs.yaml +18 -0
  11. package/guardian.profile.ecommerce.yaml +17 -0
  12. package/guardian.profile.marketing.yaml +18 -0
  13. package/guardian.profile.saas.yaml +21 -0
  14. package/package.json +69 -0
  15. package/policies/enterprise.json +12 -0
  16. package/policies/saas.json +12 -0
  17. package/policies/startup.json +12 -0
  18. package/src/guardian/attempt-engine.js +454 -0
  19. package/src/guardian/attempt-registry.js +227 -0
  20. package/src/guardian/attempt-reporter.js +507 -0
  21. package/src/guardian/attempt.js +227 -0
  22. package/src/guardian/auto-attempt-builder.js +283 -0
  23. package/src/guardian/baseline-reporter.js +143 -0
  24. package/src/guardian/baseline-storage.js +285 -0
  25. package/src/guardian/baseline.js +492 -0
  26. package/src/guardian/behavioral-signals.js +261 -0
  27. package/src/guardian/breakage-intelligence.js +223 -0
  28. package/src/guardian/browser.js +92 -0
  29. package/src/guardian/cli-summary.js +141 -0
  30. package/src/guardian/crawler.js +142 -0
  31. package/src/guardian/discovery-engine.js +661 -0
  32. package/src/guardian/enhanced-html-reporter.js +305 -0
  33. package/src/guardian/failure-taxonomy.js +169 -0
  34. package/src/guardian/flow-executor.js +374 -0
  35. package/src/guardian/flow-registry.js +67 -0
  36. package/src/guardian/html-reporter.js +414 -0
  37. package/src/guardian/index.js +218 -0
  38. package/src/guardian/init-command.js +139 -0
  39. package/src/guardian/junit-reporter.js +264 -0
  40. package/src/guardian/market-criticality.js +335 -0
  41. package/src/guardian/market-reporter.js +305 -0
  42. package/src/guardian/network-trace.js +178 -0
  43. package/src/guardian/policy.js +357 -0
  44. package/src/guardian/preset-loader.js +148 -0
  45. package/src/guardian/reality.js +547 -0
  46. package/src/guardian/reporter.js +181 -0
  47. package/src/guardian/root-cause-analysis.js +171 -0
  48. package/src/guardian/safety.js +248 -0
  49. package/src/guardian/scan-presets.js +60 -0
  50. package/src/guardian/screenshot.js +152 -0
  51. package/src/guardian/sitemap.js +225 -0
  52. package/src/guardian/snapshot-schema.js +266 -0
  53. package/src/guardian/snapshot.js +327 -0
  54. package/src/guardian/validators.js +323 -0
  55. package/src/guardian/visual-diff.js +247 -0
  56. package/src/guardian/webhook.js +206 -0
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Init Command
3
+ *
4
+ * One-command onboarding: `guardian init`
5
+ * Creates policy file, .gitignore, and prints next steps.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Initialize Guardian in current directory
13
+ * @param {object} options - Init options
14
+ * @returns {object} Result with created files
15
+ */
16
+ function initGuardian(options = {}) {
17
+ const cwd = options.cwd || process.cwd();
18
+ const policyPreset = options.preset || 'startup';
19
+
20
+ const result = {
21
+ created: [],
22
+ updated: [],
23
+ errors: []
24
+ };
25
+
26
+ console.log('\n🛡️ Initializing Guardian...\n');
27
+
28
+ // 1. Create policy file
29
+ try {
30
+ const policyPath = path.join(cwd, 'guardian.policy.json');
31
+
32
+ if (fs.existsSync(policyPath)) {
33
+ console.log('⚠️ guardian.policy.json already exists. Skipping.');
34
+ } else {
35
+ // Load preset
36
+ const presetsDir = path.join(__dirname, '../../policies');
37
+ const presetPath = path.join(presetsDir, `${policyPreset}.json`);
38
+
39
+ let policyContent;
40
+ if (fs.existsSync(presetPath)) {
41
+ policyContent = fs.readFileSync(presetPath, 'utf-8');
42
+ } else {
43
+ // Fallback to default
44
+ policyContent = JSON.stringify({
45
+ name: 'Default Policy',
46
+ description: 'Default Guardian policy',
47
+ failOnSeverity: 'CRITICAL',
48
+ maxWarnings: 999,
49
+ maxInfo: 999,
50
+ maxTotalRisk: 999,
51
+ failOnNewRegression: false,
52
+ failOnSoftFailures: false,
53
+ softFailureThreshold: 999,
54
+ requireBaseline: false
55
+ }, null, 2);
56
+ }
57
+
58
+ fs.writeFileSync(policyPath, policyContent, 'utf-8');
59
+ result.created.push('guardian.policy.json');
60
+ console.log('✅ Created guardian.policy.json');
61
+ }
62
+ } catch (error) {
63
+ result.errors.push(`Failed to create policy: ${error.message}`);
64
+ console.error(`❌ Failed to create policy: ${error.message}`);
65
+ }
66
+
67
+ // 2. Update .gitignore
68
+ try {
69
+ const gitignorePath = path.join(cwd, '.gitignore');
70
+ const guardianIgnores = [
71
+ '# Guardian artifacts',
72
+ 'artifacts/',
73
+ 'test-artifacts/',
74
+ '.odavl-guardian/',
75
+ 'tmp-artifacts/'
76
+ ].join('\n');
77
+
78
+ if (fs.existsSync(gitignorePath)) {
79
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
80
+ if (!content.includes('Guardian artifacts')) {
81
+ fs.appendFileSync(gitignorePath, '\n' + guardianIgnores + '\n');
82
+ result.updated.push('.gitignore');
83
+ console.log('✅ Updated .gitignore');
84
+ } else {
85
+ console.log('⚠️ .gitignore already has Guardian entries. Skipping.');
86
+ }
87
+ } else {
88
+ fs.writeFileSync(gitignorePath, guardianIgnores + '\n', 'utf-8');
89
+ result.created.push('.gitignore');
90
+ console.log('✅ Created .gitignore');
91
+ }
92
+ } catch (error) {
93
+ result.errors.push(`Failed to update .gitignore: ${error.message}`);
94
+ console.error(`❌ Failed to update .gitignore: ${error.message}`);
95
+ }
96
+
97
+ // 3. Print next steps
98
+ console.log('\n━'.repeat(60));
99
+ console.log('✅ Guardian initialized successfully!\n');
100
+ console.log('📝 Next steps:\n');
101
+ console.log(' 1. Run Guardian:');
102
+ console.log(' npm start -- --url https://your-site.com\n');
103
+ console.log(' 2. Or use the protect shortcut:');
104
+ console.log(' npm start -- protect https://your-site.com\n');
105
+ console.log(' 3. Review the generated report:');
106
+ console.log(' Check artifacts/ directory\n');
107
+ console.log(' 4. Customize policy:');
108
+ console.log(' Edit guardian.policy.json\n');
109
+ console.log(' 5. Integrate with CI/CD:');
110
+ console.log(' Use .github/workflows/guardian.yml as template\n');
111
+ console.log('━'.repeat(60) + '\n');
112
+
113
+ return result;
114
+ }
115
+
116
+ /**
117
+ * Detect project type
118
+ * @param {string} cwd - Current working directory
119
+ * @returns {string} Project type: 'node', 'empty', 'unknown'
120
+ */
121
+ function detectProjectType(cwd) {
122
+ const packageJsonPath = path.join(cwd, 'package.json');
123
+
124
+ if (fs.existsSync(packageJsonPath)) {
125
+ return 'node';
126
+ }
127
+
128
+ const files = fs.readdirSync(cwd);
129
+ if (files.length === 0) {
130
+ return 'empty';
131
+ }
132
+
133
+ return 'unknown';
134
+ }
135
+
136
+ module.exports = {
137
+ initGuardian,
138
+ detectProjectType
139
+ };
@@ -0,0 +1,264 @@
1
+ /**
2
+ * JUnit XML Reporter
3
+ *
4
+ * Generates JUnit-compatible XML for CI/CD integration.
5
+ * - Compatible with Jenkins, GitLab CI, GitHub Actions, etc.
6
+ * - Includes testcases for attempts and discovery results
7
+ * - Captures failures and system output
8
+ *
9
+ * NO AI. Deterministic XML generation.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ /**
16
+ * Escape XML special characters
17
+ */
18
+ function escapeXml(str) {
19
+ if (!str) return '';
20
+ return String(str)
21
+ .replace(/&/g, '&')
22
+ .replace(/</g, '&lt;')
23
+ .replace(/>/g, '&gt;')
24
+ .replace(/"/g, '&quot;')
25
+ .replace(/'/g, '&apos;');
26
+ }
27
+
28
+ /**
29
+ * Build JUnit XML from snapshot
30
+ */
31
+ function generateJunitXml(snapshot, baseUrl = '') {
32
+ const runId = snapshot.meta?.runId || 'unknown';
33
+ const createdAt = snapshot.meta?.createdAt || new Date().toISOString();
34
+ const url = baseUrl || snapshot.meta?.url || 'unknown';
35
+
36
+ let totalTests = 0;
37
+ let totalFailures = 0;
38
+ let totalSkipped = 0;
39
+ const testCases = [];
40
+
41
+ // ============================================================================
42
+ // ATTEMPTS AS TESTCASES (Phase 1, 2)
43
+ // ============================================================================
44
+
45
+ if (snapshot.attempts && Array.isArray(snapshot.attempts)) {
46
+ for (const attempt of snapshot.attempts) {
47
+ const attemptId = attempt.attemptId || 'unknown';
48
+ const attemptName = attempt.attemptName || attemptId;
49
+ const outcome = attempt.outcome || 'UNKNOWN';
50
+ const duration = (attempt.totalDurationMs || 0) / 1000; // Convert to seconds
51
+
52
+ totalTests++;
53
+
54
+ let testCase = ` <testcase name="${escapeXml(attemptName)}" classname="attempt.${escapeXml(attemptId)}" time="${duration}">\n`;
55
+
56
+ if (outcome === 'FAILURE') {
57
+ totalFailures++;
58
+ const failMsg = `Attempt failed: ${attempt.goal}`;
59
+ testCase += ` <failure message="${escapeXml(failMsg)}" type="AttemptFailure">\n`;
60
+ if (attempt.error) {
61
+ testCase += `${escapeXml(attempt.error)}\n`;
62
+ }
63
+ testCase += ` </failure>\n`;
64
+ } else if (outcome === 'FRICTION') {
65
+ const fricMsg = `Friction detected: ${attempt.friction?.summary || 'See logs'}`;
66
+ testCase += ` <skipped message="${escapeXml(fricMsg)}" />\n`;
67
+ totalSkipped++;
68
+ }
69
+
70
+ // Add soft failures as failure element
71
+ if (attempt.softFailureCount && attempt.softFailureCount > 0) {
72
+ totalFailures++;
73
+ testCase += ` <failure message="Soft failures detected" type="SoftFailure">\n`;
74
+ testCase += `${escapeXml(attempt.softFailureCount)} validator(s) failed\n`;
75
+
76
+ if (attempt.validators && Array.isArray(attempt.validators)) {
77
+ for (const validator of attempt.validators) {
78
+ if (validator.status === 'FAIL' || validator.status === 'WARN') {
79
+ testCase += ` - ${escapeXml(validator.id)}: ${escapeXml(validator.message)}\n`;
80
+ }
81
+ }
82
+ }
83
+
84
+ testCase += ` </failure>\n`;
85
+ }
86
+
87
+ testCase += ` </testcase>\n`;
88
+ testCases.push(testCase);
89
+ }
90
+ }
91
+
92
+ // ============================================================================
93
+ // DISCOVERY RESULTS AS TESTCASES (Phase 4)
94
+ // ============================================================================
95
+
96
+ if (snapshot.discovery?.results && Array.isArray(snapshot.discovery.results)) {
97
+ // Group results by outcome for cleaner reporting
98
+ const discoveryResults = snapshot.discovery.results;
99
+
100
+ // Summarize discovery: 1 testcase per outcome type
101
+ const successCount = discoveryResults.filter(r => r.outcome === 'SUCCESS').length;
102
+ const failureCount = discoveryResults.filter(r => r.outcome === 'FAILURE').length;
103
+ const frictionCount = discoveryResults.filter(r => r.outcome === 'FRICTION').length;
104
+
105
+ // Overall discovery testcase
106
+ totalTests++;
107
+ let discoveryTestCase = ` <testcase name="Discovery: Auto-Interaction Exploration" classname="discovery.autoexplore" time="0">\n`;
108
+
109
+ if (failureCount > 0) {
110
+ totalFailures++;
111
+ discoveryTestCase += ` <failure message="Discovery failures detected" type="DiscoveryFailure">\n`;
112
+ discoveryTestCase += `${failureCount} interaction(s) failed:\n`;
113
+
114
+ discoveryResults
115
+ .filter(r => r.outcome === 'FAILURE')
116
+ .slice(0, 5) // Top 5 failures
117
+ .forEach(r => {
118
+ discoveryTestCase += ` - ${escapeXml(r.interactionId || 'unknown')}: ${escapeXml(r.errorMessage || 'Unknown error')}\n`;
119
+ });
120
+
121
+ if (failureCount > 5) {
122
+ discoveryTestCase += ` ... and ${failureCount - 5} more\n`;
123
+ }
124
+
125
+ discoveryTestCase += ` </failure>\n`;
126
+ }
127
+
128
+ discoveryTestCase += ` </testcase>\n`;
129
+ testCases.push(discoveryTestCase);
130
+ }
131
+
132
+ // ============================================================================
133
+ // MARKET CRITICALITY AS TESTCASE (Phase 3)
134
+ // ============================================================================
135
+
136
+ const marketImpact = snapshot.marketImpactSummary || {};
137
+ if (Object.keys(marketImpact).length > 0) {
138
+ totalTests++;
139
+
140
+ let marketTestCase = ` <testcase name="Market Criticality Assessment" classname="market.criticality" time="0">\n`;
141
+
142
+ const criticalCount = marketImpact.countsBySeverity?.CRITICAL || 0;
143
+ const warningCount = marketImpact.countsBySeverity?.WARNING || 0;
144
+
145
+ if (criticalCount > 0) {
146
+ totalFailures++;
147
+ marketTestCase += ` <failure message="CRITICAL market risks detected" type="CriticalRisk">\n`;
148
+ marketTestCase += `${criticalCount} CRITICAL risk(s) found\n`;
149
+
150
+ if (marketImpact.topRisks && Array.isArray(marketImpact.topRisks)) {
151
+ marketImpact.topRisks.slice(0, 3).forEach(risk => {
152
+ marketTestCase += ` - ${escapeXml(risk.category || 'Unknown')}: ${escapeXml(risk.humanReadableReason || 'See logs')}\n`;
153
+ });
154
+ }
155
+
156
+ marketTestCase += ` </failure>\n`;
157
+ }
158
+
159
+ marketTestCase += ` </testcase>\n`;
160
+ testCases.push(marketTestCase);
161
+ }
162
+
163
+ // ============================================================================
164
+ // BUILD XML
165
+ // ============================================================================
166
+
167
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
168
+ xml += `<testsuites>\n`;
169
+ xml += ` <testsuite name="ODAVL Guardian" tests="${totalTests}" failures="${totalFailures}" skipped="${totalSkipped}" timestamp="${escapeXml(createdAt)}" time="0" hostname="guardian">\n`;
170
+
171
+ // Properties
172
+ xml += ` <properties>\n`;
173
+ xml += ` <property name="runId" value="${escapeXml(runId)}" />\n`;
174
+ xml += ` <property name="url" value="${escapeXml(url)}" />\n`;
175
+ xml += ` <property name="createdAt" value="${escapeXml(createdAt)}" />\n`;
176
+ xml += ` </properties>\n`;
177
+
178
+ // Testcases
179
+ xml += testCases.join('');
180
+
181
+ // System output
182
+ xml += ` <system-out>\n`;
183
+ xml += `ODAVL Guardian Test Run\n`;
184
+ xml += `URL: ${escapeXml(url)}\n`;
185
+ xml += `Run ID: ${escapeXml(runId)}\n`;
186
+ xml += `Created: ${escapeXml(createdAt)}\n\n`;
187
+
188
+ xml += `Summary:\n`;
189
+ xml += ` Total Tests: ${totalTests}\n`;
190
+ xml += ` Failures: ${totalFailures}\n`;
191
+ xml += ` Skipped: ${totalSkipped}\n`;
192
+
193
+ if (marketImpact.countsBySeverity) {
194
+ xml += `\nMarket Impact:\n`;
195
+ xml += ` CRITICAL: ${marketImpact.countsBySeverity.CRITICAL || 0}\n`;
196
+ xml += ` WARNING: ${marketImpact.countsBySeverity.WARNING || 0}\n`;
197
+ xml += ` INFO: ${marketImpact.countsBySeverity.INFO || 0}\n`;
198
+ }
199
+
200
+ if (snapshot.discovery) {
201
+ xml += `\nDiscovery Results:\n`;
202
+ xml += ` Pages Visited: ${snapshot.discovery.pagesVisitedCount || 0}\n`;
203
+ xml += ` Interactions Discovered: ${snapshot.discovery.interactionsDiscovered || 0}\n`;
204
+ xml += ` Interactions Executed: ${snapshot.discovery.interactionsExecuted || 0}\n`;
205
+ }
206
+
207
+ xml += ` </system-out>\n`;
208
+
209
+ xml += ` </testsuite>\n`;
210
+ xml += `</testsuites>\n`;
211
+
212
+ return xml;
213
+ }
214
+
215
+ /**
216
+ * Write JUnit XML to file
217
+ */
218
+ function writeJunitFile(snapshot, outputPath, baseUrl = '') {
219
+ const xml = generateJunitXml(snapshot, baseUrl);
220
+
221
+ const dir = path.dirname(outputPath);
222
+ if (!fs.existsSync(dir)) {
223
+ fs.mkdirSync(dir, { recursive: true });
224
+ }
225
+
226
+ fs.writeFileSync(outputPath, xml, 'utf8');
227
+ return outputPath;
228
+ }
229
+
230
+ /**
231
+ * Validate XML is well-formed (basic check)
232
+ */
233
+ function validateJunitXml(xmlContent) {
234
+ try {
235
+ // Basic XML validation: check opening and closing tags
236
+ if (!xmlContent.includes('<?xml')) {
237
+ return { valid: false, errors: ['Missing XML declaration'] };
238
+ }
239
+
240
+ if (!xmlContent.includes('<testsuites>') || !xmlContent.includes('</testsuites>')) {
241
+ return { valid: false, errors: ['Missing testsuites root element'] };
242
+ }
243
+
244
+ if (!xmlContent.includes('<testsuite') || !xmlContent.includes('</testsuite>')) {
245
+ return { valid: false, errors: ['Missing testsuite element'] };
246
+ }
247
+
248
+ // Check for unescaped characters
249
+ if (xmlContent.includes('&') && !xmlContent.includes('&amp;')) {
250
+ return { valid: false, errors: ['Unescaped & character found'] };
251
+ }
252
+
253
+ return { valid: true, errors: [] };
254
+ } catch (e) {
255
+ return { valid: false, errors: [e.message] };
256
+ }
257
+ }
258
+
259
+ module.exports = {
260
+ generateJunitXml,
261
+ writeJunitFile,
262
+ validateJunitXml,
263
+ escapeXml
264
+ };