@etalon/cli 1.0.3 → 1.0.5
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/README.md
CHANGED
|
@@ -12,22 +12,22 @@ npm install -g etalon
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
# Scan a website
|
|
15
|
-
|
|
15
|
+
etalon scan https://example.com
|
|
16
16
|
|
|
17
17
|
# JSON output
|
|
18
|
-
|
|
18
|
+
etalon scan https://example.com --format json
|
|
19
19
|
|
|
20
20
|
# SARIF for CI/CD (GitHub Code Scanning)
|
|
21
|
-
|
|
21
|
+
etalon scan https://example.com --format sarif
|
|
22
22
|
|
|
23
23
|
# Deep scan — scroll page, click consent dialogs
|
|
24
|
-
|
|
24
|
+
etalon scan https://example.com --deep
|
|
25
25
|
|
|
26
26
|
# Look up a single domain
|
|
27
|
-
|
|
27
|
+
etalon lookup google-analytics.com
|
|
28
28
|
|
|
29
29
|
# Registry stats
|
|
30
|
-
|
|
30
|
+
etalon info
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
## Options
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
|
@@ -20,7 +20,18 @@ import {
|
|
|
20
20
|
analyzeDataFlow,
|
|
21
21
|
toMermaid,
|
|
22
22
|
toTextSummary,
|
|
23
|
-
generatePolicy
|
|
23
|
+
generatePolicy,
|
|
24
|
+
AutoFixEngine,
|
|
25
|
+
applyContextScoring,
|
|
26
|
+
reportFalsePositive,
|
|
27
|
+
getFeedbackSummary,
|
|
28
|
+
isTelemetryEnabled,
|
|
29
|
+
enableTelemetry,
|
|
30
|
+
disableTelemetry,
|
|
31
|
+
recordAuditEvent,
|
|
32
|
+
analyzePatterns,
|
|
33
|
+
getLearningStats,
|
|
34
|
+
detectProjectContext
|
|
24
35
|
} from "@etalon/core";
|
|
25
36
|
|
|
26
37
|
// src/formatters/text.ts
|
|
@@ -105,13 +116,13 @@ function formatVendorEntry(dv, compact = false) {
|
|
|
105
116
|
const lines = [];
|
|
106
117
|
const riskColor = vendor.risk_score >= 6 ? chalk.red : vendor.risk_score >= 3 ? chalk.yellow : chalk.green;
|
|
107
118
|
lines.push(
|
|
108
|
-
`${riskColor(vendor.domains[0].padEnd(35))} ${chalk.bold(vendor.name)}`
|
|
119
|
+
`${riskColor((vendor.domains?.[0] ?? "unknown").padEnd(35))} ${chalk.bold(vendor.name)}`
|
|
109
120
|
);
|
|
110
121
|
lines.push(`\u251C\u2500 ${chalk.dim("Category:")} ${vendor.category}`);
|
|
111
122
|
if (!compact) {
|
|
112
123
|
const gdprStatus = vendor.gdpr_compliant ? chalk.green("Compliant") + (vendor.dpa_url ? chalk.dim(" (with DPA)") : "") : chalk.red("Non-compliant");
|
|
113
124
|
lines.push(`\u251C\u2500 ${chalk.dim("GDPR:")} ${gdprStatus}`);
|
|
114
|
-
if (vendor.data_collected.length > 0) {
|
|
125
|
+
if (vendor.data_collected && vendor.data_collected.length > 0) {
|
|
115
126
|
lines.push(`\u251C\u2500 ${chalk.dim("Data:")} ${vendor.data_collected.join(", ")}`);
|
|
116
127
|
}
|
|
117
128
|
if (vendor.dpa_url) {
|
|
@@ -841,14 +852,26 @@ program.command("audit").description("Scan a codebase for GDPR compliance (track
|
|
|
841
852
|
severity: options.severity,
|
|
842
853
|
includeBlame
|
|
843
854
|
});
|
|
855
|
+
const scoring = applyContextScoring(report.findings, dir);
|
|
856
|
+
report.findings = scoring.adjustedFindings;
|
|
857
|
+
if (scoring.adjustments.length > 0 && format === "text") {
|
|
858
|
+
spinner?.stop();
|
|
859
|
+
console.log(chalk4.bold(`
|
|
860
|
+
\u{1F3AF} Context: ${scoring.projectContext.industry} / ${scoring.projectContext.region}`));
|
|
861
|
+
console.log(chalk4.dim(` ${scoring.adjustments.length} finding(s) had severity adjusted based on context`));
|
|
862
|
+
for (const adj of scoring.adjustments.slice(0, 5)) {
|
|
863
|
+
console.log(` ${chalk4.dim(adj.finding_rule)}: ${adj.original_severity} \u2192 ${chalk4.yellow(adj.adjusted_severity)} (${adj.reason})`);
|
|
864
|
+
}
|
|
865
|
+
if (scoring.adjustments.length > 5) {
|
|
866
|
+
console.log(chalk4.dim(` ... and ${scoring.adjustments.length - 5} more`));
|
|
867
|
+
}
|
|
868
|
+
}
|
|
844
869
|
spinner?.stop();
|
|
845
870
|
if (autoFix) {
|
|
846
871
|
const patches = generatePatches(report.findings, dir);
|
|
847
|
-
if (patches.length
|
|
848
|
-
console.log(chalk4.yellow("\nNo auto-fixable issues found."));
|
|
849
|
-
} else {
|
|
872
|
+
if (patches.length > 0) {
|
|
850
873
|
console.log(chalk4.bold(`
|
|
851
|
-
\u{1F527} ${patches.length}
|
|
874
|
+
\u{1F527} ${patches.length} config fix(es):`));
|
|
852
875
|
for (const p of patches) {
|
|
853
876
|
console.log(` ${chalk4.dim(p.file)}:${p.line} \u2014 ${p.description}`);
|
|
854
877
|
console.log(` ${chalk4.red("- " + p.oldContent.trim())}`);
|
|
@@ -856,9 +879,58 @@ program.command("audit").description("Scan a codebase for GDPR compliance (track
|
|
|
856
879
|
}
|
|
857
880
|
const applied = applyPatches(patches, dir);
|
|
858
881
|
console.log(chalk4.green(`
|
|
859
|
-
\u2713 Applied ${applied} fix(es).`));
|
|
882
|
+
\u2713 Applied ${applied} config fix(es).`));
|
|
883
|
+
}
|
|
884
|
+
const engine = new AutoFixEngine();
|
|
885
|
+
const { readdirSync } = await import("fs");
|
|
886
|
+
const { join: joinPath } = await import("path");
|
|
887
|
+
const codeFiles = [];
|
|
888
|
+
const collectCodeFiles = (d) => {
|
|
889
|
+
try {
|
|
890
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
891
|
+
const full = joinPath(d, entry.name);
|
|
892
|
+
if (entry.isDirectory()) {
|
|
893
|
+
if (!["node_modules", ".git", "dist", "build", ".next", "__pycache__", "target"].includes(entry.name)) {
|
|
894
|
+
collectCodeFiles(full);
|
|
895
|
+
}
|
|
896
|
+
} else if (/\.(tsx?|jsx?|vue|svelte|html)$/.test(entry.name)) {
|
|
897
|
+
codeFiles.push(full);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
} catch {
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
collectCodeFiles(dir);
|
|
904
|
+
const suggestions = engine.scanFiles(codeFiles);
|
|
905
|
+
if (suggestions.length > 0) {
|
|
906
|
+
console.log(chalk4.bold(`
|
|
907
|
+
\u{1F6E1}\uFE0F ${suggestions.length} tracker consent fix(es) available:`));
|
|
908
|
+
for (const s of suggestions) {
|
|
909
|
+
console.log(` ${chalk4.cyan(s.tracker_name)} in ${chalk4.dim(s.location.file)}:${s.location.line}`);
|
|
910
|
+
console.log(` ${chalk4.dim(s.description)}`);
|
|
911
|
+
}
|
|
912
|
+
const hookPath = engine.generateConsentHook(dir);
|
|
913
|
+
console.log(chalk4.green(`
|
|
914
|
+
\u2713 Generated consent hook: ${chalk4.cyan(hookPath)}`));
|
|
915
|
+
const result = engine.applyAllFixes(suggestions);
|
|
916
|
+
console.log(chalk4.green(`\u2713 Applied ${result.applied} tracker consent fix(es).`));
|
|
917
|
+
if (result.failed > 0) {
|
|
918
|
+
console.log(chalk4.yellow(`\u26A0 ${result.failed} fix(es) could not be applied.`));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (patches.length === 0 && suggestions.length === 0) {
|
|
922
|
+
console.log(chalk4.yellow("\nNo auto-fixable issues found."));
|
|
860
923
|
}
|
|
861
924
|
}
|
|
925
|
+
recordAuditEvent({
|
|
926
|
+
total_findings: report.findings.length,
|
|
927
|
+
critical: report.summary.critical,
|
|
928
|
+
high: report.summary.high,
|
|
929
|
+
medium: report.summary.medium,
|
|
930
|
+
framework: scoring.projectContext.industry,
|
|
931
|
+
industry: scoring.projectContext.industry,
|
|
932
|
+
region: scoring.projectContext.region
|
|
933
|
+
});
|
|
862
934
|
switch (format) {
|
|
863
935
|
case "json":
|
|
864
936
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -977,7 +1049,7 @@ program.command("policy-check").description("Cross-reference privacy policy text
|
|
|
977
1049
|
const format = options.format ?? "text";
|
|
978
1050
|
const spinner = format === "text" ? ora(`Analyzing privacy policy for ${normalizedUrl}...`).start() : null;
|
|
979
1051
|
try {
|
|
980
|
-
const { checkPolicy } = await import("./policy-checker-
|
|
1052
|
+
const { checkPolicy } = await import("./policy-checker-ONMTI7X2.js");
|
|
981
1053
|
const result = await checkPolicy(normalizedUrl, {
|
|
982
1054
|
timeout: parseInt(options.timeout ?? "30000", 10),
|
|
983
1055
|
policyUrl: options.policyUrl
|
|
@@ -1149,23 +1221,22 @@ Error: ${error.message}`);
|
|
|
1149
1221
|
program.command("data-flow").description("Map PII data flows through your codebase").argument("[dir]", "Directory to analyze", "./").option("-f, --format <format>", "Output format: text, mermaid, json", "text").action(async (dir, options) => {
|
|
1150
1222
|
const spinner = ora("Analyzing data flows...").start();
|
|
1151
1223
|
try {
|
|
1152
|
-
|
|
1224
|
+
const { readdirSync } = await import("fs");
|
|
1225
|
+
const { join: join3, relative } = await import("path");
|
|
1226
|
+
const files = [];
|
|
1227
|
+
const walk = (d) => {
|
|
1153
1228
|
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
1154
1229
|
const full = join3(d, entry.name);
|
|
1155
1230
|
if (entry.isDirectory()) {
|
|
1156
1231
|
if (!["node_modules", ".git", "dist", "build", ".next", "__pycache__"].includes(entry.name)) {
|
|
1157
|
-
|
|
1232
|
+
walk(full);
|
|
1158
1233
|
}
|
|
1159
1234
|
} else {
|
|
1160
1235
|
files.push(relative(dir, full));
|
|
1161
1236
|
}
|
|
1162
1237
|
}
|
|
1163
1238
|
};
|
|
1164
|
-
|
|
1165
|
-
const { readdirSync, statSync } = await import("fs");
|
|
1166
|
-
const { join: join3, relative } = await import("path");
|
|
1167
|
-
const files = [];
|
|
1168
|
-
walk2(dir);
|
|
1239
|
+
walk(dir);
|
|
1169
1240
|
const flow = analyzeDataFlow(files, dir);
|
|
1170
1241
|
spinner.stop();
|
|
1171
1242
|
switch (options.format) {
|
|
@@ -1187,4 +1258,84 @@ Error: ${error.message}`);
|
|
|
1187
1258
|
process.exit(2);
|
|
1188
1259
|
}
|
|
1189
1260
|
});
|
|
1261
|
+
program.command("report-fp").description("Report a false positive finding").requiredOption("--domain <domain>", "Domain that was incorrectly flagged").requiredOption("--rule <rule>", "Rule that triggered the false positive").option("--file <file>", "File where the false positive was found").option("--reason <reason>", "Why this is a false positive", "Not a tracker").action((options) => {
|
|
1262
|
+
const report = reportFalsePositive({
|
|
1263
|
+
domain: options.domain,
|
|
1264
|
+
rule: options.rule,
|
|
1265
|
+
file: options.file,
|
|
1266
|
+
reason: options.reason ?? "Not a tracker",
|
|
1267
|
+
suggested_action: "whitelist_domain"
|
|
1268
|
+
});
|
|
1269
|
+
console.log(chalk4.green(`\u2713 False positive reported: ${chalk4.bold(report.id)}`));
|
|
1270
|
+
console.log(chalk4.dim(` Domain: ${report.domain}`));
|
|
1271
|
+
console.log(chalk4.dim(` Rule: ${report.rule}`));
|
|
1272
|
+
console.log(chalk4.dim(` Reason: ${report.reason}`));
|
|
1273
|
+
console.log("");
|
|
1274
|
+
console.log(chalk4.dim("Reports help ETALON learn and reduce false positives over time."));
|
|
1275
|
+
});
|
|
1276
|
+
program.command("telemetry").description("Manage anonymous usage telemetry").argument("<action>", "enable, disable, or status").action((action) => {
|
|
1277
|
+
switch (action) {
|
|
1278
|
+
case "enable":
|
|
1279
|
+
enableTelemetry();
|
|
1280
|
+
console.log(chalk4.green("\u2713 Telemetry enabled."));
|
|
1281
|
+
console.log(chalk4.dim(" Anonymous usage data helps improve ETALON for everyone."));
|
|
1282
|
+
console.log(chalk4.dim(" No PII is ever collected. Set DO_NOT_TRACK=1 to override."));
|
|
1283
|
+
break;
|
|
1284
|
+
case "disable":
|
|
1285
|
+
disableTelemetry();
|
|
1286
|
+
console.log(chalk4.yellow("\u2713 Telemetry disabled."));
|
|
1287
|
+
break;
|
|
1288
|
+
case "status":
|
|
1289
|
+
console.log(`Telemetry: ${isTelemetryEnabled() ? chalk4.green("enabled") : chalk4.yellow("disabled")}`);
|
|
1290
|
+
break;
|
|
1291
|
+
default:
|
|
1292
|
+
console.error(chalk4.red(`Unknown action: ${action}. Use enable, disable, or status.`));
|
|
1293
|
+
process.exit(1);
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
program.command("intelligence").description("Show intelligence engine status and learned patterns").argument("[dir]", "Project directory for context detection", "./").action((dir) => {
|
|
1297
|
+
console.log(chalk4.bold("ETALON Intelligence Engine"));
|
|
1298
|
+
console.log(chalk4.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
1299
|
+
console.log("");
|
|
1300
|
+
const ctx = detectProjectContext(dir);
|
|
1301
|
+
console.log(chalk4.bold("\u{1F3AF} Project Context"));
|
|
1302
|
+
console.log(` Industry: ${chalk4.cyan(ctx.industry)}`);
|
|
1303
|
+
console.log(` Region: ${chalk4.cyan(ctx.region)}`);
|
|
1304
|
+
console.log(` Data Sensitivity: ${chalk4.cyan(ctx.data_sensitivity)}`);
|
|
1305
|
+
if (ctx.detected_signals.length > 0) {
|
|
1306
|
+
console.log(chalk4.dim(" Signals:"));
|
|
1307
|
+
for (const s of ctx.detected_signals) {
|
|
1308
|
+
console.log(chalk4.dim(` \u2022 ${s}`));
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
console.log("");
|
|
1312
|
+
const stats = getLearningStats();
|
|
1313
|
+
console.log(chalk4.bold("\u{1F9E0} Learning Engine"));
|
|
1314
|
+
console.log(` Patterns learned: ${chalk4.cyan(String(stats.patterns_learned))}`);
|
|
1315
|
+
console.log(` Feedback processed: ${chalk4.cyan(String(stats.feedback_processed))}`);
|
|
1316
|
+
console.log(` Impact: ${chalk4.cyan(stats.accuracy_improvement)}`);
|
|
1317
|
+
console.log("");
|
|
1318
|
+
const feedback = getFeedbackSummary();
|
|
1319
|
+
if (feedback.total_reports > 0) {
|
|
1320
|
+
console.log(chalk4.bold("\u{1F4CA} False Positive Reports"));
|
|
1321
|
+
console.log(` Total reports: ${feedback.total_reports}`);
|
|
1322
|
+
if (feedback.suggested_whitelists.length > 0) {
|
|
1323
|
+
console.log(chalk4.dim(" Suggested whitelists (3+ reports):"));
|
|
1324
|
+
for (const d of feedback.suggested_whitelists) {
|
|
1325
|
+
console.log(chalk4.dim(` \u2022 ${d}`));
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
console.log("");
|
|
1329
|
+
}
|
|
1330
|
+
const learned = analyzePatterns();
|
|
1331
|
+
if (learned.length > 0) {
|
|
1332
|
+
console.log(chalk4.bold("\u{1F4DD} Learned Patterns"));
|
|
1333
|
+
for (const p of learned) {
|
|
1334
|
+
console.log(` ${chalk4.cyan(p.domain)} \u2014 ${p.suggested_action} (confidence: ${(p.confidence * 100).toFixed(0)}%)`);
|
|
1335
|
+
}
|
|
1336
|
+
console.log("");
|
|
1337
|
+
}
|
|
1338
|
+
console.log(chalk4.dim("Telemetry: ") + (isTelemetryEnabled() ? chalk4.green("enabled") : chalk4.yellow("disabled")));
|
|
1339
|
+
console.log("");
|
|
1340
|
+
});
|
|
1190
1341
|
program.parse();
|
|
@@ -46,7 +46,7 @@ async function findPolicyPage(page, siteUrl) {
|
|
|
46
46
|
const testUrl = `${baseUrl.origin}${path}`;
|
|
47
47
|
const response = await page.goto(testUrl, { waitUntil: "domcontentloaded", timeout: 1e4 });
|
|
48
48
|
if (response && response.status() >= 200 && response.status() < 400) {
|
|
49
|
-
const bodyText = await page.evaluate(() =>
|
|
49
|
+
const bodyText = await page.evaluate(() => document.body?.innerText?.trim() ?? "");
|
|
50
50
|
if (bodyText.length > 200) {
|
|
51
51
|
return testUrl;
|
|
52
52
|
}
|
|
@@ -85,10 +85,9 @@ async function extractPolicyText(page, policyUrl) {
|
|
|
85
85
|
await page.goto(policyUrl, { waitUntil: "domcontentloaded", timeout: 2e4 });
|
|
86
86
|
await page.waitForTimeout(1e3);
|
|
87
87
|
const text = await page.evaluate(() => {
|
|
88
|
-
const
|
|
89
|
-
const elementsToRemove = doc.querySelectorAll("script, style, nav, header, footer, iframe");
|
|
88
|
+
const elementsToRemove = document.querySelectorAll("script, style, nav, header, footer, iframe");
|
|
90
89
|
elementsToRemove.forEach((el) => el.remove());
|
|
91
|
-
return
|
|
90
|
+
return document.body?.innerText ?? "";
|
|
92
91
|
});
|
|
93
92
|
return text.replace(/\s+/g, " ").trim();
|
|
94
93
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etalon/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "ETALON — Privacy audit tool for websites. Scan any site for trackers and GDPR compliance.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"ora": "^8.0.0",
|
|
24
24
|
"playwright": "^1.42.0",
|
|
25
25
|
"yaml": "^2.3.0",
|
|
26
|
-
"@etalon/core": "^1.0.
|
|
26
|
+
"@etalon/core": "^1.0.2"
|
|
27
27
|
},
|
|
28
28
|
"keywords": [
|
|
29
29
|
"privacy",
|
|
@@ -40,4 +40,4 @@
|
|
|
40
40
|
"type": "git",
|
|
41
41
|
"url": "https://github.com/NMA-vc/etalon"
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|