itworksbut 0.3.0 → 0.5.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.
@@ -0,0 +1,115 @@
1
+ import { lineFromOffset, parseJsonWithComments } from "../helpers.js";
2
+
3
+ const CONFIG_FILES = ["src-tauri/tauri.conf.json", "src-tauri/tauri.conf.json5", "src-tauri/Tauri.toml"];
4
+ const WEAK_CSP_RE =
5
+ /"csp"\s*:\s*(?:null|false|""|"[^"]*(?:\*|unsafe-inline|unsafe-eval)[^"]*")|csp\s*=\s*(?:false|""|"[^"]*(?:\*|unsafe-inline|unsafe-eval)[^"]*")/gi;
6
+ const DANGEROUS_ASSET_CSP_RE = /"dangerousDisableAssetCspModification"\s*:\s*true|dangerousDisableAssetCspModification\s*=\s*true/gi;
7
+ const BROAD_ALLOWLIST_RE = /"allowlist"\s*:\s*{[\s\S]{0,400}?"all"\s*:\s*true|"all"\s*:\s*true[\s\S]{0,120}?"(?:shell|fs|http)"/gi;
8
+ const BROAD_PERMISSION_RE =
9
+ /"permissions"\s*:\s*\[[\s\S]{0,240}?["'`]\*["'`]|["'`](?:shell|fs|http):(?:\*|allow-\*|allow-all|allow-execute|allow-fetch)["'`]|["'`]fs:allow-[^"'`]*["'`][\s\S]{0,160}(?:\*\*|["'`]\*["'`]|\/)|["'`]http:allow-fetch["'`][\s\S]{0,220}(?:["'`]\*["'`]|https?:\/\/\*)/gi;
10
+ const REMOTE_URL_RE = /"(?:devUrl|frontendDist|url)"\s*:\s*"https?:\/\/(?!localhost\b|127\.0\.0\.1\b|0\.0\.0\.0\b)[^"]+"|(?:devUrl|frontendDist|url)\s*=\s*"https?:\/\/(?!localhost\b|127\.0\.0\.1\b|0\.0\.0\.0\b)[^"]+"/gi;
11
+
12
+ export default {
13
+ id: "tauri.remote-url-permissions-risk",
14
+ title: "Tauri remote URLs and permissions should be least privilege",
15
+ category: "tauri",
16
+ severity: "high",
17
+ tags: ["tauri", "desktop", "permissions", "heuristic"],
18
+ run: async (context) => {
19
+ if (!isTauriProject(context)) return [];
20
+ const findings = [];
21
+
22
+ for (const file of collectTauriFiles(context)) {
23
+ const content = await context.readFileSafe(file);
24
+ if (!content) continue;
25
+
26
+ if (file.endsWith(".json") || file.endsWith(".json5")) {
27
+ inspectParsedJson(content, file, findings);
28
+ }
29
+
30
+ inspectRegexPattern(content, file, findings, WEAK_CSP_RE, "weak-csp");
31
+ inspectRegexPattern(content, file, findings, DANGEROUS_ASSET_CSP_RE, "dangerous-asset-csp");
32
+ inspectRegexPattern(content, file, findings, BROAD_ALLOWLIST_RE, "broad-allowlist");
33
+ inspectRegexPattern(content, file, findings, BROAD_PERMISSION_RE, "broad-permission");
34
+ inspectRegexPattern(content, file, findings, REMOTE_URL_RE, "remote-url");
35
+ }
36
+
37
+ return dedupe(findings).slice(0, 100);
38
+ }
39
+ };
40
+
41
+ function isTauriProject(context) {
42
+ return (
43
+ context.allFiles.some((file) => file.startsWith("src-tauri/")) ||
44
+ context.hasDependency("@tauri-apps/api") ||
45
+ context.hasDevDependency("@tauri-apps/cli") ||
46
+ context.hasDependency("@tauri-apps/cli")
47
+ );
48
+ }
49
+
50
+ function collectTauriFiles(context) {
51
+ return context.textFiles.filter((file) => {
52
+ return (
53
+ CONFIG_FILES.includes(file) ||
54
+ /^src-tauri\/capabilities\/[^/]+\.json$/i.test(file) ||
55
+ /^src-tauri\/permissions\/[^/]+\.json$/i.test(file)
56
+ );
57
+ });
58
+ }
59
+
60
+ function inspectParsedJson(content, file, findings) {
61
+ let parsed;
62
+ try {
63
+ parsed = parseJsonWithComments(content);
64
+ } catch {
65
+ return;
66
+ }
67
+
68
+ const security = parsed?.app?.security || parsed?.tauri?.security || {};
69
+ if (security.csp === null || security.csp === false || security.csp === "") {
70
+ findings.push(finding(file, lineOfKey(content, "csp"), "weak-csp"));
71
+ }
72
+ if (security.dangerousDisableAssetCspModification === true) {
73
+ findings.push(finding(file, lineOfKey(content, "dangerousDisableAssetCspModification"), "dangerous-asset-csp"));
74
+ }
75
+
76
+ const permissions = JSON.stringify(parsed?.permissions || parsed?.app?.permissions || parsed?.tauri?.allowlist || []);
77
+ if (/"\*"|"all":true|shell:allow-execute|fs:allow-|http:allow-fetch/.test(permissions)) {
78
+ findings.push(finding(file, undefined, "broad-permission"));
79
+ }
80
+ }
81
+
82
+ function inspectRegexPattern(content, file, findings, regex, pattern) {
83
+ regex.lastIndex = 0;
84
+ let match;
85
+ while ((match = regex.exec(content)) !== null) {
86
+ findings.push(finding(file, lineFromOffset(content, match.index), pattern));
87
+ }
88
+ }
89
+
90
+ function finding(file, line, pattern) {
91
+ return {
92
+ message: "Tauri configuration appears to allow broad permissions, remote URLs or weak CSP settings.",
93
+ file,
94
+ line,
95
+ recommendation:
96
+ "Use least-privilege capabilities, restrict shell/fs/http permissions, avoid broad wildcards, and configure a strict CSP.",
97
+ heuristic: true,
98
+ metadata: { pattern }
99
+ };
100
+ }
101
+
102
+ function lineOfKey(content, key) {
103
+ const index = content.indexOf(`"${key}"`);
104
+ return index >= 0 ? lineFromOffset(content, index) : undefined;
105
+ }
106
+
107
+ function dedupe(findings) {
108
+ const seen = new Set();
109
+ return findings.filter((item) => {
110
+ const key = `${item.file}:${item.line || 0}:${item.metadata?.pattern || ""}`;
111
+ if (seen.has(key)) return false;
112
+ seen.add(key);
113
+ return true;
114
+ });
115
+ }
@@ -0,0 +1,63 @@
1
+ import { isCodeLikeFile, isEnvExampleFile, isLockfile, lineFromOffset } from "../helpers.js";
2
+
3
+ const PUBLIC_UPLOAD_PATH_RE = /(?:public|static|dist|build|\.next\/static)\/(?:uploads|files)/i;
4
+ const PUBLIC_UPLOAD_PATTERNS = [
5
+ /multer\s*\(\s*{[\s\S]{0,500}?dest\s*:\s*["'`](?:public|static|dist|build|\.next\/static)\/(?:uploads|files)["'`]/gi,
6
+ /\b(?:uploadDir|uploadsDir|destination)\b\s*=\s*["'`](?:public|static|dist|build|\.next\/static)\/(?:uploads|files)["'`]/gi,
7
+ /path\.join\s*\([^)]*["'`](?:public|static|dist|build)["'`]\s*,\s*["'`](?:uploads|files)["'`]/gi,
8
+ /fs\.writeFile\s*\(\s*["'`](?:public|static|dist|build|\.next\/static)\/(?:uploads|files)\//gi,
9
+ /app\.use\s*\(\s*["'`]\/(?:uploads|files)["'`]\s*,\s*express\.static\s*\(/gi,
10
+ /express\.static\s*\(\s*["'`](?:public|static)["'`]\s*\)/gi
11
+ ];
12
+ const VALIDATION_RE = /\b(?:fileFilter|limits\s*:\s*{[\s\S]{0,120}?fileSize|limits\.fileSize|mimetype|allowedTypes|allowedMimeTypes)\b/i;
13
+
14
+ export default {
15
+ id: "uploads.public-executable-upload",
16
+ title: "Uploads should not be stored directly in public web roots",
17
+ category: "uploads",
18
+ severity: "high",
19
+ tags: ["uploads", "static-files", "heuristic"],
20
+ run: async (context) => {
21
+ const findings = [];
22
+
23
+ for (const file of context.textFiles) {
24
+ if (isLockfile(file) || isEnvExampleFile(file) || !isCodeLikeFile(file)) continue;
25
+ const content = await context.readFileSafe(file);
26
+ if (!content || !/(upload|multer|express\.static|writeFile|public\/|static\/)/i.test(content)) continue;
27
+
28
+ for (const regex of PUBLIC_UPLOAD_PATTERNS) {
29
+ regex.lastIndex = 0;
30
+ let match;
31
+ while ((match = regex.exec(content)) !== null) {
32
+ if (!PUBLIC_UPLOAD_PATH_RE.test(match[0]) && !/\/(?:uploads|files)/i.test(match[0])) continue;
33
+ const line = lineFromOffset(content, match.index);
34
+ const validationMissing = !VALIDATION_RE.test(nearbyText(content, line, 12));
35
+
36
+ findings.push({
37
+ message:
38
+ "Uploaded files appear to be stored in a public directory, possibly without strict file type and size validation.",
39
+ file,
40
+ line,
41
+ recommendation:
42
+ "Store uploads outside the public web root, validate MIME type and extension, enforce file size limits, and serve files through controlled routes.",
43
+ heuristic: true,
44
+ metadata: {
45
+ validationMissing
46
+ }
47
+ });
48
+ break;
49
+ }
50
+ if (findings.some((finding) => finding.file === file)) break;
51
+ }
52
+ }
53
+
54
+ return findings.slice(0, 100);
55
+ }
56
+ };
57
+
58
+ function nearbyText(content, line, radius) {
59
+ const lines = content.split(/\r?\n/);
60
+ const start = Math.max(0, line - radius - 1);
61
+ const end = Math.min(lines.length, line + radius);
62
+ return lines.slice(start, end).join("\n");
63
+ }
@@ -0,0 +1,82 @@
1
+ import { isCodeLikeFile, isEnvExampleFile, isLockfile, lineFromOffset } from "../helpers.js";
2
+
3
+ const PROVIDER_RE =
4
+ /\b(?:stripe\.webhooks\.constructEvent|github webhook signature|x-hub-signature|svix|clerk|lemon\s*squeezy|lemonsqueezy|polar|paddle|webhook signature)\b/i;
5
+ const PARSED_BODY_SIGNATURE_RE = /\b(?:stripe\.webhooks\.)?constructEvent\s*\(\s*req\.body\b/gi;
6
+ const RAW_BODY_RE = /\b(?:express\.raw\s*\(|bodyParser\.raw\s*\(|rawBody|req\.rawBody|buffer)\b/i;
7
+ const GLOBAL_JSON_RE = /\bapp\.use\s*\(\s*express\.json\s*\(/i;
8
+ const WEBHOOK_ROUTE_RE = /\bapp\.(?:post|put|patch)\s*\(\s*["'`][^"'`]*webhook/i;
9
+
10
+ export default {
11
+ id: "webhooks.missing-raw-body",
12
+ title: "Signed webhooks should verify the exact raw body",
13
+ category: "webhooks",
14
+ severity: "high",
15
+ tags: ["webhooks", "signatures", "heuristic"],
16
+ run: async (context) => {
17
+ const findings = [];
18
+
19
+ for (const file of context.textFiles) {
20
+ if (isLockfile(file) || isEnvExampleFile(file) || !isCodeLikeFile(file)) continue;
21
+ const content = await context.readFileSafe(file);
22
+ if (!content || !PROVIDER_RE.test(content)) continue;
23
+
24
+ PARSED_BODY_SIGNATURE_RE.lastIndex = 0;
25
+ let match;
26
+ while ((match = PARSED_BODY_SIGNATURE_RE.exec(content)) !== null) {
27
+ const line = lineFromOffset(content, match.index);
28
+ if (RAW_BODY_RE.test(nearbyText(content, line, 12))) continue;
29
+ findings.push(webhookFinding(file, line, "parsed-body-signature-check"));
30
+ }
31
+
32
+ const jsonLine = firstLineMatching(content, GLOBAL_JSON_RE);
33
+ const routeLine = firstLineMatching(content, WEBHOOK_ROUTE_RE);
34
+ if (jsonLine && routeLine && jsonLine < routeLine && !RAW_BODY_RE.test(content)) {
35
+ findings.push(webhookFinding(file, jsonLine, "json-parser-before-webhook-route"));
36
+ }
37
+ }
38
+
39
+ return dedupe(findings).slice(0, 100);
40
+ }
41
+ };
42
+
43
+ function webhookFinding(file, line, pattern) {
44
+ return {
45
+ message:
46
+ "Webhook signature verification appears to use a parsed request body. Some providers require the exact raw body.",
47
+ file,
48
+ line,
49
+ recommendation:
50
+ "Use a raw body parser for signed webhook routes and register it before JSON parsing middleware.",
51
+ heuristic: true,
52
+ metadata: {
53
+ pattern
54
+ }
55
+ };
56
+ }
57
+
58
+ function firstLineMatching(content, regex) {
59
+ const lines = content.split(/\r?\n/);
60
+ for (let index = 0; index < lines.length; index += 1) {
61
+ regex.lastIndex = 0;
62
+ if (regex.test(lines[index])) return index + 1;
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function nearbyText(content, line, radius) {
68
+ const lines = content.split(/\r?\n/);
69
+ const start = Math.max(0, line - radius - 1);
70
+ const end = Math.min(lines.length, line + radius);
71
+ return lines.slice(start, end).join("\n");
72
+ }
73
+
74
+ function dedupe(findings) {
75
+ const seen = new Set();
76
+ return findings.filter((finding) => {
77
+ const key = `${finding.file}:${finding.line}:${finding.metadata.pattern}`;
78
+ if (seen.has(key)) return false;
79
+ seen.add(key);
80
+ return true;
81
+ });
82
+ }
package/src/cli/output.js CHANGED
@@ -1,11 +1,10 @@
1
1
  import gradient from 'gradient-string';
2
2
 
3
3
  export function printUsage() {
4
- process.stdout.write(`ItWorksBut
4
+ process.stdout.write(`ItWorksBut
5
5
 
6
6
  Usage:
7
7
  itworksbut scan [options]
8
- node ./bin/itworksbut.js scan [options]
9
8
 
10
9
  Options:
11
10
  --path <path> Project path to scan. Defaults to current directory.
@@ -25,14 +24,14 @@ Options:
25
24
  }
26
25
 
27
26
  export function printVersion(version) {
28
- try {
29
- process.stdout.write(`${gradient.rainbow(version)}\n`);
30
- } catch {
31
- process.stdout.write(`${version}\n`);
32
- }
27
+ try {
28
+ process.stdout.write(`${gradient.rainbow(version)}\n`);
29
+ } catch {
30
+ process.stdout.write(`${version}\n`);
31
+ }
33
32
  }
34
33
 
35
34
  export function printRuntimeError(error) {
36
- const message = error instanceof Error ? error.message : String(error);
37
- process.stderr.write(`ItWorksBut runtime error: ${message}\n`);
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ process.stderr.write(`ItWorksBut runtime error: ${message}\n`);
38
37
  }
@@ -7,6 +7,7 @@ const EDGY_TITLES = {
7
7
  'env.env-file-tracked': 'It works, but your .env is tracked.',
8
8
  'env.possible-secret-in-code': 'It works, but your repo may be leaking secrets.',
9
9
  'env.frontend-secret-exposure': 'It works, but your frontend env variable smells like a backend secret.',
10
+ 'secrets.secrets-in-logs': 'It works, but your logs may be leaking secrets.',
10
11
  'git.gitignore-missing': 'It works, but your repo forgot what not to commit.',
11
12
  'git.gitignore-incomplete': 'It works, but your .gitignore has holes.',
12
13
  'git.ignored-files-tracked': 'It works, but Git is already tracking files you meant to ignore.',
@@ -19,14 +20,33 @@ const EDGY_TITLES = {
19
20
  'node.rate-limit-missing': 'It works, but your endpoints have no brakes.',
20
21
  'node.helmet-missing': 'It works, but your HTTP headers are underdressed.',
21
22
  'node.cors-wildcard': 'It works, but CORS is holding the door open.',
23
+ 'node.child-process-user-input': 'It works, but your shell command trusts the internet.',
22
24
  'web.dangerous-inner-html': 'It works, but your frontend is injecting HTML with sharp edges.',
23
25
  'api.missing-auth-on-routes': 'It works, but this API route appears to trust strangers.',
24
26
  'api.idor-risk': 'It works, but this ID lookup may belong to someone else.',
27
+ 'auth.jwt-secret-weak-or-fallback': 'It works, but your JWT secret has a fallback key.',
28
+ 'auth.password-hashing-missing': 'It works, but your passwords may be stored too honestly.',
29
+ 'auth.missing-csrf-protection': 'It works, but your browser may be submitting forms behind your back.',
30
+ 'api.missing-method-guard': 'It works, but your API does not care how it gets called.',
31
+ 'api.mass-assignment-risk': 'It works, but your users may be editing fields they should never touch.',
32
+ 'api.no-schema-validation': 'It works, but your API believes whatever the request says.',
25
33
  'database.raw-sql-interpolation': 'It works, but your SQL query is one template string away from pain.',
26
34
  'database.no-migrations': 'It works, but your database schema has no paper trail.',
35
+ 'cookies.insecure-session-cookie': 'It works, but your session cookie is dressed for localhost.',
36
+ 'uploads.public-executable-upload': 'It works, but your uploads are sitting in the front window.',
37
+ 'webhooks.missing-raw-body': 'It works, but your webhook signature check may be checking the wrong body.',
38
+ 'llm.prompt-injection-risk': 'It works, but your AI output has admin energy.',
39
+ 'frontend.sourcemaps-production': 'It works, but your source code may be shipping with the app.',
40
+ 'frontend.localstorage-token': 'It works, but your auth token lives where XSS can read it.',
41
+ 'files.path-traversal-risk': 'It works, but your file path may be taking requests too literally.',
42
+ 'ssrf.user-controlled-fetch': 'It works, but your server is fetching whatever strangers ask for.',
43
+ 'next.public-server-code-risk': 'It works, but your client component is carrying server baggage.',
44
+ 'config.debug-production': 'It works, but production still thinks it is a dev server.',
27
45
  'electron.node-integration-enabled': 'It works, but Electron is holding the Node.js door open.',
28
46
  'electron.context-isolation-disabled': 'It works, but your renderer and backend are sharing a room.',
47
+ 'electron.remote-content-with-node': 'It works, but Electron is letting the internet sit next to Node.js.',
29
48
  'tauri.dangerous-allowlist-or-capabilities': 'It works, but your Tauri permissions look too generous.',
49
+ 'tauri.remote-url-permissions-risk': 'It works, but your Tauri app is trusting too much surface area.',
30
50
  };
31
51
 
32
52
  const SEVERITY_META = {
@@ -44,6 +64,8 @@ const FIX_PROMPT_ACTIONS = {
44
64
  'Move hardcoded secret material into a runtime secret store or CI secret, replace committed values with placeholders, and avoid printing secret values anywhere.',
45
65
  'env.frontend-secret-exposure':
46
66
  'Move secret-like frontend environment variables to server-side code and keep only intentionally public values behind public prefixes.',
67
+ 'secrets.secrets-in-logs':
68
+ 'Remove sensitive logging, mask secrets, and log only explicit non-sensitive fields.',
47
69
  'git.gitignore-missing':
48
70
  'Add a project-appropriate .gitignore for dependencies, local env files, build output, logs, databases, OS files, and coverage artifacts.',
49
71
  'git.gitignore-incomplete':
@@ -73,6 +95,8 @@ const FIX_PROMPT_ACTIONS = {
73
95
  'Install and apply Helmet or equivalent security headers early in the Express middleware stack.',
74
96
  'node.cors-wildcard':
75
97
  'Restrict CORS origins to trusted application origins and avoid wildcard or credentials-unsafe configurations.',
98
+ 'node.child-process-user-input':
99
+ 'Avoid shell execution with user input. Use spawn with fixed command and argument arrays, validate against allowlists, and never concatenate shell strings.',
76
100
  'web.client-side-auth-only':
77
101
  'Move authorization enforcement to server-side API or route handlers and keep frontend checks as UI-only hints.',
78
102
  'web.dangerous-inner-html':
@@ -82,15 +106,51 @@ const FIX_PROMPT_ACTIONS = {
82
106
  'Add explicit authentication and authorization to the route, or document why the route is intentionally public.',
83
107
  'api.idor-risk':
84
108
  'Scope object access by authenticated user, owner, tenant, account, or organization in addition to object id.',
109
+ 'auth.jwt-secret-weak-or-fallback':
110
+ 'Require a strong JWT secret from the environment in production and fail startup if it is missing.',
111
+ 'auth.password-hashing-missing':
112
+ 'Hash passwords with argon2, bcrypt, scrypt or PBKDF2 before storage. Never store raw passwords.',
113
+ 'auth.missing-csrf-protection':
114
+ 'Use SameSite cookies, CSRF tokens or another explicit CSRF mitigation for state-changing routes.',
115
+ 'api.missing-method-guard':
116
+ 'Restrict API routes to the intended HTTP methods and return 405 Method Not Allowed for unsupported methods.',
117
+ 'api.mass-assignment-risk':
118
+ 'Whitelist allowed fields explicitly. Never pass req.body directly into database create/update calls.',
119
+ 'api.no-schema-validation':
120
+ 'Validate request body, query and params with a schema library such as Zod, Joi, Valibot, AJV or equivalent.',
85
121
  'database.raw-sql-interpolation':
86
122
  'Replace SQL string interpolation or concatenation with parameterized queries, prepared statements, or a safe ORM query builder.',
87
123
  'database.no-migrations': 'Add versioned database migrations that match the detected ORM or database stack.',
124
+ 'cookies.insecure-session-cookie':
125
+ 'Set httpOnly, secure and sameSite for session cookies. Use secure: true in production.',
126
+ 'uploads.public-executable-upload':
127
+ 'Store uploads outside the public web root, validate MIME type and extension, enforce file size limits, and serve files through controlled routes.',
128
+ 'webhooks.missing-raw-body':
129
+ 'Use a raw body parser for signed webhook routes and register it before JSON parsing middleware.',
130
+ 'llm.prompt-injection-risk':
131
+ 'Treat model output as untrusted input. Validate with schemas, use allowlists, require human approval for dangerous actions, and never execute raw model output.',
132
+ 'frontend.sourcemaps-production':
133
+ 'Disable public production source maps unless intentionally needed. If needed, upload them privately to error tracking instead of serving them publicly.',
134
+ 'frontend.localstorage-token':
135
+ 'Prefer secure, httpOnly cookies for session tokens where appropriate. If browser storage is unavoidable, minimize token lifetime and harden XSS protections.',
136
+ 'files.path-traversal-risk':
137
+ 'Normalize and validate paths, use allowlists, reject traversal sequences, and ensure resolved paths stay inside an intended base directory.',
138
+ 'ssrf.user-controlled-fetch':
139
+ 'Use strict URL allowlists, block private/internal IP ranges including 127.0.0.1, localhost, 169.254.169.254 and RFC1918 ranges, and avoid fetching arbitrary user-provided URLs.',
140
+ 'next.public-server-code-risk':
141
+ 'Move database, filesystem, secret and server-only logic into Server Components, API routes or server actions. Keep Client Components free of backend dependencies.',
142
+ 'config.debug-production':
143
+ 'Disable verbose errors and debug flags in production. Avoid exposing stack traces, internal paths or development tooling.',
88
144
  'electron.node-integration-enabled':
89
145
  'Set nodeIntegration to false and expose only narrowly scoped APIs through preload.',
90
146
  'electron.context-isolation-disabled':
91
147
  'Enable contextIsolation and review preload boundaries for renderer-to-main communication.',
148
+ 'electron.remote-content-with-node':
149
+ 'Avoid loading remote content with Node.js integration. Use nodeIntegration: false, contextIsolation: true, sandbox: true, webSecurity: true and a minimal preload bridge.',
92
150
  'tauri.dangerous-allowlist-or-capabilities':
93
151
  'Tighten Tauri allowlists, capabilities, scopes, shell access, filesystem access, remote URLs, and CSP.',
152
+ 'tauri.remote-url-permissions-risk':
153
+ 'Use least-privilege capabilities, restrict shell/fs/http permissions, avoid broad wildcards, and configure a strict CSP.',
94
154
  };
95
155
 
96
156
  export function getConsoleFindingTitle(finding) {