srclawyer 0.4.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.
Files changed (77) hide show
  1. package/LICENSE +112 -0
  2. package/README.md +58 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +32 -0
  5. package/dist/cloud/config.d.ts +5 -0
  6. package/dist/cloud/config.js +23 -0
  7. package/dist/cloud/protectedLogicClient.d.ts +38 -0
  8. package/dist/cloud/protectedLogicClient.js +71 -0
  9. package/dist/cloud/resolveProtectedLogic.d.ts +14 -0
  10. package/dist/cloud/resolveProtectedLogic.js +67 -0
  11. package/dist/commands/init.d.ts +1 -0
  12. package/dist/commands/init.js +67 -0
  13. package/dist/commands/scan.d.ts +6 -0
  14. package/dist/commands/scan.js +66 -0
  15. package/dist/config/config.d.ts +18 -0
  16. package/dist/config/config.js +26 -0
  17. package/dist/engine/astUtils.d.ts +2 -0
  18. package/dist/engine/astUtils.js +22 -0
  19. package/dist/engine/discoverFiles.d.ts +7 -0
  20. package/dist/engine/discoverFiles.js +25 -0
  21. package/dist/engine/discoverProjects.d.ts +3 -0
  22. package/dist/engine/discoverProjects.js +34 -0
  23. package/dist/engine/languageDetection.d.ts +1 -0
  24. package/dist/engine/languageDetection.js +48 -0
  25. package/dist/engine/scanEngine.d.ts +11 -0
  26. package/dist/engine/scanEngine.js +130 -0
  27. package/dist/engine/secretRedaction.d.ts +2 -0
  28. package/dist/engine/secretRedaction.js +31 -0
  29. package/dist/engine/tsProject.d.ts +4 -0
  30. package/dist/engine/tsProject.js +68 -0
  31. package/dist/engine/types.d.ts +71 -0
  32. package/dist/engine/types.js +1 -0
  33. package/dist/index.d.ts +10 -0
  34. package/dist/index.js +9 -0
  35. package/dist/llm/anthropicProvider.d.ts +8 -0
  36. package/dist/llm/anthropicProvider.js +130 -0
  37. package/dist/llm/buildRequestItem.d.ts +10 -0
  38. package/dist/llm/buildRequestItem.js +17 -0
  39. package/dist/llm/resolveAmbiguousFindings.d.ts +7 -0
  40. package/dist/llm/resolveAmbiguousFindings.js +103 -0
  41. package/dist/llm/resolveProviderForTier.d.ts +3 -0
  42. package/dist/llm/resolveProviderForTier.js +31 -0
  43. package/dist/llm/types.d.ts +29 -0
  44. package/dist/llm/types.js +1 -0
  45. package/dist/policy/dataCategoryLabels.d.ts +2 -0
  46. package/dist/policy/dataCategoryLabels.js +18 -0
  47. package/dist/policy/disclaimer.d.ts +1 -0
  48. package/dist/policy/disclaimer.js +1 -0
  49. package/dist/policy/generatePolicy.d.ts +3 -0
  50. package/dist/policy/generatePolicy.js +149 -0
  51. package/dist/report/formatText.d.ts +2 -0
  52. package/dist/report/formatText.js +53 -0
  53. package/dist/rules/confidenceFactors.d.ts +14 -0
  54. package/dist/rules/confidenceFactors.js +14 -0
  55. package/dist/rules/dataCategoryLexicon.d.ts +3 -0
  56. package/dist/rules/dataCategoryLexicon.js +26 -0
  57. package/dist/rules/frameworkRules.d.ts +5 -0
  58. package/dist/rules/frameworkRules.js +98 -0
  59. package/dist/rules/htmlRules.d.ts +6 -0
  60. package/dist/rules/htmlRules.js +75 -0
  61. package/dist/rules/processorProfiles.d.ts +2 -0
  62. package/dist/rules/processorProfiles.js +42 -0
  63. package/dist/rules/safeFieldNames.d.ts +1 -0
  64. package/dist/rules/safeFieldNames.js +25 -0
  65. package/dist/rules/sdkRules.d.ts +3 -0
  66. package/dist/rules/sdkRules.js +65 -0
  67. package/dist/rules/typedRequestBodyRules.d.ts +4 -0
  68. package/dist/rules/typedRequestBodyRules.js +83 -0
  69. package/dist/rules/webApiRules.d.ts +3 -0
  70. package/dist/rules/webApiRules.js +89 -0
  71. package/dist/rules/zodPreFilter.d.ts +9 -0
  72. package/dist/rules/zodPreFilter.js +91 -0
  73. package/dist/schemaParsers/graphql.d.ts +2 -0
  74. package/dist/schemaParsers/graphql.js +52 -0
  75. package/dist/schemaParsers/openapi.d.ts +2 -0
  76. package/dist/schemaParsers/openapi.js +60 -0
  77. package/package.json +51 -0
package/LICENSE ADDED
@@ -0,0 +1,112 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: Mystique Technologies LLC
6
+ Licensed Work: SrcLawyer
7
+ The Licensed Work is © 2026 Mystique Technologies LLC
8
+ Additional Use Grant: You may use, copy, modify, and self-host the
9
+ Licensed Work for any purpose, including internal
10
+ use within a commercial organization of any size,
11
+ and including use as part of your own software
12
+ development and compliance processes. You may not
13
+ offer the Licensed Work, or any modified or
14
+ derivative version of it, to third parties as a
15
+ hosted or managed service, or embed it in a
16
+ competing commercial product or service, without
17
+ a separate commercial license from Licensor.
18
+ Change Date: Four years from the date of the first publicly
19
+ available distribution of a specific version of
20
+ the Licensed Work.
21
+ Change License: Apache License, Version 2.0
22
+
23
+ For information about alternative licensing arrangements for the
24
+ Licensed Work, please contact contact@mystique-technologies.com.
25
+
26
+ Notice
27
+
28
+ The Business Source License (this document, or the "License") is not
29
+ an Open Source license. However, the Licensed Work will eventually be
30
+ made available under an Open Source License, as stated in this
31
+ License.
32
+
33
+ License text copyright © 2024 MariaDB plc, All Rights Reserved.
34
+ "Business Source License" is a trademark of MariaDB plc.
35
+
36
+ Terms
37
+
38
+ The Licensor hereby grants you the right to copy, modify, create
39
+ derivative works, redistribute, and make non-production use of the
40
+ Licensed Work. The Licensor may make an Additional Use Grant, above,
41
+ permitting limited production use.
42
+
43
+ Effective on the Change Date, or the fourth anniversary of the first
44
+ publicly available distribution of a specific version of the Licensed
45
+ Work under this License, whichever comes first, the Licensor hereby
46
+ grants you rights under the terms of the Change License, and the
47
+ rights granted in the paragraph above terminate.
48
+
49
+ If your use of the Licensed Work does not comply with the requirements
50
+ currently in effect as described in this License, you must purchase a
51
+ commercial license from the Licensor, its affiliated entities, or
52
+ authorized resellers, or you must refrain from using the Licensed
53
+ Work.
54
+
55
+ All copies of the original and modified Licensed Work, and derivative
56
+ works of the Licensed Work, are subject to this License. This License
57
+ applies separately for each version of the Licensed Work and the
58
+ Change Date may vary for each version of the Licensed Work released by
59
+ Licensor.
60
+
61
+ You must conspicuously display this License on each original or
62
+ modified copy of the Licensed Work. If you receive the Licensed Work
63
+ in original or modified form from a third party, the terms and
64
+ conditions set forth in this License apply to your use of that work.
65
+
66
+ Any use of the Licensed Work in violation of this License will
67
+ automatically terminate your rights under this License for the
68
+ current and all other versions of the Licensed Work.
69
+
70
+ This License does not grant you any right in any trademark or logo of
71
+ Licensor or its affiliates (provided that you may use a trademark or
72
+ logo of Licensor as expressly required by this License). TO THE EXTENT
73
+ PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS
74
+ IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
75
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
76
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT,
77
+ AND TITLE.
78
+
79
+ MariaDB hereby grants you permission to use this License's text to
80
+ license your works, and to refer to it using the trademark "Business
81
+ Source License", as long as you comply with the Covenants of Licensor
82
+ below.
83
+
84
+ Covenants of Licensor
85
+
86
+ In consideration of the right to use this License's text and the
87
+ "Business Source License" name and trademark, Licensor covenants to
88
+ MariaDB, and to all other recipients of the licensed work to be
89
+ provided by Licensor:
90
+
91
+ 1. To specify as the Change License the GPL Version 2.0 or any later
92
+ version, or a license that is compatible with GPL Version 2.0 or a
93
+ later version, where "compatible" means that software provided
94
+ under the Change License can be included in a program with software
95
+ provided under GPL Version 2.0 or a later version. Licensor may
96
+ specify additional Change Licenses without limitation.
97
+
98
+ 2. To either: (a) specify an additional grant of rights to use that
99
+ does not impose any additional restriction on the right granted in
100
+ this License, as the Additional Use Grant; or (b) insert the text
101
+ "None" to specify a Change Date.
102
+
103
+ 3. To specify a Change Date.
104
+
105
+ 4. Not to modify this License in any other way.
106
+
107
+ Notice
108
+
109
+ The Business Source License (this document, or the "License") is not
110
+ an Open Source license. However, the Licensed Work will eventually be
111
+ made available under an Open Source License, as stated in this
112
+ License.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # SrcLawyer
2
+
3
+ Static analysis engine that derives privacy policy documentation directly from source code, and keeps it in sync as the code changes.
4
+
5
+ > This is a Phase 1 scaffold: the CLI (`init`, `scan`), the Layer 1 static analysis engine — SDK/framework pattern detection, HTML/browser-API detection, and OpenAPI/GraphQL schema parsing — LLM-assisted reasoning for ambiguous findings (Layer 2, bring-your-own-key or managed), and markdown privacy policy generation. CI/CD integration is not yet implemented.
6
+ >
7
+ > Scanning is manual-only (`srclawyer scan`, run on demand) — this is the free/solo tier's permanent behavior, not a placeholder. CI/CD integration (PR-triggered scans, a weekly backstop scan) is scoped for the paid/team tier only; see `PRODUCT_SPEC.md` Section 8.5.
8
+
9
+ ## Usage
10
+
11
+ ```bash
12
+ npm install
13
+ npm run build
14
+ node dist/cli.js init # one-time: jurisdiction, markets, industry
15
+ node dist/cli.js scan # analyze the codebase and write PRIVACY_POLICY.md
16
+ node dist/cli.js scan --json # findings as JSON (still writes the policy file)
17
+ node dist/cli.js scan --out docs/privacy.md
18
+ ```
19
+
20
+ During development, run directly from source with `npm run dev -- scan`.
21
+
22
+ ## What `scan` does today
23
+
24
+ - **Known SDK integrations** (import/require detection): Stripe, SendGrid, Twilio, Google Analytics, Mixpanel, Auth0, Firebase, Cognito — each mapped to the data categories it typically processes (`src/rules/processorProfiles.ts`).
25
+ - **Request-body field collection** in Express (`req.body.x`, destructured `req.body`) and Next.js (both `pages/api` and the app router's `await request.json()`), classified against a field-name lexicon (`src/rules/dataCategoryLexicon.ts`).
26
+ - **OpenAPI/Swagger schemas** (`components.schemas.*.properties`) and **GraphQL SDL** type/input fields.
27
+ - **HTML forms and inline scripts** (`src/rules/htmlRules.ts`): native `<input>` elements classified by `type` (password/email/tel are unambiguous native signals) or by `id`/`name` against the lexicon; inline `<script>` blocks are extracted and run through the same SDK/framework/web-API rules as any other file.
28
+ - **Browser platform APIs** (`src/rules/webApiRules.ts`): `getUserMedia` (split into microphone and camera access), geolocation, and `localStorage`/`sessionStorage` writes classified by key name.
29
+ - Fields that can't be confidently classified (generic names like `data`, `payload`) are surfaced with `requiresReview: true` rather than silently guessed.
30
+ - **A field-name allowlist** (`src/rules/safeFieldNames.ts`) suppresses findings entirely for exact-match structural fields (`id`, `createdAt`, `status`, pagination params, etc.) so "needs review" stays a signal, not noise from routine boilerplate. Anything not on the allowlist keeps the existing classify-or-flag behavior above.
31
+ - **Unsupported language/framework detection** (`src/engine/languageDetection.ts`) runs at the start of every scan: it checks for Django (`manage.py`), Rails (`Gemfile` + `app/controllers`), gRPC (`.proto` files), and falls back to a file-count ratio for Python/Ruby/Go/Java/PHP/C# vs JS/TS/HTML. When it fires, a bordered warning appears at the top of the CLI output *and* at the top of the generated `PRIVACY_POLICY.md` (not just a log line easy to miss), the warning is included in `--json` output, and the process exits with code 1 so CI can't silently treat a scan of an unsupported codebase as a pass.
32
+ - **Generates a markdown privacy policy** (`src/policy/generatePolicy.ts`) in the same command: findings are grouped by data category, each clause cites the file:line that justified it, third-party processors get their own section, and anything flagged `requiresReview` is excluded from the body and listed separately under "Items Requiring Manual Review" instead of being guessed. The mandatory disclaimer (`src/policy/disclaimer.ts`) is always appended, unconditionally.
33
+
34
+ Every finding traces back to a file and line (`src/engine/types.ts#Finding`).
35
+
36
+ ## Network calls
37
+
38
+ Detection itself is fully local and offline — no code leaves your machine for pattern matching. Once detection completes, `scan` makes one batched network call to grade the confidence of what it found (field names and derived yes/no signals only, never source code), and, for scans that use Zod schemas as request validators, one additional call sending a small redacted code fragment. This runs on every scan regardless of tier and is unrelated to the separate, optional LLM-assisted (Layer 2) reasoning described above. See `PRODUCT_SPEC.md`'s "Static Analysis Depth" section for the precise wording.
39
+
40
+ ## Secret handling
41
+
42
+ `src/engine/secretRedaction.ts` strips known secret-shaped values (API keys, connection strings, JWTs, private key blocks) out of any evidence string before it's stored or printed, and out of any code fragment before it's sent as part of the confidence-grading call above — enforced at the base layer, independent of whether the optional LLM-assisted (Layer 2) path is configured.
43
+
44
+ ## Test fixtures
45
+
46
+ `test-fixtures/` contains a small Express+Stripe app, a Next.js app (pages + app router), an OpenAPI spec, and a GraphQL schema, used to validate detection end-to-end. Run `node dist/cli.js scan` from inside `test-fixtures/` to see it work.
47
+
48
+ ## Accuracy corpus
49
+
50
+ `npm test` never touches the network. A separate, real-world accuracy check lives in `test-fixtures/golden-corpus/` — see its own README for what it measures and why it's not part of the default test run.
51
+
52
+ ## License
53
+
54
+ [Business Source License 1.1](LICENSE) (BUSL-1.1) — source-available, not OSI-approved open source.
55
+
56
+ Free for any use, including internal use within a commercial organization of any size and as part of your own software development and compliance processes. The only restriction: you may not offer SrcLawyer, or a modified/derivative version of it, to third parties as a hosted or managed service, or embed it in a competing commercial product, without a separate commercial license from Mystique Technologies LLC (contact@mystique-technologies.com).
57
+
58
+ Each version converts to the Apache License 2.0 four years after its first public release.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { Command } from "commander";
6
+ import { runInit } from "./commands/init.js";
7
+ import { runScan } from "./commands/scan.js";
8
+ // Read from package.json rather than hardcoding, so --version can't silently drift out of sync with
9
+ // the published version the way a literal string did through every prior release.
10
+ const HERE = dirname(fileURLToPath(import.meta.url));
11
+ const { version } = JSON.parse(readFileSync(join(HERE, "../package.json"), "utf8"));
12
+ const program = new Command();
13
+ program
14
+ .name("srclawyer")
15
+ .description("Static analysis engine that derives privacy policy documentation directly from source code")
16
+ .version(version);
17
+ program
18
+ .command("init")
19
+ .description("One-time setup: jurisdiction, markets, industry")
20
+ .action(async () => {
21
+ await runInit(process.cwd());
22
+ });
23
+ program
24
+ .command("scan")
25
+ .description("Scan the codebase for data-collection patterns and generate a privacy policy")
26
+ .option("--json", "output findings as JSON", false)
27
+ .option("--out <path>", "path to write the generated privacy policy", "PRIVACY_POLICY.md")
28
+ .option("--no-llm", "skip AI-assisted resolution of ambiguous findings for this run")
29
+ .action(async (opts) => {
30
+ await runScan(process.cwd(), { json: opts.json, out: opts.out, llm: opts.llm });
31
+ });
32
+ program.parseAsync(process.argv);
@@ -0,0 +1,5 @@
1
+ export interface ProtectedLogicEndpoint {
2
+ baseUrl: string;
3
+ apiKey: string;
4
+ }
5
+ export declare function resolveProtectedLogicEndpoint(): ProtectedLogicEndpoint;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Protected-logic service endpoint + auth. Not tier-gated (unlike src/llm/*) — this is Layer 1
3
+ * infrastructure now, needed regardless of free/byok/managed, so it isn't part of SrcLawyerConfig's
4
+ * tier/llm fields. `SRCLAWYER_PROTECTED_LOGIC_URL` is meant for local development against a
5
+ * `wrangler dev` instance; real deployments should never need to override it.
6
+ *
7
+ * DEFAULT_BASE_URL is the real deployed Worker (srclawyer-rules, tag v0.2.1). DEFAULT_API_KEY is
8
+ * the real shared free-tier key, matching the Worker's own API_KEY secret (set via `wrangler secret
9
+ * put API_KEY` on the Worker directly, never as a repo/CI secret). This is a deliberate choice, not
10
+ * an oversight: it's a low-privilege, purpose-built credential that only grants rate-limited access
11
+ * to the two protected-logic endpoints — categorically different from a GitHub PAT or a Cloudflare
12
+ * API token, which must never appear in a public repo. Baking it in here is what gives free-tier
13
+ * users zero-setup access with no account/key management of their own. Verified end-to-end against
14
+ * the live Worker (not just wrangler dev) before this was committed.
15
+ */
16
+ const DEFAULT_BASE_URL = "https://srclawyer-rules.anishjha352.workers.dev";
17
+ const DEFAULT_API_KEY = "dae3d13ec14ab78eeb5bae1398e0bee8fba55f218861860c81aa279c03c64888";
18
+ export function resolveProtectedLogicEndpoint() {
19
+ return {
20
+ baseUrl: process.env.SRCLAWYER_PROTECTED_LOGIC_URL ?? DEFAULT_BASE_URL,
21
+ apiKey: process.env.SRCLAWYER_PROTECTED_LOGIC_API_KEY ?? DEFAULT_API_KEY,
22
+ };
23
+ }
@@ -0,0 +1,38 @@
1
+ import type { Confidence, DataCategory } from "../engine/types.js";
2
+ export interface ProtectedLogicClientOptions {
3
+ baseUrl: string;
4
+ apiKey: string;
5
+ batchSize?: number;
6
+ timeoutMs?: number;
7
+ }
8
+ export interface ScoreFieldsItem {
9
+ id: string;
10
+ lexiconCategory: DataCategory | null;
11
+ ambiguousName: boolean;
12
+ runtimeVerified: boolean;
13
+ siblingConfidentCount: number;
14
+ }
15
+ export interface ScoreFieldsResult {
16
+ id: string;
17
+ confidence: Confidence;
18
+ requiresReview: boolean;
19
+ }
20
+ export interface DetectZodSchemaItem {
21
+ id: string;
22
+ codeFragment: string;
23
+ }
24
+ export interface DetectZodSchemaResult {
25
+ id: string;
26
+ matched: boolean;
27
+ fields: Array<{
28
+ name: string;
29
+ line: number;
30
+ }>;
31
+ }
32
+ export interface ProtectedLogicOutcome<T> {
33
+ results: Map<string, T>;
34
+ /** Non-null if any batch failed — callers should surface this via ScanResult.protectedLogicWarning. */
35
+ error: string | null;
36
+ }
37
+ export declare function scoreFields(items: ScoreFieldsItem[], options: ProtectedLogicClientOptions): Promise<ProtectedLogicOutcome<ScoreFieldsResult>>;
38
+ export declare function detectZodSchemas(items: DetectZodSchemaItem[], options: ProtectedLogicClientOptions): Promise<ProtectedLogicOutcome<DetectZodSchemaResult>>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Client for the two protected-logic endpoints (see PRODUCT_SPEC.md for the trust-claim language
3
+ * these calls are bound by). Mirrors resolveAmbiguousFindings.ts's established pattern deliberately:
4
+ * batch, try/catch per batch, never throw out of this module — a failure here degrades the scan
5
+ * (safe local fallback + a scan-level warning), it never crashes it.
6
+ */
7
+ const DEFAULT_BATCH_SIZE = 25;
8
+ const DEFAULT_TIMEOUT_MS = 10_000;
9
+ function chunk(items, size) {
10
+ const batches = [];
11
+ for (let i = 0; i < items.length; i += size)
12
+ batches.push(items.slice(i, i + size));
13
+ return batches;
14
+ }
15
+ async function postJson(url, apiKey, body, timeoutMs) {
16
+ const controller = new AbortController();
17
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
18
+ try {
19
+ const response = await fetch(url, {
20
+ method: "POST",
21
+ headers: { "content-type": "application/json", "x-api-key": apiKey },
22
+ body: JSON.stringify(body),
23
+ signal: controller.signal,
24
+ });
25
+ if (!response.ok) {
26
+ throw new Error(`protected-logic request failed (${response.status})`);
27
+ }
28
+ return (await response.json());
29
+ }
30
+ finally {
31
+ clearTimeout(timeout);
32
+ }
33
+ }
34
+ export async function scoreFields(items, options) {
35
+ const results = new Map();
36
+ if (items.length === 0)
37
+ return { results, error: null };
38
+ const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
39
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
40
+ let error = null;
41
+ for (const batch of chunk(items, batchSize)) {
42
+ try {
43
+ const response = await postJson(`${options.baseUrl}/v1/score-fields`, options.apiKey, { fields: batch }, timeoutMs);
44
+ for (const result of response.results)
45
+ results.set(result.id, result);
46
+ }
47
+ catch (err) {
48
+ error = err instanceof Error ? err.message : String(err);
49
+ }
50
+ }
51
+ return { results, error };
52
+ }
53
+ export async function detectZodSchemas(items, options) {
54
+ const results = new Map();
55
+ if (items.length === 0)
56
+ return { results, error: null };
57
+ const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
58
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
59
+ let error = null;
60
+ for (const batch of chunk(items, batchSize)) {
61
+ try {
62
+ const response = await postJson(`${options.baseUrl}/v1/detect-zod-schema`, options.apiKey, { candidates: batch }, timeoutMs);
63
+ for (const result of response.results)
64
+ results.set(result.id, result);
65
+ }
66
+ catch (err) {
67
+ error = err instanceof Error ? err.message : String(err);
68
+ }
69
+ }
70
+ return { results, error };
71
+ }
@@ -0,0 +1,14 @@
1
+ import type { Finding } from "../engine/types.js";
2
+ import type { ZodCandidate } from "../rules/zodPreFilter.js";
3
+ import { type ProtectedLogicClientOptions } from "./protectedLogicClient.js";
4
+ export interface ResolveProtectedLogicResult {
5
+ findings: Finding[];
6
+ warning: string | null;
7
+ }
8
+ /**
9
+ * The one place that turns "findings with provisional confidence" and "files that might have a Zod
10
+ * schema" into final results, by calling the protected-logic service. Layered on top of scan() the
11
+ * same way resolveAmbiguousFindings.ts is layered on top of it for Layer 2 — not called from inside
12
+ * scan() itself, so scan()'s own purity/auditability claim stays true.
13
+ */
14
+ export declare function resolveProtectedLogic(findings: Finding[], pendingZodCandidates: ZodCandidate[], options: ProtectedLogicClientOptions): Promise<ResolveProtectedLogicResult>;
@@ -0,0 +1,67 @@
1
+ import { classifyFieldName, isAmbiguousFieldName } from "../rules/dataCategoryLexicon.js";
2
+ import { countConfidentSiblings } from "../rules/confidenceFactors.js";
3
+ import { detectZodSchemas, scoreFields } from "./protectedLogicClient.js";
4
+ /**
5
+ * The one place that turns "findings with provisional confidence" and "files that might have a Zod
6
+ * schema" into final results, by calling the protected-logic service. Layered on top of scan() the
7
+ * same way resolveAmbiguousFindings.ts is layered on top of it for Layer 2 — not called from inside
8
+ * scan() itself, so scan()'s own purity/auditability claim stays true.
9
+ */
10
+ export async function resolveProtectedLogic(findings, pendingZodCandidates, options) {
11
+ let workingFindings = findings;
12
+ const warnings = [];
13
+ if (pendingZodCandidates.length > 0) {
14
+ const byId = new Map(pendingZodCandidates.map((c) => [c.id, c]));
15
+ const { results, error } = await detectZodSchemas(pendingZodCandidates.map((c) => ({ id: c.id, codeFragment: c.codeFragment })), options);
16
+ if (error)
17
+ warnings.push(`Zod-schema detection unavailable (${error}) — schemas in this scan were not checked.`);
18
+ const zodFindings = [];
19
+ for (const [id, result] of results) {
20
+ const candidate = byId.get(id);
21
+ if (!candidate || !result.matched || result.fields.length === 0)
22
+ continue;
23
+ const allNames = result.fields.map((f) => f.name);
24
+ for (const field of result.fields) {
25
+ const sourceLine = candidate.fragmentLineToSourceLine[field.line - 1] ?? field.line;
26
+ const lexiconCategory = classifyFieldName(field.name);
27
+ const ambiguousName = isAmbiguousFieldName(field.name);
28
+ const siblingConfidentCount = countConfidentSiblings(allNames, field.name);
29
+ zodFindings.push({
30
+ id: `zod:${id}:${sourceLine}:${field.name}`,
31
+ dataCategories: lexiconCategory ? [lexiconCategory] : ["generic_pii"],
32
+ processor: null,
33
+ description: lexiconCategory
34
+ ? `Zod schema field "${field.name}" collected, classified as ${lexiconCategory}.`
35
+ : `Zod schema field "${field.name}" collected; could not confidently classify — needs review.`,
36
+ confidence: "low",
37
+ source: "zod-rule",
38
+ location: { file: id, line: sourceLine, column: 0 },
39
+ evidence: "", // the raw fragment already went through redactSecrets(); no further evidence is retained here
40
+ requiresReview: true,
41
+ confidenceFactors: { lexiconCategory, ambiguousName, runtimeVerified: true, siblingConfidentCount },
42
+ });
43
+ }
44
+ }
45
+ workingFindings = [...workingFindings, ...zodFindings];
46
+ }
47
+ const toScore = workingFindings.filter((f) => !!f.confidenceFactors);
48
+ if (toScore.length > 0) {
49
+ const items = toScore.map((f) => ({
50
+ id: f.id,
51
+ lexiconCategory: (f.confidenceFactors.lexiconCategory ?? null),
52
+ ambiguousName: Boolean(f.confidenceFactors.ambiguousName),
53
+ runtimeVerified: Boolean(f.confidenceFactors.runtimeVerified),
54
+ siblingConfidentCount: Number(f.confidenceFactors.siblingConfidentCount ?? 0),
55
+ }));
56
+ const { results, error } = await scoreFields(items, options);
57
+ if (error)
58
+ warnings.push(`Confidence scoring unavailable (${error}) — affected findings kept their safe local default (low confidence, flagged for review).`);
59
+ workingFindings = workingFindings.map((f) => {
60
+ const scored = results.get(f.id);
61
+ if (!scored)
62
+ return f;
63
+ return { ...f, confidence: scored.confidence, requiresReview: scored.requiresReview };
64
+ });
65
+ }
66
+ return { findings: workingFindings, warning: warnings.length > 0 ? warnings.join(" ") : null };
67
+ }
@@ -0,0 +1 @@
1
+ export declare function runInit(root: string): Promise<void>;
@@ -0,0 +1,67 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { CONFIG_FILENAME, DEFAULT_CONFIG, configPath, writeConfig } from "../config/config.js";
5
+ function guessIndustry(root) {
6
+ const pkgPath = join(root, "package.json");
7
+ if (!existsSync(pkgPath))
8
+ return null;
9
+ try {
10
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
11
+ if (typeof pkg.description === "string" && pkg.description.length > 0)
12
+ return pkg.description;
13
+ return null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ async function ask(lines, question, fallback) {
20
+ const suffix = fallback ? ` [${fallback}]` : "";
21
+ process.stdout.write(`${question}${suffix}: `);
22
+ const { value, done } = await lines.next();
23
+ const answer = done ? "" : value.trim();
24
+ return answer.length > 0 ? answer : fallback;
25
+ }
26
+ export async function runInit(root) {
27
+ const existingPath = configPath(root);
28
+ if (existsSync(existingPath)) {
29
+ console.log(`${CONFIG_FILENAME} already exists. Delete it first if you want to redo setup.`);
30
+ return;
31
+ }
32
+ const guessedIndustry = guessIndustry(root);
33
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
34
+ const lines = rl[Symbol.asyncIterator]();
35
+ console.log("SrcLawyer setup — answered once, reused on every scan.\n");
36
+ const entityLocation = await ask(lines, "Where is your legal entity located? (country)", DEFAULT_CONFIG.entityLocation ?? "");
37
+ const targetMarketsRaw = await ask(lines, "Which regions/markets do you serve? (comma-separated, e.g. US, EU)", "");
38
+ const industry = await ask(lines, "What industry/vertical is this product in?", guessedIndustry ?? "");
39
+ const childrensDataRaw = await ask(lines, "Does this product knowingly collect data from children under 13/16?", "no");
40
+ const tierRaw = await ask(lines, "Tier: free (static analysis only) / byok (bring your own Anthropic key) / managed (not yet available)", "free");
41
+ const tier = normalizeTier(tierRaw);
42
+ let apiKeyEnvVar = null;
43
+ if (tier === "byok") {
44
+ apiKeyEnvVar = await ask(lines, "Which environment variable holds your Anthropic API key?", "ANTHROPIC_API_KEY");
45
+ console.log(`\nNote: only the name "${apiKeyEnvVar}" is written to ${CONFIG_FILENAME} — the key itself is never written to disk. It's read from your environment at scan time.`);
46
+ }
47
+ else if (tier === "managed") {
48
+ console.log("\nHeads up: the managed tier's hosted backend isn't available yet. Layer 1 (static analysis) will still run fine; AI-assisted resolution will report a clear error until it ships.");
49
+ }
50
+ rl.close();
51
+ const config = {
52
+ entityLocation: entityLocation || null,
53
+ targetMarkets: targetMarketsRaw.split(",").map((s) => s.trim()).filter(Boolean),
54
+ industry: industry || null,
55
+ collectsChildrensData: /^y(es)?$/i.test(childrensDataRaw.trim()),
56
+ tier,
57
+ ...(tier === "byok" ? { llm: { provider: "anthropic", apiKeyEnvVar } } : {}),
58
+ };
59
+ writeConfig(root, config);
60
+ console.log(`\nWrote ${CONFIG_FILENAME}. Run "srclawyer scan" to analyze this codebase.`);
61
+ }
62
+ function normalizeTier(raw) {
63
+ const normalized = raw.trim().toLowerCase();
64
+ if (normalized === "byok" || normalized === "managed")
65
+ return normalized;
66
+ return "free";
67
+ }
@@ -0,0 +1,6 @@
1
+ export interface ScanOptions {
2
+ json: boolean;
3
+ out: string;
4
+ llm: boolean;
5
+ }
6
+ export declare function runScan(root: string, options: ScanOptions): Promise<void>;
@@ -0,0 +1,66 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { loadConfig } from "../config/config.js";
4
+ import { scan } from "../engine/scanEngine.js";
5
+ import { formatText } from "../report/formatText.js";
6
+ import { generatePolicyMarkdown } from "../policy/generatePolicy.js";
7
+ import { resolveProviderForTier } from "../llm/resolveProviderForTier.js";
8
+ import { resolveAmbiguousFindings } from "../llm/resolveAmbiguousFindings.js";
9
+ import { resolveProtectedLogic } from "../cloud/resolveProtectedLogic.js";
10
+ import { resolveProtectedLogicEndpoint } from "../cloud/config.js";
11
+ export async function runScan(root, options) {
12
+ const config = loadConfig(root);
13
+ if (!config) {
14
+ console.error('No .privacypolicy.yml found. Run "srclawyer init" first.\n');
15
+ }
16
+ const result = await scan(root);
17
+ let findings = result.findings;
18
+ // Not tier-gated, not behind --no-llm: confidence scoring and Zod-schema detection are Layer 1
19
+ // capabilities now, needed on every real scan regardless of which LLM tier (if any) is configured.
20
+ // A failure here degrades honestly (findings keep their safe local fallback) rather than blocking
21
+ // the scan — see resolveProtectedLogic's own per-call error handling.
22
+ const hasScorableFindings = findings.some((f) => f.confidenceFactors) || result.pendingZodCandidates.length > 0;
23
+ let protectedLogicWarning = null;
24
+ if (hasScorableFindings) {
25
+ const endpoint = resolveProtectedLogicEndpoint();
26
+ const resolved = await resolveProtectedLogic(findings, result.pendingZodCandidates, endpoint);
27
+ findings = resolved.findings;
28
+ protectedLogicWarning = resolved.warning;
29
+ if (protectedLogicWarning)
30
+ console.error(protectedLogicWarning);
31
+ }
32
+ let llmError = null;
33
+ if (options.llm && config) {
34
+ const { provider, error } = resolveProviderForTier(config);
35
+ if (error) {
36
+ llmError = error;
37
+ console.error(error);
38
+ }
39
+ else if (provider) {
40
+ const ambiguous = findings.filter((f) => f.requiresReview);
41
+ if (ambiguous.length > 0) {
42
+ console.log(`Sending ${ambiguous.length} ambiguous item(s) to ${provider.name}...`);
43
+ const resolved = await resolveAmbiguousFindings(ambiguous, provider, {
44
+ collectsChildrensData: config.collectsChildrensData,
45
+ });
46
+ const resolvedById = new Map(resolved.map((f) => [f.id, f]));
47
+ findings = findings.map((f) => resolvedById.get(f.id) ?? f);
48
+ }
49
+ }
50
+ }
51
+ const ambiguousCount = findings.filter((f) => f.requiresReview).length;
52
+ const finalResult = { ...result, findings, ambiguousCount };
53
+ const policyPath = resolve(root, options.out);
54
+ writeFileSync(policyPath, generatePolicyMarkdown(finalResult, config, llmError));
55
+ if (result.unsupportedStackWarning || llmError || protectedLogicWarning) {
56
+ process.exitCode = 1;
57
+ }
58
+ if (options.json) {
59
+ console.log(JSON.stringify({ ...finalResult, policyPath, llmError, protectedLogicWarning }, null, 2));
60
+ return;
61
+ }
62
+ console.log(formatText(finalResult));
63
+ if (protectedLogicWarning)
64
+ console.log(`\n${protectedLogicWarning}`);
65
+ console.log(`\nPrivacy policy written to ${join(options.out)}`);
66
+ }
@@ -0,0 +1,18 @@
1
+ export declare const CONFIG_FILENAME = ".privacypolicy.yml";
2
+ export type SrcLawyerTier = "free" | "byok" | "managed";
3
+ export interface SrcLawyerLlmConfig {
4
+ provider: "anthropic";
5
+ apiKeyEnvVar: string | null;
6
+ }
7
+ export interface SrcLawyerConfig {
8
+ entityLocation: string | null;
9
+ targetMarkets: string[];
10
+ industry: string | null;
11
+ collectsChildrensData: boolean;
12
+ tier?: SrcLawyerTier;
13
+ llm?: SrcLawyerLlmConfig;
14
+ }
15
+ export declare const DEFAULT_CONFIG: SrcLawyerConfig;
16
+ export declare function configPath(root: string): string;
17
+ export declare function loadConfig(root: string): SrcLawyerConfig | null;
18
+ export declare function writeConfig(root: string, config: SrcLawyerConfig): void;
@@ -0,0 +1,26 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import yaml from "js-yaml";
4
+ export const CONFIG_FILENAME = ".privacypolicy.yml";
5
+ export const DEFAULT_CONFIG = {
6
+ entityLocation: null,
7
+ targetMarkets: [],
8
+ industry: null,
9
+ collectsChildrensData: false,
10
+ tier: "free",
11
+ };
12
+ export function configPath(root) {
13
+ return join(root, CONFIG_FILENAME);
14
+ }
15
+ export function loadConfig(root) {
16
+ const path = configPath(root);
17
+ if (!existsSync(path))
18
+ return null;
19
+ const raw = readFileSync(path, "utf8");
20
+ return { ...DEFAULT_CONFIG, ...yaml.load(raw) };
21
+ }
22
+ export function writeConfig(root, config) {
23
+ const path = configPath(root);
24
+ const header = "# Generated by srclawyer init. Safe to edit by hand.\n";
25
+ writeFileSync(path, header + yaml.dump(config), "utf8");
26
+ }
@@ -0,0 +1,2 @@
1
+ import type { File } from "@babel/types";
2
+ export declare function parseSource(source: string, filePath: string): File | null;
@@ -0,0 +1,22 @@
1
+ import { parse } from "@babel/parser";
2
+ import { extname } from "node:path";
3
+ export function parseSource(source, filePath) {
4
+ const ext = extname(filePath);
5
+ const plugins = ["jsx"];
6
+ if (ext === ".ts" || ext === ".tsx") {
7
+ plugins.length = 0;
8
+ plugins.push("typescript");
9
+ if (ext === ".tsx")
10
+ plugins.push("jsx");
11
+ }
12
+ try {
13
+ return parse(source, {
14
+ sourceType: "unambiguous",
15
+ plugins,
16
+ errorRecovery: true,
17
+ });
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }