@lintsure/core 0.1.0 → 0.1.1
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/src/assessment.js +2 -2
- package/dist/src/index.d.ts +6 -3
- package/dist/src/index.js +39 -19
- package/dist/src/rules/index.js +4 -2
- package/package.json +1 -1
package/dist/src/assessment.js
CHANGED
|
@@ -7,7 +7,7 @@ const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">
|
|
|
7
7
|
const NAME = { "eu-ai-act": "EU AI Act", gdpr: "GDPR", soc2: "SOC 2", "iso-27001": "ISO/IEC 27001" };
|
|
8
8
|
const STATUS = { gap: "Gap", partial: "Partial", met: "Met", outside: "Outside code" };
|
|
9
9
|
const SEV = ["high", "medium", "low"];
|
|
10
|
-
const SKIPPED = "node_modules, .git, dist, build, .next, venv, .venv, __pycache__, coverage";
|
|
10
|
+
const SKIPPED = "node_modules, .git, dist, build, out, .output, .next, .nuxt, .svelte-kit, .astro, .vercel, .netlify, .wrangler, .turbo, .cache, venv, .venv, __pycache__, coverage";
|
|
11
11
|
function page(title, body, n, total, project) {
|
|
12
12
|
return `<section class="page"><header><span>Lintsure readiness assessment · ${esc(project)}</span><span>Draft until reviewed by a person</span></header>
|
|
13
13
|
<h2>${esc(title)}</h2>${body}<footer><span>Not an audit, a certification or legal advice.</span><span>Section ${n} of ${total}</span></footer></section>`;
|
|
@@ -51,7 +51,7 @@ export function toAssessmentHtml(r, opts) {
|
|
|
51
51
|
${fws.map((f) => `<tr><td>${NAME[f]}</td><td>${r.findings.filter((x) => x.status === "gap" && x.controls.some((c) => c.framework === f)).length}</td><td>${r.findings.filter((x) => x.status === "partial" && x.controls.some((c) => c.framework === f)).length}</td><td>${r.met.filter((m) => m.controls.some((c) => c.framework === f)).length}</td><td>${r.outside.filter((o) => o.framework === f).length}</td></tr>`).join("")}</table>
|
|
52
52
|
<div class="callout"><b>What this summary does not mean.</b> ${esc(NOT_A_VERDICT)}</div>`;
|
|
53
53
|
const method = `<h3>What was checked</h3>
|
|
54
|
-
<p>Lintsure read ${r.filesChecked} files in the project folder on the computer or CI runner where it ran. Nothing was uploaded. It skipped these folders: ${SKIPPED}, and any file over 1 MB or binary.</p>
|
|
54
|
+
<p>Lintsure read ${r.filesChecked} files in the project folder on the computer or CI runner where it ran. Nothing was uploaded. It skipped these folders: ${SKIPPED}, any paths listed under ignore in lintsure.config.yaml, minified files, and any file over 1 MB or binary.</p>
|
|
55
55
|
<h3>How results are produced</h3>
|
|
56
56
|
<ul><li>A fixed set of rules (version ${esc(r.rulesVersion)}) looks for known patterns, line by line or file by file. It does not use a language model to decide results, so the same code gives the same findings.</li>
|
|
57
57
|
<li><b>Gap</b>: a rule found code that may break a control. <b>Partial</b>: a likely problem that depends on context only you can confirm. <b>Met</b>: the rule ran and found nothing in the files it read. <b>Outside code</b>: work that code cannot show.</li>
|
package/dist/src/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { rules };
|
|
|
5
5
|
export * from "./guidance.js";
|
|
6
6
|
export { toAssessmentHtml } from "./assessment.js";
|
|
7
7
|
export * from "./knowledge/index.js";
|
|
8
|
-
export declare const RULES_VERSION = "0.1.
|
|
8
|
+
export declare const RULES_VERSION = "0.1.1";
|
|
9
9
|
/** Shown with every result, in every interface. Lintsure prepares; people decide. */
|
|
10
10
|
export declare const DISCLAIMER: string;
|
|
11
11
|
/** What a report must never be read as. Used in the report and by the MCP server. */
|
|
@@ -17,12 +17,15 @@ export declare const ALL_FRAMEWORKS: FrameworkId[];
|
|
|
17
17
|
* Only the simple list form is supported:
|
|
18
18
|
* frameworks:
|
|
19
19
|
* - gdpr
|
|
20
|
-
* Ids Lintsure doesn't check yet (for example "hipaa") are returned in `skipped`.
|
|
20
|
+
* Ids Lintsure doesn't check yet (for example "hipaa") are returned in `skipped`.
|
|
21
|
+
* An `ignore:` list of paths (relative to the root, for example tests/ or fixtures/) is skipped by scan(). */
|
|
21
22
|
export declare function readConfig(root: string): Promise<{
|
|
22
23
|
frameworks: FrameworkId[] | null;
|
|
23
24
|
skipped: string[];
|
|
25
|
+
ignore: string[];
|
|
24
26
|
}>;
|
|
27
|
+
export declare const SKIP_DIRS: string[];
|
|
25
28
|
/** Scout: list files to check, sorted so output order never depends on the file system. */
|
|
26
|
-
export declare function listFiles(root: string): Promise<string[]>;
|
|
29
|
+
export declare function listFiles(root: string, ignore?: string[]): Promise<string[]>;
|
|
27
30
|
export declare function scan(root: string, frameworks?: FrameworkId[]): Promise<Report>;
|
|
28
31
|
export declare function toMarkdown(r: Report): string;
|
package/dist/src/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { rules };
|
|
|
9
9
|
export * from "./guidance.js";
|
|
10
10
|
export { toAssessmentHtml } from "./assessment.js";
|
|
11
11
|
export * from "./knowledge/index.js";
|
|
12
|
-
export const RULES_VERSION = "0.1.
|
|
12
|
+
export const RULES_VERSION = "0.1.1";
|
|
13
13
|
/** Shown with every result, in every interface. Lintsure prepares; people decide. */
|
|
14
14
|
export const DISCLAIMER = "Lintsure is a readiness tool, not an audit, a certification or legal advice. " +
|
|
15
15
|
"Its rules reflect our reading of the publicly available text of each framework, and they check code only. " +
|
|
@@ -34,46 +34,64 @@ export const ALL_FRAMEWORKS = ["gdpr", "soc2", "iso-27001", "eu-ai-act"];
|
|
|
34
34
|
* Only the simple list form is supported:
|
|
35
35
|
* frameworks:
|
|
36
36
|
* - gdpr
|
|
37
|
-
* Ids Lintsure doesn't check yet (for example "hipaa") are returned in `skipped`.
|
|
37
|
+
* Ids Lintsure doesn't check yet (for example "hipaa") are returned in `skipped`.
|
|
38
|
+
* An `ignore:` list of paths (relative to the root, for example tests/ or fixtures/) is skipped by scan(). */
|
|
38
39
|
export async function readConfig(root) {
|
|
39
40
|
let text;
|
|
40
41
|
try {
|
|
41
42
|
text = await readFile(join(root, "lintsure.config.yaml"), "utf8");
|
|
42
43
|
}
|
|
43
44
|
catch {
|
|
44
|
-
return { frameworks: null, skipped: [] };
|
|
45
|
+
return { frameworks: null, skipped: [], ignore: [] };
|
|
45
46
|
}
|
|
46
|
-
const
|
|
47
|
-
let
|
|
47
|
+
const lists = { frameworks: [], ignore: [] };
|
|
48
|
+
let key = "";
|
|
49
|
+
let emptyFw = false;
|
|
48
50
|
for (const raw of text.split(/\r?\n/)) {
|
|
49
51
|
const line = raw.replace(/#.*$/, "").trimEnd();
|
|
50
|
-
if (/^frameworks\s*:\s*\[\s*\]\s*$/.test(line))
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
inList = true;
|
|
52
|
+
if (/^frameworks\s*:\s*\[\s*\]\s*$/.test(line)) {
|
|
53
|
+
emptyFw = true;
|
|
54
|
+
key = "";
|
|
54
55
|
continue;
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const head = line.match(/^(frameworks|ignore)\s*:\s*$/);
|
|
58
|
+
if (head) {
|
|
59
|
+
key = head[1];
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (key) {
|
|
63
|
+
const m = line.match(/^\s+-\s*["']?([^"'\s]+)["']?\s*$/);
|
|
58
64
|
if (m)
|
|
59
|
-
|
|
65
|
+
lists[key].push(m[1]);
|
|
60
66
|
else if (line.trim())
|
|
61
|
-
|
|
67
|
+
key = "";
|
|
62
68
|
}
|
|
63
69
|
}
|
|
70
|
+
const ignore = lists.ignore.map((p) => p.replace(/\\/g, "/").replace(/^\.?\//, "").replace(/\/+$/, ""));
|
|
71
|
+
if (emptyFw)
|
|
72
|
+
return { frameworks: [], skipped: [], ignore };
|
|
73
|
+
const ids = lists.frameworks;
|
|
74
|
+
if (!ids.length)
|
|
75
|
+
return { frameworks: null, skipped: [], ignore };
|
|
64
76
|
const known = ids.filter((i) => ALL_FRAMEWORKS.includes(i));
|
|
65
|
-
return { frameworks: known, skipped: ids.filter((i) => !known.includes(i)) };
|
|
77
|
+
return { frameworks: known, skipped: ids.filter((i) => !known.includes(i)), ignore };
|
|
66
78
|
}
|
|
67
|
-
|
|
79
|
+
// Dependencies, build output and caches: generated code, not code you wrote.
|
|
80
|
+
export const SKIP_DIRS = ["node_modules", ".git", "dist", "build", "out", ".output", ".next", ".nuxt", ".svelte-kit", ".astro", ".vercel", ".netlify", ".wrangler", ".turbo", ".cache", "venv", ".venv", "__pycache__", "coverage"];
|
|
81
|
+
const SKIP = new Set(SKIP_DIRS);
|
|
82
|
+
const MAX_LINE = 1000; // a line this long means minified or generated code
|
|
68
83
|
const MAX_BYTES = 1_000_000;
|
|
69
84
|
/** Scout: list files to check, sorted so output order never depends on the file system. */
|
|
70
|
-
export async function listFiles(root) {
|
|
85
|
+
export async function listFiles(root, ignore = []) {
|
|
71
86
|
const out = [];
|
|
87
|
+
const ignored = (p) => { const r = relative(root, p).split(sep).join("/"); return ignore.some((i) => r === i || r.startsWith(i + "/")); };
|
|
72
88
|
async function walk(dir) {
|
|
73
89
|
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
74
|
-
if (SKIP.has(e.name))
|
|
90
|
+
if (SKIP.has(e.name) || /\.min\.(js|css)$/.test(e.name))
|
|
75
91
|
continue;
|
|
76
92
|
const p = join(dir, e.name);
|
|
93
|
+
if (ignored(p))
|
|
94
|
+
continue;
|
|
77
95
|
if (e.isDirectory())
|
|
78
96
|
await walk(p);
|
|
79
97
|
else if (e.isFile())
|
|
@@ -87,7 +105,7 @@ export async function scan(root, frameworks = ALL_FRAMEWORKS) {
|
|
|
87
105
|
const active = rules.filter((r) => r.controls.some((c) => frameworks.includes(c.framework)));
|
|
88
106
|
const findings = [];
|
|
89
107
|
let filesChecked = 0;
|
|
90
|
-
for (const abs of await listFiles(root)) {
|
|
108
|
+
for (const abs of await listFiles(root, (await readConfig(root)).ignore)) {
|
|
91
109
|
const ext = extname(abs) || (abs.endsWith(".env") ? ".env" : "");
|
|
92
110
|
const relevant = active.filter((r) => r.extensions.includes(ext));
|
|
93
111
|
if (!relevant.length)
|
|
@@ -95,9 +113,11 @@ export async function scan(root, frameworks = ALL_FRAMEWORKS) {
|
|
|
95
113
|
const buf = await readFile(abs);
|
|
96
114
|
if (buf.length > MAX_BYTES || buf.includes(0))
|
|
97
115
|
continue; // skip big or binary files
|
|
98
|
-
filesChecked++;
|
|
99
116
|
const file = relative(root, abs).split(sep).join("/");
|
|
100
117
|
const lines = buf.toString("utf8").split(/\r?\n/);
|
|
118
|
+
if (lines.some((l) => l.length > MAX_LINE))
|
|
119
|
+
continue; // minified or generated
|
|
120
|
+
filesChecked++;
|
|
101
121
|
const add = (r, line) => findings.push({
|
|
102
122
|
ruleId: r.id, file, line, status: r.status, severity: r.severity, title: r.title,
|
|
103
123
|
controls: r.controls.filter((c) => frameworks.includes(c.framework)), fix: r.fix, note: r.note,
|
package/dist/src/rules/index.js
CHANGED
|
@@ -2,6 +2,8 @@ const CODE = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py"];
|
|
|
2
2
|
// Beta: mapped by us, awaiting review by a qualified compliance professional.
|
|
3
3
|
const DRAFT = { mappedBy: "beta", reviewedAt: "pending" };
|
|
4
4
|
const LOG_CALL = /\b(console\.(log|info|warn|error|debug)|logger\.\w+|log\.(info|warn|error|debug)|logging\.\w+|print)\s*\(/;
|
|
5
|
+
// Drops the text of string literals but keeps interpolations, so "email error" is ignored and `${user.email}` or f"{user.email}" is not.
|
|
6
|
+
const codeOnly = (l) => l.replace(/(["'`])(?:\\.|(?!\1).)*\1/g, (s) => (s.match(/\{[^}]*\}/g) ?? []).join(" "));
|
|
5
7
|
const PII = /\b(e-?mail|phone|ssn|social_?security|passport|date_?of_?birth|dob|password|passwd|credit_?card|card_?number|iban)\b/i;
|
|
6
8
|
export const rules = [
|
|
7
9
|
{
|
|
@@ -16,7 +18,7 @@ export const rules = [
|
|
|
16
18
|
fix: "Mask or remove the personal field before logging, for example mask(user.email).",
|
|
17
19
|
note: "Logs are copied to many places and kept for a long time. Personal data in them is hard to find and delete.",
|
|
18
20
|
extensions: CODE,
|
|
19
|
-
test: (l) => LOG_CALL.test(l) && PII.test(l),
|
|
21
|
+
test: (l) => LOG_CALL.test(l) && PII.test(codeOnly(l)),
|
|
20
22
|
...DRAFT,
|
|
21
23
|
},
|
|
22
24
|
{
|
|
@@ -107,7 +109,7 @@ export const rules = [
|
|
|
107
109
|
fix: "Remove the override and trust a proper certificate. For self-signed certs in tests, scope it to the test only.",
|
|
108
110
|
note: "With checks off, anyone on the network path can read or change the traffic.",
|
|
109
111
|
extensions: [...CODE, ".env"],
|
|
110
|
-
test: (l) => /rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*[=:]\s*["']?0|
|
|
112
|
+
test: (l) => /rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*[=:]\s*["']?0|\b(requests|httpx|session|client|Client|AsyncClient|urlopen)\b.*\bverify\s*=\s*False|InsecureSkipVerify\s*:\s*true|CERT_NONE/.test(l),
|
|
111
113
|
...DRAFT,
|
|
112
114
|
},
|
|
113
115
|
{
|
package/package.json
CHANGED