@etalon/cli 1.0.4 → 1.1.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/dist/index.js +439 -126
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
|
|
6
6
|
// src/index.ts
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import { writeFileSync as
|
|
8
|
+
import ora2 from "ora";
|
|
9
|
+
import chalk6 from "chalk";
|
|
10
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
11
11
|
import {
|
|
12
12
|
normalizeUrl,
|
|
13
13
|
VendorRegistry,
|
|
14
|
-
auditProject,
|
|
14
|
+
auditProject as auditProject2,
|
|
15
15
|
formatAuditSarif,
|
|
16
16
|
generateBadgeSvg,
|
|
17
17
|
calculateScore,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
toTextSummary,
|
|
23
23
|
generatePolicy,
|
|
24
24
|
AutoFixEngine,
|
|
25
|
-
applyContextScoring,
|
|
25
|
+
applyContextScoring as applyContextScoring2,
|
|
26
26
|
reportFalsePositive,
|
|
27
27
|
getFeedbackSummary,
|
|
28
28
|
isTelemetryEnabled,
|
|
@@ -116,13 +116,13 @@ function formatVendorEntry(dv, compact = false) {
|
|
|
116
116
|
const lines = [];
|
|
117
117
|
const riskColor = vendor.risk_score >= 6 ? chalk.red : vendor.risk_score >= 3 ? chalk.yellow : chalk.green;
|
|
118
118
|
lines.push(
|
|
119
|
-
`${riskColor(vendor.domains[0].padEnd(35))} ${chalk.bold(vendor.name)}`
|
|
119
|
+
`${riskColor((vendor.domains?.[0] ?? "unknown").padEnd(35))} ${chalk.bold(vendor.name)}`
|
|
120
120
|
);
|
|
121
121
|
lines.push(`\u251C\u2500 ${chalk.dim("Category:")} ${vendor.category}`);
|
|
122
122
|
if (!compact) {
|
|
123
123
|
const gdprStatus = vendor.gdpr_compliant ? chalk.green("Compliant") + (vendor.dpa_url ? chalk.dim(" (with DPA)") : "") : chalk.red("Non-compliant");
|
|
124
124
|
lines.push(`\u251C\u2500 ${chalk.dim("GDPR:")} ${gdprStatus}`);
|
|
125
|
-
if (vendor.data_collected.length > 0) {
|
|
125
|
+
if (vendor.data_collected && vendor.data_collected.length > 0) {
|
|
126
126
|
lines.push(`\u251C\u2500 ${chalk.dim("Data:")} ${vendor.data_collected.join(", ")}`);
|
|
127
127
|
}
|
|
128
128
|
if (vendor.dpa_url) {
|
|
@@ -767,12 +767,281 @@ async function runInit(dir, options = {}) {
|
|
|
767
767
|
console.log("");
|
|
768
768
|
}
|
|
769
769
|
|
|
770
|
+
// src/commands/cloud.ts
|
|
771
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, unlinkSync } from "fs";
|
|
772
|
+
import { homedir } from "os";
|
|
773
|
+
import { join as join3 } from "path";
|
|
774
|
+
import chalk4 from "chalk";
|
|
775
|
+
var CONFIG_DIR = join3(homedir(), ".etalon");
|
|
776
|
+
var CONFIG_FILE = join3(CONFIG_DIR, "config.json");
|
|
777
|
+
var DEFAULT_API_URL = "https://etalon.nma.vc/api";
|
|
778
|
+
function saveCloudConfig(config) {
|
|
779
|
+
if (!existsSync3(CONFIG_DIR)) {
|
|
780
|
+
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
781
|
+
}
|
|
782
|
+
writeFileSync2(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
783
|
+
}
|
|
784
|
+
function loadCloudConfig() {
|
|
785
|
+
if (!existsSync3(CONFIG_FILE)) {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
try {
|
|
789
|
+
return JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
|
|
790
|
+
} catch {
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function removeCloudConfig() {
|
|
795
|
+
if (existsSync3(CONFIG_FILE)) {
|
|
796
|
+
unlinkSync(CONFIG_FILE);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async function verifyApiKey(apiKey, apiUrl = DEFAULT_API_URL) {
|
|
800
|
+
try {
|
|
801
|
+
const response = await fetch(`${apiUrl}/auth/verify`, {
|
|
802
|
+
method: "POST",
|
|
803
|
+
headers: {
|
|
804
|
+
"Content-Type": "application/json",
|
|
805
|
+
Authorization: `Bearer ${apiKey}`
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
return response.ok;
|
|
809
|
+
} catch {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function uploadScan(config, siteId, url, results, cliVersion) {
|
|
814
|
+
try {
|
|
815
|
+
const response = await fetch(`${config.apiUrl}/ingest`, {
|
|
816
|
+
method: "POST",
|
|
817
|
+
headers: {
|
|
818
|
+
"Content-Type": "application/json",
|
|
819
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
820
|
+
},
|
|
821
|
+
body: JSON.stringify({
|
|
822
|
+
siteId,
|
|
823
|
+
url,
|
|
824
|
+
results,
|
|
825
|
+
cliVersion
|
|
826
|
+
})
|
|
827
|
+
});
|
|
828
|
+
const data = await response.json();
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
return { success: false, error: data.error || response.statusText };
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
success: true,
|
|
834
|
+
scanId: data.scanId,
|
|
835
|
+
score: data.score,
|
|
836
|
+
grade: data.grade,
|
|
837
|
+
dashboardUrl: data.dashboardUrl
|
|
838
|
+
};
|
|
839
|
+
} catch (err) {
|
|
840
|
+
return { success: false, error: err instanceof Error ? err.message : "Network error" };
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async function runLogin() {
|
|
844
|
+
console.log("");
|
|
845
|
+
console.log(chalk4.bold("\u{1F510} Login to ETALON Cloud"));
|
|
846
|
+
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\u2550\u2550\u2550\u2550"));
|
|
847
|
+
console.log("");
|
|
848
|
+
console.log(chalk4.gray("Get your API key from: https://etalon.nma.vc/dashboard/api-keys"));
|
|
849
|
+
console.log("");
|
|
850
|
+
const apiKey = await readLine("Enter your API key: ");
|
|
851
|
+
if (!apiKey || apiKey.length < 10) {
|
|
852
|
+
console.log(chalk4.red("\u274C Invalid API key"));
|
|
853
|
+
process.exit(1);
|
|
854
|
+
}
|
|
855
|
+
const spinner = (await import("ora")).default;
|
|
856
|
+
const s = spinner("Verifying API key...").start();
|
|
857
|
+
const isValid = await verifyApiKey(apiKey);
|
|
858
|
+
if (!isValid) {
|
|
859
|
+
s.fail("API key verification failed");
|
|
860
|
+
console.log(chalk4.gray("\nMake sure you copied the full key from the dashboard."));
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
saveCloudConfig({ apiKey, apiUrl: DEFAULT_API_URL });
|
|
864
|
+
s.succeed("Successfully logged in!");
|
|
865
|
+
console.log("");
|
|
866
|
+
console.log(chalk4.gray("You can now use:"));
|
|
867
|
+
console.log(` ${chalk4.cyan("etalon scan <url> --upload --site <id>")} Upload scan results`);
|
|
868
|
+
console.log(` ${chalk4.cyan("etalon auth status")} Check login status`);
|
|
869
|
+
console.log(` ${chalk4.cyan("etalon auth logout")} Remove stored key`);
|
|
870
|
+
console.log("");
|
|
871
|
+
}
|
|
872
|
+
function runLogout() {
|
|
873
|
+
removeCloudConfig();
|
|
874
|
+
console.log(chalk4.green("\u2713 Logged out successfully"));
|
|
875
|
+
}
|
|
876
|
+
async function runStatus() {
|
|
877
|
+
const config = loadCloudConfig();
|
|
878
|
+
if (!config) {
|
|
879
|
+
console.log(chalk4.yellow("\u274C Not logged in"));
|
|
880
|
+
console.log(chalk4.gray("\nRun: etalon auth login"));
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const spinner = (await import("ora")).default;
|
|
884
|
+
const s = spinner("Checking API key...").start();
|
|
885
|
+
const isValid = await verifyApiKey(config.apiKey, config.apiUrl);
|
|
886
|
+
if (isValid) {
|
|
887
|
+
s.succeed("Logged in");
|
|
888
|
+
console.log(chalk4.gray(` API: ${config.apiUrl}`));
|
|
889
|
+
console.log(chalk4.gray(` Key: ${config.apiKey.slice(0, 12)}...`));
|
|
890
|
+
} else {
|
|
891
|
+
s.fail("API key expired or invalid");
|
|
892
|
+
console.log(chalk4.gray("\nRun: etalon auth login"));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function readLine(prompt) {
|
|
896
|
+
return new Promise((resolve) => {
|
|
897
|
+
process.stdout.write(prompt);
|
|
898
|
+
let data = "";
|
|
899
|
+
process.stdin.setEncoding("utf-8");
|
|
900
|
+
process.stdin.resume();
|
|
901
|
+
process.stdin.on("data", (chunk) => {
|
|
902
|
+
data += chunk;
|
|
903
|
+
if (data.includes("\n")) {
|
|
904
|
+
process.stdin.pause();
|
|
905
|
+
resolve(data.trim());
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
async function listSites(config) {
|
|
911
|
+
try {
|
|
912
|
+
const response = await fetch(`${config.apiUrl}/sites`, {
|
|
913
|
+
headers: {
|
|
914
|
+
Authorization: `Bearer ${config.apiKey}`
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
const data = await response.json();
|
|
918
|
+
if (!response.ok) {
|
|
919
|
+
return { success: false, error: data.error || response.statusText };
|
|
920
|
+
}
|
|
921
|
+
return { success: true, sites: data.sites };
|
|
922
|
+
} catch (err) {
|
|
923
|
+
return { success: false, error: err instanceof Error ? err.message : "Network error" };
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
async function runListSites() {
|
|
927
|
+
const config = loadCloudConfig();
|
|
928
|
+
if (!config) {
|
|
929
|
+
console.log(chalk4.yellow("\u274C Not logged in"));
|
|
930
|
+
console.log(chalk4.gray("\nRun: etalon auth login"));
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const spinner = (await import("ora")).default;
|
|
934
|
+
const s = spinner("Fetching sites...").start();
|
|
935
|
+
const result = await listSites(config);
|
|
936
|
+
if (!result.success || !result.sites) {
|
|
937
|
+
s.fail(`Failed: ${result.error}`);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
s.stop();
|
|
941
|
+
if (result.sites.length === 0) {
|
|
942
|
+
console.log(chalk4.yellow("No sites found."));
|
|
943
|
+
console.log(chalk4.gray("Add a site at: https://etalon.nma.vc/dashboard/sites"));
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
console.log("");
|
|
947
|
+
console.log(chalk4.bold("\u{1F4CB} Your Sites"));
|
|
948
|
+
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\u2550\u2550\u2550\u2550"));
|
|
949
|
+
console.log("");
|
|
950
|
+
for (const site of result.sites) {
|
|
951
|
+
const name = site.name || new URL(site.url).hostname;
|
|
952
|
+
const lastScan = site.last_scanned_at ? chalk4.gray(`Last scan: ${new Date(site.last_scanned_at).toLocaleDateString()}`) : chalk4.gray("Never scanned");
|
|
953
|
+
console.log(` ${chalk4.cyan(site.id)} ${chalk4.bold(name)}`);
|
|
954
|
+
console.log(` ${chalk4.dim(site.url)} ${lastScan}`);
|
|
955
|
+
console.log("");
|
|
956
|
+
}
|
|
957
|
+
console.log(chalk4.dim("Use the ID with: etalon scan <url> --upload --site <id>"));
|
|
958
|
+
console.log("");
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/commands/push.ts
|
|
962
|
+
import chalk5 from "chalk";
|
|
963
|
+
import ora from "ora";
|
|
964
|
+
import { auditProject, applyContextScoring } from "@etalon/core";
|
|
965
|
+
var VERSION = "1.1.0";
|
|
966
|
+
async function runPush(url, dir, options) {
|
|
967
|
+
const siteId = options.site;
|
|
968
|
+
if (!siteId) {
|
|
969
|
+
console.log("");
|
|
970
|
+
console.log(chalk5.red("\u274C --site <id> is required"));
|
|
971
|
+
console.log(chalk5.gray(" Get your site ID from: https://etalon.nma.vc/dashboard/sites"));
|
|
972
|
+
console.log(chalk5.gray(" Or run: etalon sites"));
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
const config = loadCloudConfig();
|
|
976
|
+
if (!config) {
|
|
977
|
+
console.log("");
|
|
978
|
+
console.log(chalk5.red("\u274C Not logged in. Run: etalon auth login"));
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
console.log("");
|
|
982
|
+
console.log(chalk5.bold("\u{1F680} ETALON Push"));
|
|
983
|
+
console.log(chalk5.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\u2550\u2550\u2550\u2550"));
|
|
984
|
+
console.log("");
|
|
985
|
+
const scanSpinner = ora(`Scanning ${url}...`).start();
|
|
986
|
+
let scanReport;
|
|
987
|
+
try {
|
|
988
|
+
const normalizedUrl2 = url.startsWith("http") ? url : `https://${url}`;
|
|
989
|
+
const scanOptions = {
|
|
990
|
+
deep: options.deep ?? false,
|
|
991
|
+
timeout: parseInt(options.timeout ?? "30000", 10),
|
|
992
|
+
waitForNetworkIdle: false
|
|
993
|
+
};
|
|
994
|
+
scanReport = await scanSite(normalizedUrl2, scanOptions);
|
|
995
|
+
scanSpinner.succeed(`Scan complete \u2014 ${scanReport.summary.total} vendor(s) detected`);
|
|
996
|
+
} catch (err) {
|
|
997
|
+
scanSpinner.fail("Scan failed");
|
|
998
|
+
console.error(chalk5.red(` ${err instanceof Error ? err.message : String(err)}`));
|
|
999
|
+
process.exit(2);
|
|
1000
|
+
}
|
|
1001
|
+
let auditReport;
|
|
1002
|
+
const auditSpinner = ora(`Auditing ${dir}...`).start();
|
|
1003
|
+
try {
|
|
1004
|
+
auditReport = await auditProject(dir, {});
|
|
1005
|
+
const scoring = applyContextScoring(auditReport.findings, dir);
|
|
1006
|
+
auditReport.findings = scoring.adjustedFindings;
|
|
1007
|
+
auditSpinner.succeed(`Audit complete \u2014 ${auditReport.findings.length} finding(s)`);
|
|
1008
|
+
} catch {
|
|
1009
|
+
auditSpinner.warn("Audit skipped (no project found or not a codebase)");
|
|
1010
|
+
auditReport = null;
|
|
1011
|
+
}
|
|
1012
|
+
const uploadSpinner = ora("Uploading to ETALON Cloud...").start();
|
|
1013
|
+
const normalizedUrl = url.startsWith("http") ? url : `https://${url}`;
|
|
1014
|
+
const result = await uploadScan(config, siteId, normalizedUrl, scanReport, VERSION);
|
|
1015
|
+
if (result.success) {
|
|
1016
|
+
uploadSpinner.succeed(`Uploaded! Grade: ${chalk5.bold(result.grade)} (${result.score}/100)`);
|
|
1017
|
+
} else {
|
|
1018
|
+
uploadSpinner.fail(`Upload failed: ${result.error}`);
|
|
1019
|
+
}
|
|
1020
|
+
console.log("");
|
|
1021
|
+
console.log(chalk5.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1022
|
+
if (result.success) {
|
|
1023
|
+
console.log(chalk5.green("\u2713 Push complete"));
|
|
1024
|
+
console.log(chalk5.gray(` Dashboard: ${result.dashboardUrl}`));
|
|
1025
|
+
}
|
|
1026
|
+
if (scanReport.summary.highRisk > 0) {
|
|
1027
|
+
console.log(chalk5.yellow(` \u26A0 ${scanReport.summary.highRisk} high-risk tracker(s) detected`));
|
|
1028
|
+
}
|
|
1029
|
+
if (auditReport && auditReport.findings.length > 0) {
|
|
1030
|
+
const critical = auditReport.findings.filter((f) => f.severity === "critical").length;
|
|
1031
|
+
const high = auditReport.findings.filter((f) => f.severity === "high").length;
|
|
1032
|
+
if (critical + high > 0) {
|
|
1033
|
+
console.log(chalk5.yellow(` \u26A0 ${critical} critical, ${high} high severity code findings`));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
console.log("");
|
|
1037
|
+
}
|
|
1038
|
+
|
|
770
1039
|
// src/index.ts
|
|
771
|
-
var
|
|
1040
|
+
var VERSION2 = "1.1.0";
|
|
772
1041
|
function showBanner() {
|
|
773
|
-
const blue =
|
|
774
|
-
const dim =
|
|
775
|
-
const cyan =
|
|
1042
|
+
const blue = chalk6.hex("#3B82F6");
|
|
1043
|
+
const dim = chalk6.hex("#64748B");
|
|
1044
|
+
const cyan = chalk6.hex("#06B6D4");
|
|
776
1045
|
console.log("");
|
|
777
1046
|
console.log(blue.bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557"));
|
|
778
1047
|
console.log(blue.bold(" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551"));
|
|
@@ -781,15 +1050,15 @@ function showBanner() {
|
|
|
781
1050
|
console.log(blue.bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551"));
|
|
782
1051
|
console.log(blue.bold(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D"));
|
|
783
1052
|
console.log("");
|
|
784
|
-
console.log(dim(` v${
|
|
1053
|
+
console.log(dim(` v${VERSION2} `) + chalk6.white("Privacy audit tool for AI coding agents"));
|
|
785
1054
|
console.log(dim(" Open-source GDPR compliance scanner ") + cyan("etalon.nma.vc"));
|
|
786
1055
|
console.log("");
|
|
787
1056
|
}
|
|
788
1057
|
var program = new Command();
|
|
789
|
-
program.name("etalon").description("ETALON \u2014 Open-source privacy auditor. Scan websites for trackers and GDPR compliance.").version(
|
|
1058
|
+
program.name("etalon").description("ETALON \u2014 Open-source privacy auditor. Scan websites for trackers and GDPR compliance.").version(VERSION2).hook("preAction", () => {
|
|
790
1059
|
showBanner();
|
|
791
1060
|
});
|
|
792
|
-
program.command("scan").description("Scan a website for third-party trackers").argument("<url>", "URL to scan").option("-f, --format <format>", "Output format: text, json, sarif", "text").option("-d, --deep", "Deep scan: scroll page, interact with consent dialogs", false).option("-t, --timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--idle", "Wait for network idle (slower but more thorough)").option("--config <path>", "Path to etalon.yaml config file").action(async (url, options) => {
|
|
1061
|
+
program.command("scan").description("Scan a website for third-party trackers").argument("<url>", "URL to scan").option("-f, --format <format>", "Output format: text, json, sarif", "text").option("-d, --deep", "Deep scan: scroll page, interact with consent dialogs", false).option("-t, --timeout <ms>", "Navigation timeout in milliseconds", "30000").option("--idle", "Wait for network idle (slower but more thorough)").option("--config <path>", "Path to etalon.yaml config file").option("--upload", "Upload results to ETALON Cloud").option("--site <id>", "Site ID for cloud upload (from dashboard)").action(async (url, options) => {
|
|
793
1062
|
const normalizedUrl = normalizeUrl(url);
|
|
794
1063
|
const format = options.format ?? "text";
|
|
795
1064
|
const config = loadConfig(options.config);
|
|
@@ -807,7 +1076,7 @@ program.command("scan").description("Scan a website for third-party trackers").a
|
|
|
807
1076
|
}
|
|
808
1077
|
}
|
|
809
1078
|
const showSpinner = format === "text";
|
|
810
|
-
const spinner = showSpinner ?
|
|
1079
|
+
const spinner = showSpinner ? ora2(`Scanning ${normalizedUrl}...`).start() : null;
|
|
811
1080
|
try {
|
|
812
1081
|
const report = await scanSite(normalizedUrl, scanOptions);
|
|
813
1082
|
spinner?.stop();
|
|
@@ -823,6 +1092,30 @@ program.command("scan").description("Scan a website for third-party trackers").a
|
|
|
823
1092
|
console.log(formatText(report));
|
|
824
1093
|
break;
|
|
825
1094
|
}
|
|
1095
|
+
if (options.upload) {
|
|
1096
|
+
const siteId = options.site;
|
|
1097
|
+
if (!siteId) {
|
|
1098
|
+
console.log("");
|
|
1099
|
+
console.log(chalk6.red("\u274C --site <id> is required when using --upload"));
|
|
1100
|
+
console.log(chalk6.gray(" Get your site ID from: https://etalon.nma.vc/dashboard/sites"));
|
|
1101
|
+
process.exit(1);
|
|
1102
|
+
}
|
|
1103
|
+
const cloudConfig = loadCloudConfig();
|
|
1104
|
+
if (!cloudConfig) {
|
|
1105
|
+
console.log("");
|
|
1106
|
+
console.log(chalk6.red("\u274C Not logged in. Run: etalon auth login"));
|
|
1107
|
+
process.exit(1);
|
|
1108
|
+
}
|
|
1109
|
+
const uploadSpinner = ora2("Uploading to ETALON Cloud...").start();
|
|
1110
|
+
const result = await uploadScan(cloudConfig, siteId, normalizedUrl, report, VERSION2);
|
|
1111
|
+
if (result.success) {
|
|
1112
|
+
uploadSpinner.succeed(`Uploaded! Grade: ${chalk6.bold(result.grade)} (${result.score}/100)`);
|
|
1113
|
+
console.log(chalk6.gray(` View: ${result.dashboardUrl}`));
|
|
1114
|
+
console.log("");
|
|
1115
|
+
} else {
|
|
1116
|
+
uploadSpinner.fail(`Upload failed: ${result.error}`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
826
1119
|
if (report.summary.highRisk > 0) {
|
|
827
1120
|
process.exit(1);
|
|
828
1121
|
}
|
|
@@ -846,39 +1139,39 @@ program.command("audit").description("Scan a codebase for GDPR compliance (track
|
|
|
846
1139
|
const includeBlame = options.includeBlame;
|
|
847
1140
|
const autoFix = options.fix;
|
|
848
1141
|
const showSpinner = format === "text";
|
|
849
|
-
const spinner = showSpinner ?
|
|
1142
|
+
const spinner = showSpinner ? ora2(`Auditing ${dir}...`).start() : null;
|
|
850
1143
|
try {
|
|
851
|
-
const report = await
|
|
1144
|
+
const report = await auditProject2(dir, {
|
|
852
1145
|
severity: options.severity,
|
|
853
1146
|
includeBlame
|
|
854
1147
|
});
|
|
855
|
-
const scoring =
|
|
1148
|
+
const scoring = applyContextScoring2(report.findings, dir);
|
|
856
1149
|
report.findings = scoring.adjustedFindings;
|
|
857
1150
|
if (scoring.adjustments.length > 0 && format === "text") {
|
|
858
1151
|
spinner?.stop();
|
|
859
|
-
console.log(
|
|
1152
|
+
console.log(chalk6.bold(`
|
|
860
1153
|
\u{1F3AF} Context: ${scoring.projectContext.industry} / ${scoring.projectContext.region}`));
|
|
861
|
-
console.log(
|
|
1154
|
+
console.log(chalk6.dim(` ${scoring.adjustments.length} finding(s) had severity adjusted based on context`));
|
|
862
1155
|
for (const adj of scoring.adjustments.slice(0, 5)) {
|
|
863
|
-
console.log(` ${
|
|
1156
|
+
console.log(` ${chalk6.dim(adj.finding_rule)}: ${adj.original_severity} \u2192 ${chalk6.yellow(adj.adjusted_severity)} (${adj.reason})`);
|
|
864
1157
|
}
|
|
865
1158
|
if (scoring.adjustments.length > 5) {
|
|
866
|
-
console.log(
|
|
1159
|
+
console.log(chalk6.dim(` ... and ${scoring.adjustments.length - 5} more`));
|
|
867
1160
|
}
|
|
868
1161
|
}
|
|
869
1162
|
spinner?.stop();
|
|
870
1163
|
if (autoFix) {
|
|
871
1164
|
const patches = generatePatches(report.findings, dir);
|
|
872
1165
|
if (patches.length > 0) {
|
|
873
|
-
console.log(
|
|
1166
|
+
console.log(chalk6.bold(`
|
|
874
1167
|
\u{1F527} ${patches.length} config fix(es):`));
|
|
875
1168
|
for (const p of patches) {
|
|
876
|
-
console.log(` ${
|
|
877
|
-
console.log(` ${
|
|
878
|
-
console.log(` ${
|
|
1169
|
+
console.log(` ${chalk6.dim(p.file)}:${p.line} \u2014 ${p.description}`);
|
|
1170
|
+
console.log(` ${chalk6.red("- " + p.oldContent.trim())}`);
|
|
1171
|
+
console.log(` ${chalk6.green("+ " + p.newContent.trim())}`);
|
|
879
1172
|
}
|
|
880
1173
|
const applied = applyPatches(patches, dir);
|
|
881
|
-
console.log(
|
|
1174
|
+
console.log(chalk6.green(`
|
|
882
1175
|
\u2713 Applied ${applied} config fix(es).`));
|
|
883
1176
|
}
|
|
884
1177
|
const engine = new AutoFixEngine();
|
|
@@ -903,23 +1196,23 @@ program.command("audit").description("Scan a codebase for GDPR compliance (track
|
|
|
903
1196
|
collectCodeFiles(dir);
|
|
904
1197
|
const suggestions = engine.scanFiles(codeFiles);
|
|
905
1198
|
if (suggestions.length > 0) {
|
|
906
|
-
console.log(
|
|
1199
|
+
console.log(chalk6.bold(`
|
|
907
1200
|
\u{1F6E1}\uFE0F ${suggestions.length} tracker consent fix(es) available:`));
|
|
908
1201
|
for (const s of suggestions) {
|
|
909
|
-
console.log(` ${
|
|
910
|
-
console.log(` ${
|
|
1202
|
+
console.log(` ${chalk6.cyan(s.tracker_name)} in ${chalk6.dim(s.location.file)}:${s.location.line}`);
|
|
1203
|
+
console.log(` ${chalk6.dim(s.description)}`);
|
|
911
1204
|
}
|
|
912
1205
|
const hookPath = engine.generateConsentHook(dir);
|
|
913
|
-
console.log(
|
|
914
|
-
\u2713 Generated consent hook: ${
|
|
1206
|
+
console.log(chalk6.green(`
|
|
1207
|
+
\u2713 Generated consent hook: ${chalk6.cyan(hookPath)}`));
|
|
915
1208
|
const result = engine.applyAllFixes(suggestions);
|
|
916
|
-
console.log(
|
|
1209
|
+
console.log(chalk6.green(`\u2713 Applied ${result.applied} tracker consent fix(es).`));
|
|
917
1210
|
if (result.failed > 0) {
|
|
918
|
-
console.log(
|
|
1211
|
+
console.log(chalk6.yellow(`\u26A0 ${result.failed} fix(es) could not be applied.`));
|
|
919
1212
|
}
|
|
920
1213
|
}
|
|
921
1214
|
if (patches.length === 0 && suggestions.length === 0) {
|
|
922
|
-
console.log(
|
|
1215
|
+
console.log(chalk6.yellow("\nNo auto-fixable issues found."));
|
|
923
1216
|
}
|
|
924
1217
|
}
|
|
925
1218
|
recordAuditEvent({
|
|
@@ -941,8 +1234,8 @@ program.command("audit").description("Scan a codebase for GDPR compliance (track
|
|
|
941
1234
|
case "html": {
|
|
942
1235
|
const html = generateHtmlReport(report);
|
|
943
1236
|
const outPath = "etalon-report.html";
|
|
944
|
-
|
|
945
|
-
console.log(
|
|
1237
|
+
writeFileSync3(outPath, html, "utf-8");
|
|
1238
|
+
console.log(chalk6.green(`\u2713 Report written to ${chalk6.cyan(outPath)}`));
|
|
946
1239
|
break;
|
|
947
1240
|
}
|
|
948
1241
|
case "text":
|
|
@@ -993,7 +1286,7 @@ program.command("init").description("Set up ETALON in your project (config, CI,
|
|
|
993
1286
|
program.command("consent-check").description("Test if trackers fire before/after rejecting cookies on a website").argument("<url>", "URL to check").option("-f, --format <format>", "Output format: text, json", "text").option("-t, --timeout <ms>", "Navigation timeout", "15000").action(async (url, options) => {
|
|
994
1287
|
const normalizedUrl = normalizeUrl(url);
|
|
995
1288
|
const format = options.format ?? "text";
|
|
996
|
-
const spinner = format === "text" ?
|
|
1289
|
+
const spinner = format === "text" ? ora2(`Checking consent on ${normalizedUrl}...`).start() : null;
|
|
997
1290
|
try {
|
|
998
1291
|
const { checkConsent } = await import("./consent-checker-QRPTMQWN.js");
|
|
999
1292
|
const result = await checkConsent(normalizedUrl, {
|
|
@@ -1004,33 +1297,33 @@ program.command("consent-check").description("Test if trackers fire before/after
|
|
|
1004
1297
|
console.log(JSON.stringify(result, null, 2));
|
|
1005
1298
|
} else {
|
|
1006
1299
|
console.log("");
|
|
1007
|
-
console.log(
|
|
1008
|
-
console.log(
|
|
1009
|
-
console.log(`URL: ${
|
|
1010
|
-
console.log(`Banner: ${result.bannerDetected ?
|
|
1011
|
-
console.log(`Reject: ${result.rejectClicked ?
|
|
1300
|
+
console.log(chalk6.bold("ETALON Consent Verification"));
|
|
1301
|
+
console.log(chalk6.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\u2550\u2550\u2550\u2550"));
|
|
1302
|
+
console.log(`URL: ${chalk6.cyan(result.url)}`);
|
|
1303
|
+
console.log(`Banner: ${result.bannerDetected ? chalk6.green("\u2713 Detected") : chalk6.red("\u2717 Not found")}${result.bannerType ? ` (${result.bannerType})` : ""}`);
|
|
1304
|
+
console.log(`Reject: ${result.rejectClicked ? chalk6.green("\u2713 Clicked") : chalk6.yellow("\u2717 Could not reject")}`);
|
|
1012
1305
|
console.log("");
|
|
1013
1306
|
if (result.preConsentTrackers.length > 0) {
|
|
1014
|
-
console.log(
|
|
1307
|
+
console.log(chalk6.bold("\u{1F50D} Trackers before consent:"));
|
|
1015
1308
|
for (const t of result.preConsentTrackers) {
|
|
1016
|
-
console.log(` \u2022 ${t.name} (${
|
|
1309
|
+
console.log(` \u2022 ${t.name} (${chalk6.dim(t.matchedDomain)})`);
|
|
1017
1310
|
}
|
|
1018
1311
|
console.log("");
|
|
1019
1312
|
}
|
|
1020
1313
|
if (result.postRejectTrackers.length > 0) {
|
|
1021
|
-
console.log(
|
|
1314
|
+
console.log(chalk6.bold("\u26A0\uFE0F Trackers after rejection:"));
|
|
1022
1315
|
for (const t of result.postRejectTrackers) {
|
|
1023
|
-
console.log(` \u2022 ${
|
|
1316
|
+
console.log(` \u2022 ${chalk6.red(t.name)} (${chalk6.dim(t.matchedDomain)})`);
|
|
1024
1317
|
}
|
|
1025
1318
|
console.log("");
|
|
1026
1319
|
}
|
|
1027
1320
|
if (result.violations.length > 0) {
|
|
1028
|
-
console.log(
|
|
1321
|
+
console.log(chalk6.red.bold(`\u{1F534} ${result.violations.length} consent violation(s)`));
|
|
1029
1322
|
for (const v of result.violations) {
|
|
1030
1323
|
console.log(` ${v.phase === "before-interaction" ? "\u23F1" : "\u{1F534}"} ${v.message}`);
|
|
1031
1324
|
}
|
|
1032
1325
|
} else {
|
|
1033
|
-
console.log(
|
|
1326
|
+
console.log(chalk6.green.bold("\u2713 No consent violations detected"));
|
|
1034
1327
|
}
|
|
1035
1328
|
console.log("");
|
|
1036
1329
|
}
|
|
@@ -1047,7 +1340,7 @@ Error: ${error.message}`);
|
|
|
1047
1340
|
program.command("policy-check").description("Cross-reference privacy policy text against actual detected trackers").argument("<url>", "URL to check").option("-f, --format <format>", "Output format: text, json", "text").option("-t, --timeout <ms>", "Navigation timeout", "30000").option("--policy-url <url>", "Directly specify the privacy policy URL").action(async (url, options) => {
|
|
1048
1341
|
const normalizedUrl = normalizeUrl(url);
|
|
1049
1342
|
const format = options.format ?? "text";
|
|
1050
|
-
const spinner = format === "text" ?
|
|
1343
|
+
const spinner = format === "text" ? ora2(`Analyzing privacy policy for ${normalizedUrl}...`).start() : null;
|
|
1051
1344
|
try {
|
|
1052
1345
|
const { checkPolicy } = await import("./policy-checker-ONMTI7X2.js");
|
|
1053
1346
|
const result = await checkPolicy(normalizedUrl, {
|
|
@@ -1059,60 +1352,60 @@ program.command("policy-check").description("Cross-reference privacy policy text
|
|
|
1059
1352
|
console.log(JSON.stringify(result, null, 2));
|
|
1060
1353
|
} else {
|
|
1061
1354
|
console.log("");
|
|
1062
|
-
console.log(
|
|
1063
|
-
console.log(
|
|
1064
|
-
console.log(`URL: ${
|
|
1065
|
-
console.log(`Policy page: ${result.policyFound ?
|
|
1355
|
+
console.log(chalk6.bold("ETALON Policy vs. Reality Audit"));
|
|
1356
|
+
console.log(chalk6.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\u2550\u2550\u2550\u2550"));
|
|
1357
|
+
console.log(`URL: ${chalk6.cyan(result.url)}`);
|
|
1358
|
+
console.log(`Policy page: ${result.policyFound ? chalk6.green(result.policyUrl) : chalk6.red("\u2717 Not found")}`);
|
|
1066
1359
|
console.log("");
|
|
1067
|
-
console.log(`\u{1F4CB} Vendors mentioned in policy: ${
|
|
1068
|
-
console.log(`\u{1F50D} Vendors detected by scan: ${
|
|
1360
|
+
console.log(`\u{1F4CB} Vendors mentioned in policy: ${chalk6.bold(String(result.mentionedVendors.length))}`);
|
|
1361
|
+
console.log(`\u{1F50D} Vendors detected by scan: ${chalk6.bold(String(result.detectedVendors.length))}`);
|
|
1069
1362
|
console.log("");
|
|
1070
1363
|
if (!result.policyFound) {
|
|
1071
|
-
console.log(
|
|
1364
|
+
console.log(chalk6.red.bold("\u26A0 No privacy policy page found \u2014 all detected trackers are undisclosed"));
|
|
1072
1365
|
console.log("");
|
|
1073
1366
|
}
|
|
1074
1367
|
if (result.undisclosed.length > 0) {
|
|
1075
|
-
console.log(
|
|
1368
|
+
console.log(chalk6.red.bold(`\u{1F534} ${result.undisclosed.length} UNDISCLOSED (detected on site, not in policy):`));
|
|
1076
1369
|
for (const m of result.undisclosed) {
|
|
1077
|
-
const icon = m.severity === "critical" ?
|
|
1370
|
+
const icon = m.severity === "critical" ? chalk6.red("\u2717 CRITICAL") : m.severity === "high" ? chalk6.yellow("\u2717 HIGH") : m.severity === "medium" ? chalk6.yellow("\u2717 MEDIUM") : chalk6.dim("\u2717 LOW");
|
|
1078
1371
|
console.log(` ${icon} ${m.vendorName} \u2014 ${m.message}`);
|
|
1079
1372
|
}
|
|
1080
1373
|
console.log("");
|
|
1081
1374
|
}
|
|
1082
1375
|
if (result.disclosed.length > 0) {
|
|
1083
|
-
console.log(
|
|
1376
|
+
console.log(chalk6.green.bold(`\u2713 ${result.disclosed.length} DISCLOSED (in both policy and scan):`));
|
|
1084
1377
|
for (const m of result.disclosed) {
|
|
1085
|
-
console.log(` ${
|
|
1378
|
+
console.log(` ${chalk6.green("\u2713")} ${m.vendorName}`);
|
|
1086
1379
|
}
|
|
1087
1380
|
console.log("");
|
|
1088
1381
|
}
|
|
1089
1382
|
if (result.overclaimed.length > 0) {
|
|
1090
|
-
console.log(
|
|
1383
|
+
console.log(chalk6.dim(`\u2139 ${result.overclaimed.length} OVERCLAIMED (in policy, not detected):`));
|
|
1091
1384
|
for (const m of result.overclaimed) {
|
|
1092
|
-
console.log(` ${
|
|
1385
|
+
console.log(` ${chalk6.dim("\u2013")} ${m.vendorName}`);
|
|
1093
1386
|
}
|
|
1094
1387
|
console.log("");
|
|
1095
1388
|
}
|
|
1096
1389
|
if (result.pass) {
|
|
1097
|
-
console.log(
|
|
1390
|
+
console.log(chalk6.green.bold("\u2713 All detected trackers are disclosed in the privacy policy"));
|
|
1098
1391
|
} else {
|
|
1099
|
-
console.log(
|
|
1392
|
+
console.log(chalk6.red.bold(`\u2717 ${result.undisclosed.length} tracker(s) not disclosed in privacy policy`));
|
|
1100
1393
|
}
|
|
1101
1394
|
console.log("");
|
|
1102
1395
|
if (result.disclosures.length > 0) {
|
|
1103
|
-
console.log(
|
|
1104
|
-
console.log(
|
|
1396
|
+
console.log(chalk6.bold.cyan("\u{1F4DD} Add this to your privacy policy:"));
|
|
1397
|
+
console.log(chalk6.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1105
1398
|
for (const d of result.disclosures) {
|
|
1106
1399
|
console.log("");
|
|
1107
|
-
console.log(
|
|
1400
|
+
console.log(chalk6.bold(` ${d.vendorName}`));
|
|
1108
1401
|
console.log(` ${d.snippet}`);
|
|
1109
1402
|
if (d.dpaUrl) {
|
|
1110
|
-
console.log(
|
|
1403
|
+
console.log(chalk6.dim(` DPA: ${d.dpaUrl}`));
|
|
1111
1404
|
}
|
|
1112
1405
|
}
|
|
1113
1406
|
console.log("");
|
|
1114
|
-
console.log(
|
|
1115
|
-
console.log(
|
|
1407
|
+
console.log(chalk6.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1408
|
+
console.log(chalk6.dim("Copy the text above into your privacy policy or CMS."));
|
|
1116
1409
|
console.log("");
|
|
1117
1410
|
}
|
|
1118
1411
|
}
|
|
@@ -1127,10 +1420,10 @@ Error: ${error.message}`);
|
|
|
1127
1420
|
}
|
|
1128
1421
|
});
|
|
1129
1422
|
program.command("generate-policy").description("Generate a GDPR privacy policy from code audit + network scan").argument("[dir]", "Project directory to audit", "./").requiredOption("--company <name>", "Your company/organization name").requiredOption("--email <email>", "Privacy contact / DPO email").option("--url <url>", "Also scan a live URL for network trackers").option("--country <country>", 'Jurisdiction (e.g. "EU", "Germany")').option("-o, --output <file>", "Output file", "privacy-policy.md").option("-f, --format <format>", "Output format: md, html, txt", "md").action(async (dir, options) => {
|
|
1130
|
-
const spinner =
|
|
1423
|
+
const spinner = ora2("Generating privacy policy...").start();
|
|
1131
1424
|
try {
|
|
1132
1425
|
spinner.text = "Running code audit...";
|
|
1133
|
-
const audit = await
|
|
1426
|
+
const audit = await auditProject2(dir);
|
|
1134
1427
|
spinner.text = "Analyzing data flows...";
|
|
1135
1428
|
const { collectFiles } = await import("@etalon/core");
|
|
1136
1429
|
let dataFlow;
|
|
@@ -1161,32 +1454,32 @@ program.command("generate-policy").description("Generate a GDPR privacy policy f
|
|
|
1161
1454
|
dataFlow
|
|
1162
1455
|
});
|
|
1163
1456
|
const outputFile = options.output ?? "privacy-policy.md";
|
|
1164
|
-
|
|
1457
|
+
writeFileSync3(outputFile, policy.fullText, "utf-8");
|
|
1165
1458
|
spinner.succeed(`Privacy policy generated!`);
|
|
1166
1459
|
console.log("");
|
|
1167
|
-
console.log(
|
|
1168
|
-
console.log(
|
|
1169
|
-
console.log(`Company: ${
|
|
1170
|
-
console.log(`Contact: ${
|
|
1460
|
+
console.log(chalk6.bold("ETALON Privacy Policy Generator"));
|
|
1461
|
+
console.log(chalk6.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"));
|
|
1462
|
+
console.log(`Company: ${chalk6.cyan(options.company)}`);
|
|
1463
|
+
console.log(`Contact: ${chalk6.cyan(options.email)}`);
|
|
1171
1464
|
if (options.url) {
|
|
1172
|
-
console.log(`Site scanned: ${
|
|
1465
|
+
console.log(`Site scanned: ${chalk6.cyan(options.url)}`);
|
|
1173
1466
|
}
|
|
1174
|
-
console.log(`Sources: ${
|
|
1467
|
+
console.log(`Sources: ${chalk6.dim(policy.meta.sources.join(", "))}`);
|
|
1175
1468
|
console.log("");
|
|
1176
|
-
console.log(`\u{1F4CB} Sections: ${
|
|
1177
|
-
console.log(`\u{1F50D} Vendors: ${
|
|
1178
|
-
console.log(`\u{1F6E1}\uFE0F PII types: ${
|
|
1469
|
+
console.log(`\u{1F4CB} Sections: ${chalk6.bold(String(policy.sections.length))}`);
|
|
1470
|
+
console.log(`\u{1F50D} Vendors: ${chalk6.bold(String(policy.vendors.length))}`);
|
|
1471
|
+
console.log(`\u{1F6E1}\uFE0F PII types: ${chalk6.bold(String(policy.piiTypes.length))}`);
|
|
1179
1472
|
console.log("");
|
|
1180
1473
|
if (policy.vendors.length > 0) {
|
|
1181
|
-
console.log(
|
|
1474
|
+
console.log(chalk6.dim("Third-party vendors included:"));
|
|
1182
1475
|
for (const v of policy.vendors) {
|
|
1183
|
-
const src = v.source === "both" ?
|
|
1184
|
-
console.log(` ${
|
|
1476
|
+
const src = v.source === "both" ? chalk6.magenta("code+network") : v.source === "code" ? chalk6.blue("code") : chalk6.green("network");
|
|
1477
|
+
console.log(` ${chalk6.bold(v.vendorName)} ${chalk6.dim(`(${v.category})`)} \u2014 ${src}`);
|
|
1185
1478
|
}
|
|
1186
1479
|
console.log("");
|
|
1187
1480
|
}
|
|
1188
|
-
console.log(
|
|
1189
|
-
console.log(
|
|
1481
|
+
console.log(chalk6.green(`\u2713 Written to ${chalk6.bold(outputFile)}`));
|
|
1482
|
+
console.log(chalk6.dim("\u26A0 Review with a legal professional before publishing."));
|
|
1190
1483
|
console.log("");
|
|
1191
1484
|
} catch (error) {
|
|
1192
1485
|
spinner.fail("Policy generation failed");
|
|
@@ -1198,16 +1491,16 @@ Error: ${error.message}`);
|
|
|
1198
1491
|
}
|
|
1199
1492
|
});
|
|
1200
1493
|
program.command("badge").description("Generate a compliance badge SVG for your README").argument("[dir]", "Directory to audit", "./").option("-o, --output <file>", "Output file", "etalon-badge.svg").action(async (dir, options) => {
|
|
1201
|
-
const spinner =
|
|
1494
|
+
const spinner = ora2("Generating badge...").start();
|
|
1202
1495
|
try {
|
|
1203
|
-
const report = await
|
|
1496
|
+
const report = await auditProject2(dir);
|
|
1204
1497
|
const score = report.score ?? calculateScore(report);
|
|
1205
1498
|
const svg = generateBadgeSvg(score);
|
|
1206
|
-
|
|
1207
|
-
spinner.succeed(`Badge written to ${
|
|
1499
|
+
writeFileSync3(options.output, svg, "utf-8");
|
|
1500
|
+
spinner.succeed(`Badge written to ${chalk6.cyan(options.output)} \u2014 Grade: ${score.grade} (${score.score}/100)`);
|
|
1208
1501
|
console.log("");
|
|
1209
|
-
console.log(
|
|
1210
|
-
console.log(
|
|
1502
|
+
console.log(chalk6.bold("Shields.io badge for your README:"));
|
|
1503
|
+
console.log(chalk6.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1211
1504
|
const { badgeMarkdown: toBadgeMd } = await import("@etalon/core");
|
|
1212
1505
|
console.log(toBadgeMd(score.grade, score.score));
|
|
1213
1506
|
console.log("");
|
|
@@ -1219,14 +1512,14 @@ Error: ${error.message}`);
|
|
|
1219
1512
|
}
|
|
1220
1513
|
});
|
|
1221
1514
|
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) => {
|
|
1222
|
-
const spinner =
|
|
1515
|
+
const spinner = ora2("Analyzing data flows...").start();
|
|
1223
1516
|
try {
|
|
1224
1517
|
const { readdirSync } = await import("fs");
|
|
1225
|
-
const { join:
|
|
1518
|
+
const { join: join4, relative } = await import("path");
|
|
1226
1519
|
const files = [];
|
|
1227
1520
|
const walk = (d) => {
|
|
1228
1521
|
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
1229
|
-
const full =
|
|
1522
|
+
const full = join4(d, entry.name);
|
|
1230
1523
|
if (entry.isDirectory()) {
|
|
1231
1524
|
if (!["node_modules", ".git", "dist", "build", ".next", "__pycache__"].includes(entry.name)) {
|
|
1232
1525
|
walk(full);
|
|
@@ -1266,76 +1559,96 @@ program.command("report-fp").description("Report a false positive finding").requ
|
|
|
1266
1559
|
reason: options.reason ?? "Not a tracker",
|
|
1267
1560
|
suggested_action: "whitelist_domain"
|
|
1268
1561
|
});
|
|
1269
|
-
console.log(
|
|
1270
|
-
console.log(
|
|
1271
|
-
console.log(
|
|
1272
|
-
console.log(
|
|
1562
|
+
console.log(chalk6.green(`\u2713 False positive reported: ${chalk6.bold(report.id)}`));
|
|
1563
|
+
console.log(chalk6.dim(` Domain: ${report.domain}`));
|
|
1564
|
+
console.log(chalk6.dim(` Rule: ${report.rule}`));
|
|
1565
|
+
console.log(chalk6.dim(` Reason: ${report.reason}`));
|
|
1273
1566
|
console.log("");
|
|
1274
|
-
console.log(
|
|
1567
|
+
console.log(chalk6.dim("Reports help ETALON learn and reduce false positives over time."));
|
|
1275
1568
|
});
|
|
1276
1569
|
program.command("telemetry").description("Manage anonymous usage telemetry").argument("<action>", "enable, disable, or status").action((action) => {
|
|
1277
1570
|
switch (action) {
|
|
1278
1571
|
case "enable":
|
|
1279
1572
|
enableTelemetry();
|
|
1280
|
-
console.log(
|
|
1281
|
-
console.log(
|
|
1282
|
-
console.log(
|
|
1573
|
+
console.log(chalk6.green("\u2713 Telemetry enabled."));
|
|
1574
|
+
console.log(chalk6.dim(" Anonymous usage data helps improve ETALON for everyone."));
|
|
1575
|
+
console.log(chalk6.dim(" No PII is ever collected. Set DO_NOT_TRACK=1 to override."));
|
|
1283
1576
|
break;
|
|
1284
1577
|
case "disable":
|
|
1285
1578
|
disableTelemetry();
|
|
1286
|
-
console.log(
|
|
1579
|
+
console.log(chalk6.yellow("\u2713 Telemetry disabled."));
|
|
1287
1580
|
break;
|
|
1288
1581
|
case "status":
|
|
1289
|
-
console.log(`Telemetry: ${isTelemetryEnabled() ?
|
|
1582
|
+
console.log(`Telemetry: ${isTelemetryEnabled() ? chalk6.green("enabled") : chalk6.yellow("disabled")}`);
|
|
1290
1583
|
break;
|
|
1291
1584
|
default:
|
|
1292
|
-
console.error(
|
|
1585
|
+
console.error(chalk6.red(`Unknown action: ${action}. Use enable, disable, or status.`));
|
|
1293
1586
|
process.exit(1);
|
|
1294
1587
|
}
|
|
1295
1588
|
});
|
|
1296
1589
|
program.command("intelligence").description("Show intelligence engine status and learned patterns").argument("[dir]", "Project directory for context detection", "./").action((dir) => {
|
|
1297
|
-
console.log(
|
|
1298
|
-
console.log(
|
|
1590
|
+
console.log(chalk6.bold("ETALON Intelligence Engine"));
|
|
1591
|
+
console.log(chalk6.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
1592
|
console.log("");
|
|
1300
1593
|
const ctx = detectProjectContext(dir);
|
|
1301
|
-
console.log(
|
|
1302
|
-
console.log(` Industry: ${
|
|
1303
|
-
console.log(` Region: ${
|
|
1304
|
-
console.log(` Data Sensitivity: ${
|
|
1594
|
+
console.log(chalk6.bold("\u{1F3AF} Project Context"));
|
|
1595
|
+
console.log(` Industry: ${chalk6.cyan(ctx.industry)}`);
|
|
1596
|
+
console.log(` Region: ${chalk6.cyan(ctx.region)}`);
|
|
1597
|
+
console.log(` Data Sensitivity: ${chalk6.cyan(ctx.data_sensitivity)}`);
|
|
1305
1598
|
if (ctx.detected_signals.length > 0) {
|
|
1306
|
-
console.log(
|
|
1599
|
+
console.log(chalk6.dim(" Signals:"));
|
|
1307
1600
|
for (const s of ctx.detected_signals) {
|
|
1308
|
-
console.log(
|
|
1601
|
+
console.log(chalk6.dim(` \u2022 ${s}`));
|
|
1309
1602
|
}
|
|
1310
1603
|
}
|
|
1311
1604
|
console.log("");
|
|
1312
1605
|
const stats = getLearningStats();
|
|
1313
|
-
console.log(
|
|
1314
|
-
console.log(` Patterns learned: ${
|
|
1315
|
-
console.log(` Feedback processed: ${
|
|
1316
|
-
console.log(` Impact: ${
|
|
1606
|
+
console.log(chalk6.bold("\u{1F9E0} Learning Engine"));
|
|
1607
|
+
console.log(` Patterns learned: ${chalk6.cyan(String(stats.patterns_learned))}`);
|
|
1608
|
+
console.log(` Feedback processed: ${chalk6.cyan(String(stats.feedback_processed))}`);
|
|
1609
|
+
console.log(` Impact: ${chalk6.cyan(stats.accuracy_improvement)}`);
|
|
1317
1610
|
console.log("");
|
|
1318
1611
|
const feedback = getFeedbackSummary();
|
|
1319
1612
|
if (feedback.total_reports > 0) {
|
|
1320
|
-
console.log(
|
|
1613
|
+
console.log(chalk6.bold("\u{1F4CA} False Positive Reports"));
|
|
1321
1614
|
console.log(` Total reports: ${feedback.total_reports}`);
|
|
1322
1615
|
if (feedback.suggested_whitelists.length > 0) {
|
|
1323
|
-
console.log(
|
|
1616
|
+
console.log(chalk6.dim(" Suggested whitelists (3+ reports):"));
|
|
1324
1617
|
for (const d of feedback.suggested_whitelists) {
|
|
1325
|
-
console.log(
|
|
1618
|
+
console.log(chalk6.dim(` \u2022 ${d}`));
|
|
1326
1619
|
}
|
|
1327
1620
|
}
|
|
1328
1621
|
console.log("");
|
|
1329
1622
|
}
|
|
1330
1623
|
const learned = analyzePatterns();
|
|
1331
1624
|
if (learned.length > 0) {
|
|
1332
|
-
console.log(
|
|
1625
|
+
console.log(chalk6.bold("\u{1F4DD} Learned Patterns"));
|
|
1333
1626
|
for (const p of learned) {
|
|
1334
|
-
console.log(` ${
|
|
1627
|
+
console.log(` ${chalk6.cyan(p.domain)} \u2014 ${p.suggested_action} (confidence: ${(p.confidence * 100).toFixed(0)}%)`);
|
|
1335
1628
|
}
|
|
1336
1629
|
console.log("");
|
|
1337
1630
|
}
|
|
1338
|
-
console.log(
|
|
1631
|
+
console.log(chalk6.dim("Telemetry: ") + (isTelemetryEnabled() ? chalk6.green("enabled") : chalk6.yellow("disabled")));
|
|
1339
1632
|
console.log("");
|
|
1340
1633
|
});
|
|
1634
|
+
var authCmd = program.command("auth").description("Manage ETALON Cloud authentication");
|
|
1635
|
+
authCmd.command("login").description("Login with API key from dashboard").action(async () => {
|
|
1636
|
+
await runLogin();
|
|
1637
|
+
});
|
|
1638
|
+
authCmd.command("logout").description("Logout and remove stored API key").action(() => {
|
|
1639
|
+
runLogout();
|
|
1640
|
+
});
|
|
1641
|
+
authCmd.command("status").description("Show current authentication status").action(async () => {
|
|
1642
|
+
await runStatus();
|
|
1643
|
+
});
|
|
1644
|
+
program.command("push").description("Scan a website, audit codebase, and upload results to ETALON Cloud").argument("<url>", "URL to scan").argument("[dir]", "Directory to audit", "./").option("--site <id>", "Site ID (from dashboard or `etalon sites`)").option("-t, --timeout <ms>", "Navigation timeout in milliseconds", "30000").option("-d, --deep", "Deep scan mode", false).action(async (url, dir, options) => {
|
|
1645
|
+
await runPush(url, dir, {
|
|
1646
|
+
site: options.site,
|
|
1647
|
+
timeout: options.timeout,
|
|
1648
|
+
deep: options.deep
|
|
1649
|
+
});
|
|
1650
|
+
});
|
|
1651
|
+
program.command("sites").description("List your cloud sites and their IDs").action(async () => {
|
|
1652
|
+
await runListSites();
|
|
1653
|
+
});
|
|
1341
1654
|
program.parse();
|
package/package.json
CHANGED