logcop 1.0.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/README.md ADDED
@@ -0,0 +1,286 @@
1
+ # 🚓 logcop
2
+
3
+ <p align="center">
4
+ <img src="./logcop.png" alt="logcop" width="600" />
5
+ </p>
6
+
7
+ ### Detect. Remove. Protect.
8
+
9
+ ---
10
+
11
+ You're three hours deep into a bug.
12
+
13
+ You've added `console.log` everywhere function arguments, API responses, auth tokens, the whole request object. You finally find the bug, fix it, close the laptop.
14
+
15
+ You forgot to remove the logs.
16
+
17
+ They ship. They hit production. Your JWT is now sitting in Datadog. Your `user.password` is in CloudWatch. Your entire `process.env` just got logged in a platform that your whole team and maybe your infrastructure vendor has access to.
18
+
19
+ You didn't mean to do it. Nobody does. That's exactly the problem.
20
+
21
+ **logcop is the last check before code leaves your machine.**
22
+
23
+ ```bash
24
+ npx logcop scan
25
+ ```
26
+
27
+ ---
28
+
29
+ ## The problem is bigger than you think
30
+
31
+ Every developer debugs with `console.log`. It's fast, it's easy, it works. The issue isn't the logs themselves it's what they contain, and that they never get cleaned up.
32
+
33
+ Here's what a typical debugging session looks like:
34
+
35
+ ```js
36
+ console.log("user object:", user); // contains email, role, password hash
37
+ console.log("auth response:", response.data); // contains JWT token
38
+ console.log("env check:", process.env); // contains every secret in your app
39
+ console.log("request body:", req.body); // contains raw user input
40
+ console.log("here"); // the classic
41
+ ```
42
+
43
+ Five logs. Four of them are a security incident waiting to happen.
44
+
45
+ The code works so you ship it. The logs go with it. Now those values are streaming into your log aggregator on every request silently, permanently, until someone notices or something goes wrong.
46
+
47
+ This is not a hypothetical. It happens constantly. It happens to senior engineers. It happens at companies with security teams. It happens because cleanup is the last thing on your mind when you've finally fixed the bug.
48
+
49
+ logcop makes cleanup automatic.
50
+
51
+ ---
52
+
53
+ ## What it does
54
+
55
+ Scans your entire codebase for every `console.log`, `console.error`, `console.warn`, and `console.debug` and tells you not just that they exist, but **what they're logging and how dangerous it is.**
56
+
57
+ ```
58
+ ✔ Scan completed
59
+
60
+ ⚠ CRITICAL RISK potential secret leaks:
61
+
62
+ CRITICAL src/auth.js:14
63
+ → console.log(password)
64
+
65
+ CRITICAL src/api.js:8
66
+ → console.log(process.env)
67
+
68
+ src/auth.js
69
+ → console.log(password) :14 CRITICAL
70
+ → console.log(token) :22 CRITICAL
71
+ → console.log(user) :31 HIGH
72
+ → console.log("starting app") :45
73
+
74
+ ┌──────────────────────────────────────────────────┐
75
+ │ │
76
+ │ Found 4 console statements across 1 file │
77
+ │ 🔴 2 critical │
78
+ │ 🟡 1 high risk │
79
+ │ Run logcop fix to remove them │
80
+ │ │
81
+ └──────────────────────────────────────────────────┘
82
+ ```
83
+
84
+ This is the difference between logcop and ESLint's `no-console`:
85
+
86
+ ESLint sees `console.log(x)` and flags it.
87
+ logcop looks **inside** and tells you `x` is your JWT token.
88
+
89
+ ---
90
+
91
+ ## Install
92
+
93
+ ```bash
94
+ npm install -g logcop
95
+ ```
96
+
97
+ Or run without installing:
98
+
99
+ ```bash
100
+ npx logcop scan
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Commands
106
+
107
+ ### Scan
108
+
109
+ ```bash
110
+ logcop scan
111
+ ```
112
+
113
+ Scans every `.js`, `.ts`, `.jsx`, `.tsx` file in your project. Prints results grouped by file with risk levels inline. Critical findings are pulled to the top impossible to miss.
114
+
115
+ ### Fix
116
+
117
+ ```bash
118
+ logcop fix
119
+ ```
120
+
121
+ Removes all console statements automatically. Handles trailing semicolons, blank lines, and indentation leaves your code clean, not full of ghost whitespace.
122
+
123
+ ```bash
124
+ logcop fix --dry-run
125
+ ```
126
+
127
+ Preview exactly what would be removed before touching anything. Always a good idea before running on a real codebase.
128
+
129
+ ### Comment
130
+
131
+ ```bash
132
+ logcop comment
133
+ ```
134
+
135
+ Not ready to delete? Comments them out instead of removing them:
136
+
137
+ ```js
138
+ // console.log(user) // logcop: disabled
139
+ ```
140
+
141
+ Non-destructive, reversible, and still silences the output. Useful when you want to keep the log for reference but not have it run.
142
+
143
+ ```bash
144
+ logcop comment --dry-run
145
+ ```
146
+
147
+ ### Git Hook
148
+
149
+ ```bash
150
+ logcop install-hook
151
+ ```
152
+
153
+ Installs a git pre-commit hook that runs `logcop scan --ci` before every commit. If console statements are found, the commit is blocked. One command. No configuration. Works forever.
154
+
155
+ ### CI Mode
156
+
157
+ ```bash
158
+ logcop scan --ci
159
+ ```
160
+
161
+ Exits with code `1` if any console statements are found, `0` if the project is clean. Drop this into any pipeline and no console statement ever merges to main again.
162
+
163
+ ### JSON Output
164
+
165
+ ```bash
166
+ logcop scan --json
167
+ ```
168
+
169
+ Machine-readable output for pipelines, scripts, dashboards, and custom tooling:
170
+
171
+ ```json
172
+ {
173
+ "total": 4,
174
+ "files": 1,
175
+ "critical": 2,
176
+ "high": 1,
177
+ "results": [
178
+ {
179
+ "file": "src/auth.js",
180
+ "line": 14,
181
+ "type": "log",
182
+ "risk": "critical",
183
+ "argsSource": "password"
184
+ }
185
+ ]
186
+ }
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Risk Levels
192
+
193
+ logcop doesn't just find console statements it reads their arguments and scores them by how dangerous they are. String contents are ignored. Only actual variable names and object properties are checked, so `console.log("request failed")` won't be flagged but `console.log(request)` will.
194
+
195
+ | Level | What it catches |
196
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------- |
197
+ | 🔴 Critical | `password`, `secret`, `token`, `apiKey`, `jwt`, `privateKey`, `Authorization`, `process.env`, `accessToken`, `clientSecret` |
198
+ | 🟡 High | `user`, `userData`, `req.body`, `headers`, `config`, `db`, `connectionString`, `response.data` |
199
+ | 🟢 Medium | `email`, `phone`, `payload`, `data`, `body` |
200
+
201
+ Critical findings are always shown first, separated from the rest of the output. If your scan has red you should not ship.
202
+
203
+ ---
204
+
205
+ ## GitHub Actions
206
+
207
+ Add this to your repo and logcop runs on every pull request:
208
+
209
+ ```yaml
210
+ # .github/workflows/logcop.yml
211
+ name: logcop
212
+
213
+ on: [pull_request]
214
+
215
+ jobs:
216
+ scan:
217
+ runs-on: ubuntu-latest
218
+ steps:
219
+ - uses: actions/checkout@v3
220
+ - uses: actions/setup-node@v3
221
+ with:
222
+ node-version: 18
223
+ - run: npx logcop scan --ci
224
+ ```
225
+
226
+ PRs with console statements fail the check. They cannot merge until the logs are removed or the risk is reviewed. This is the zero-effort way to make console hygiene a team standard without adding it to your code review checklist.
227
+
228
+ ---
229
+
230
+ ## Config
231
+
232
+ Create `logcop.config.js` in your project root to customize behavior for your team:
233
+
234
+ ```js
235
+ module.exports = {
236
+ // folders to skip
237
+ ignore: ["node_modules/**", "dist/**", "build/**"],
238
+
239
+ // console methods to never touch
240
+ // useful if your team uses console.error for intentional error logging
241
+ keep: ["error", "warn"],
242
+
243
+ // customize risk patterns to match your codebase
244
+ risk: {
245
+ critical: ["password", "secret", "token", "process.env", "apiKey", "jwt"],
246
+ high: ["user", "userData", "req.body", "headers", "config", "db"],
247
+ medium: ["email", "phone", "payload"],
248
+ },
249
+ };
250
+ ```
251
+
252
+ Commit this file to your repo and the whole team runs with the same rules. No per-developer configuration needed.
253
+
254
+ ---
255
+
256
+ ## Why not just use ESLint?
257
+
258
+ You probably already have ESLint. You might already have `no-console` enabled. Here's why that's not enough:
259
+
260
+ ESLint tells you a log exists. It has no idea what's inside it. `console.log(password)` and `console.log("hello")` look identical to ESLint both get flagged the same way, both get fixed the same way.
261
+
262
+ logcop treats them differently because they are different. One is noise. One is a credentials leak.
263
+
264
+ Beyond that logcop gives you choices. You can fix, comment, preview, or just scan. You can keep `error` and `warn` while removing `log` and `debug`. You can run it in CI, wire it to a git hook, or pipe its output as JSON into your own tooling.
265
+
266
+ It's not a replacement for ESLint. It's the thing ESLint can't do.
267
+
268
+ ---
269
+
270
+ ## Roadmap
271
+
272
+ - [x] Console statement detection
273
+ - [x] Security risk scanner
274
+ - [x] Auto-fix with clean removal
275
+ - [x] Comment mode
276
+ - [x] Dry run mode
277
+ - [x] Git hook integration
278
+ - [x] CI/CD pipeline mode
279
+ - [x] JSON output
280
+ - [x] Team config file
281
+
282
+ ---
283
+
284
+ ## License
285
+
286
+ MIT © Prakhar Mishra
package/bin/logcop.js ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const { Command } = require("commander");
5
+ const chalk = require("chalk");
6
+ const ora = require("ora").default;
7
+ const boxen = require("boxen").default;
8
+ const figlet = require("figlet");
9
+ const gradient = require("gradient-string");
10
+ const { scanProject } = require("../src/core/scanner");
11
+ const { fixProject, commentProject } = require("../src/core/fixer");
12
+ const program = new Command();
13
+
14
+ program
15
+ .name("logcop")
16
+ .description("🚓 Detect and remove console.log statements")
17
+ .version("1.0.0");
18
+
19
+ // SCAN
20
+ /*program
21
+ .command("scan")
22
+ .description("Scan project for console logs")
23
+ .option("--ci", "Exit with code 1 if any console statements found")
24
+ .action(async (options) => {
25
+ await scanProject({ ci: options.ci });
26
+ });
27
+ */
28
+ program
29
+ .command("scan")
30
+ .description("Scan project for console logs")
31
+ .option("--ci", "Exit with code 1 if any console statements found")
32
+ .option("--json", "Output results as JSON")
33
+ .action(async (options) => {
34
+ await scanProject({ ci: options.ci, json: options.json });
35
+ });
36
+ /*
37
+
38
+ program
39
+ .command("fix")
40
+ .description("Remove console logs automatically")
41
+ .action(async () => {
42
+ await fixProject();
43
+ });
44
+ *
45
+ // INSTALL
46
+ /*program
47
+ .command("install-hook")
48
+ .description("Install git pre-commit hook")
49
+ .action(async () => {
50
+ const spinner = ora("Installing git hook...").start();
51
+
52
+ await new Promise((r) => setTimeout(r, 1000));
53
+
54
+ spinner.succeed(chalk.green("Git hook installed"));
55
+ });
56
+ */
57
+
58
+ // FIX
59
+ program
60
+ .command("fix")
61
+ .description("Remove console logs automatically")
62
+ .option("--dry-run", "Preview what would be removed without changing files")
63
+ .action(async (options) => {
64
+ await fixProject({ dryRun: options.dryRun });
65
+ });
66
+
67
+ //comment rather than fully removing the logs;
68
+ // COMMENT
69
+ program
70
+ .command("comment")
71
+ .description("Comment out console statements instead of removing them")
72
+ .option("--dry-run", "Preview what would be commented without changing files")
73
+ .action(async (options) => {
74
+ await commentProject({ dryRun: options.dryRun });
75
+ });
76
+
77
+ program
78
+ .command("install-hook")
79
+ .description("Install git pre-commit hook")
80
+ .action(async () => {
81
+ const spinner = ora("Installing git hook...").start();
82
+
83
+ const hookDir = path.join(process.cwd(), ".git", "hooks");
84
+ const hookPath = path.join(hookDir, "pre-commit");
85
+
86
+ //check for .git's existence
87
+ if (!fs.existsSync(hookDir)) {
88
+ spinner.fail(
89
+ chalk.red("No .git directory found. Are you in a git repo?"),
90
+ );
91
+ process.exit(1);
92
+ }
93
+ const hookScript = `#!/bin/sh
94
+ npx logcop scan --ci
95
+ if [ $? -ne 0 ]; then
96
+ echo ""
97
+ echo " logcop: console statements detected. Run 'logcop fix' to remove them."
98
+ exit 1
99
+ fi
100
+ `;
101
+
102
+ fs.writeFileSync(hookPath, hookScript, { mode: 0o755 });
103
+
104
+ spinner.succeed(chalk.green("Git hook installed"));
105
+ console.log(chalk.gray(` → ${hookPath}`));
106
+ console.log(chalk.gray(" logcop will now run before every commit."));
107
+ });
108
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "logcop",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to detect and remove console.log statements before committing code",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "logcop": "bin/logcop.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"No tests yet\""
11
+ },
12
+ "keywords": [
13
+ "console-log",
14
+ "cleanup",
15
+ "cli",
16
+ "git-hook",
17
+ "developer-tool",
18
+ "vibe-coding",
19
+ "security",
20
+ "linter",
21
+ "javascript",
22
+ "typescript"
23
+ ],
24
+ "author": "Prakhar Mishra",
25
+ "license": "MIT",
26
+ "type": "commonjs",
27
+ "dependencies": {
28
+ "acorn": "^8.16.0",
29
+ "acorn-walk": "^8.3.5",
30
+ "boxen": "^8.0.1",
31
+ "chalk": "^4.1.2",
32
+ "commander": "^14.0.3",
33
+ "figlet": "^1.10.0",
34
+ "glob": "^13.0.6",
35
+ "gradient-string": "^3.0.0",
36
+ "ora": "^9.3.0"
37
+ }
38
+ }
@@ -0,0 +1,81 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const defaults = {
5
+ ignore: ["node_modules/**", "dist/**", "build/**", "coverage/**", ".next/**"],
6
+ keep: [],
7
+ risk: {
8
+ critical: [
9
+ "process.env",
10
+ "password",
11
+ "secret",
12
+ "token",
13
+ "apiKey",
14
+ "api_key",
15
+ "privateKey",
16
+ "private_key",
17
+ "jwt",
18
+ "Authorization",
19
+ "accessToken",
20
+ "access_token",
21
+ "clientSecret",
22
+ "client_secret",
23
+ ],
24
+ high: [
25
+ "user",
26
+ "userData",
27
+ "currentUser",
28
+ "req.body",
29
+ "request.body",
30
+ "response.data",
31
+ "result",
32
+ "headers",
33
+ "req.headers",
34
+ "config",
35
+ "settings",
36
+ "db",
37
+ "connection",
38
+ "connectionString",
39
+ ],
40
+ medium: [
41
+ "email",
42
+ "phone",
43
+ "ssn",
44
+ "payload",
45
+ "data",
46
+ "body",
47
+ "res",
48
+ "ctx",
49
+ "context",
50
+ ],
51
+ },
52
+ };
53
+
54
+ function loadConfig() {
55
+ const configPath = path.join(process.cwd(), "logcop.config.js");
56
+
57
+ // no config file found, use defaults
58
+ if (!fs.existsSync(configPath)) {
59
+ return defaults;
60
+ }
61
+
62
+ try {
63
+ const userConfig = require(configPath);
64
+
65
+ // deep merge
66
+ return {
67
+ ignore: userConfig.ignore || defaults.ignore,
68
+ keep: userConfig.keep || defaults.keep,
69
+ risk: {
70
+ critical: userConfig.risk?.critical || defaults.risk.critical,
71
+ high: userConfig.risk?.high || defaults.risk.high,
72
+ medium: userConfig.risk?.medium || defaults.risk.medium,
73
+ },
74
+ };
75
+ } catch (e) {
76
+ console.warn("⚠ Could not load logcop.config.js, using defaults.");
77
+ return defaults;
78
+ }
79
+ }
80
+
81
+ module.exports = { loadConfig };
@@ -0,0 +1,179 @@
1
+ const fs = require("fs");
2
+ const glob = require("glob");
3
+ const ora = require("ora").default;
4
+ const chalk = require("chalk");
5
+ const boxen = require("boxen").default;
6
+ const { parseFile } = require("./scanner");
7
+ const { loadConfig } = require("./config");
8
+ //detect logs
9
+ function fixFile(file) {
10
+ const code = fs.readFileSync(file, "utf-8");
11
+
12
+ if (!code.includes("console")) return 0;
13
+
14
+ const logs = parseFile(file); //reusing the old parsefile function instead of reinventing the wheel;
15
+
16
+ if (!logs.length) return 0;
17
+
18
+ let updated = code;
19
+
20
+ // removing from bottom → top to avoid index shift
21
+ logs
22
+ .sort((a, b) => b.start - a.start)
23
+ .forEach((log) => {
24
+ let start = log.start;
25
+ let end = log.end;
26
+ //consumptioon of trailing semicolon ;edge case
27
+ if (updated[end] === ";") end += 1;
28
+
29
+ //consumption of the entire line if nothinng else is on it
30
+ const lineStart = updated.lastIndexOf("\n", start - 1) + 1;
31
+ const beforeLog = updated.slice(lineStart, start).trim();
32
+ if (beforeLog === "") {
33
+ // line is only whitespace + the console statement, remove whole line
34
+ start = lineStart;
35
+ if (updated[end] === "\n") end += 1;
36
+ }
37
+
38
+ updated = updated.slice(0, start) + updated.slice(end);
39
+ });
40
+
41
+ // collapse multiple blank lines into one
42
+ updated = updated.replace(/\n{3,}/g, "\n\n");
43
+
44
+ fs.writeFileSync(file, updated, "utf-8");
45
+
46
+ return logs.length;
47
+ }
48
+ function commentFile(file) {
49
+ const code = fs.readFileSync(file, "utf-8");
50
+
51
+ if (!code.includes("console")) return 0;
52
+
53
+ const logs = parseFile(file);
54
+
55
+ if (!logs.length) return 0;
56
+
57
+ const lines = code.split("\n");
58
+
59
+ let commented = 0;
60
+
61
+ logs.forEach((log) => {
62
+ const lineIndex = log.line - 1;
63
+ const line = lines[lineIndex];
64
+
65
+ // skip if already commented out
66
+ if (line.trimStart().startsWith("//")) return;
67
+
68
+ // get the indentation
69
+ const indent = line.match(/^(\s*)/)[1];
70
+
71
+ // comment it out with a logcop tag
72
+ lines[lineIndex] = `${indent}// ${line.trim()} // logcop: disabled`;
73
+ commented++;
74
+ });
75
+
76
+ fs.writeFileSync(file, lines.join("\n"), "utf-8");
77
+
78
+ return commented;
79
+ }
80
+
81
+ async function commentProject({ dryRun = false } = {}) {
82
+ const spinner = ora(
83
+ dryRun ? "Previewing comments..." : "Commenting out console statements...",
84
+ ).start();
85
+
86
+ const config = loadConfig();
87
+ const files = glob.sync("**/*.{js,ts,jsx,tsx}", {
88
+ ignore: config.ignore,
89
+ });
90
+
91
+ let commented = 0;
92
+
93
+ files.forEach((file) => {
94
+ if (dryRun) {
95
+ const logs = parseFile(file);
96
+ const uncommented = logs.filter((log) => {
97
+ const lines = fs.readFileSync(file, "utf-8").split("\n");
98
+ return !lines[log.line - 1].trimStart().startsWith("//");
99
+ });
100
+ if (uncommented.length > 0) {
101
+ console.log("");
102
+ console.log(chalk.cyan.bold(` ${file}`));
103
+ uncommented.forEach((log) => {
104
+ console.log(
105
+ ` ${chalk.gray("→")} would comment console.${chalk.yellow(log.type)}${chalk.gray(`(${log.argsSource})`)} ${chalk.gray(`:${log.line}`)}`,
106
+ );
107
+ });
108
+ commented += uncommented.length;
109
+ }
110
+ } else {
111
+ commented += commentFile(file);
112
+ }
113
+ });
114
+
115
+ spinner.succeed(
116
+ chalk.green(dryRun ? "Dry run completed" : "Comment completed"),
117
+ );
118
+
119
+ console.log(
120
+ boxen(
121
+ dryRun
122
+ ? chalk.cyan(
123
+ ` Would comment ${commented} console statement${commented === 1 ? "" : "s"}\n`,
124
+ ) +
125
+ chalk.gray(" (no files were changed)\n") +
126
+ chalk.gray(" Run logcop comment to apply")
127
+ : chalk.cyan(
128
+ ` Commented out ${commented} console statement${commented === 1 ? "" : "s"}`,
129
+ ),
130
+ { padding: 1, borderColor: "cyan" },
131
+ ),
132
+ );
133
+ }
134
+ async function fixProject({ dryRun = false } = {}) {
135
+ const spinner = ora(
136
+ dryRun ? "Previewing changes..." : "Removing console statements...",
137
+ ).start();
138
+ const config = loadConfig();
139
+ const files = glob.sync("**/*.{js,ts,jsx,tsx}", {
140
+ ignore: config.ignore,
141
+ });
142
+ let removed = 0;
143
+
144
+ files.forEach((file) => {
145
+ if (dryRun) {
146
+ const logs = parseFile(file);
147
+ if (logs.length > 0) {
148
+ console.log("");
149
+ console.log(chalk.cyan.bold(` ${file}`));
150
+ logs.forEach((log) => {
151
+ console.log(
152
+ ` ${chalk.gray("→")} would remove console.${chalk.yellow(log.type)}${chalk.gray(`(${log.argsSource})`)} ${chalk.gray(`:${log.line}`)}`,
153
+ );
154
+ });
155
+ removed += logs.length;
156
+ }
157
+ } else {
158
+ removed += fixFile(file);
159
+ }
160
+ });
161
+
162
+ spinner.succeed(chalk.green(dryRun ? "Dry run completed" : "Fix completed"));
163
+
164
+ console.log(
165
+ boxen(
166
+ dryRun
167
+ ? chalk.cyan(
168
+ ` Would remove ${removed} console statement${removed === 1 ? "" : "s"}\n`,
169
+ ) +
170
+ chalk.gray(" (no files were changed)\n") +
171
+ chalk.gray(" Run logcop fix to apply")
172
+ : chalk.cyan(
173
+ ` Removed ${removed} console statement${removed === 1 ? "" : "s"}`,
174
+ ),
175
+ { padding: 1, borderColor: "cyan" },
176
+ ),
177
+ );
178
+ }
179
+ module.exports = { fixProject, commentProject };
@@ -0,0 +1,213 @@
1
+ const fs = require("fs");
2
+ const ora = require("ora").default;
3
+ const chalk = require("chalk");
4
+ const boxen = require("boxen").default;
5
+ const glob = require("glob");
6
+ const acorn = require("acorn");
7
+ const walk = require("acorn-walk");
8
+ const { loadConfig } = require("./config");
9
+
10
+ function detectRisk(argsSource, config) {
11
+ // strip string literals before checking — we only care about variable names
12
+ // e.g. console.log("request failed") should NOT match "request"
13
+ const withoutStrings = argsSource
14
+ .replace(/"[^"]*"/g, '""') // remove double quoted strings
15
+ .replace(/'[^']*'/g, "''") // remove single quoted strings
16
+ .replace(/`[^`]*`/g, "``"); // remove template literals
17
+
18
+ const text = withoutStrings.toLowerCase();
19
+
20
+ for (const pattern of config.risk.critical) {
21
+ if (text.includes(pattern.toLowerCase())) return "critical";
22
+ }
23
+ for (const pattern of config.risk.high) {
24
+ if (text.includes(pattern.toLowerCase())) return "high";
25
+ }
26
+ for (const pattern of config.risk.medium) {
27
+ if (text.includes(pattern.toLowerCase())) return "medium";
28
+ }
29
+
30
+ return null;
31
+ }
32
+
33
+ // parse a single file
34
+ function parseFile(file) {
35
+ const code = fs.readFileSync(file, "utf-8");
36
+ const logs = [];
37
+ // fast skip if file doesn't contain console
38
+ if (!code.includes("console")) {
39
+ return logs;
40
+ }
41
+ const config = loadConfig();
42
+ const allowedMethods = ["log", "error", "warn", "debug"].filter(
43
+ (x) => !config.keep.includes(x),
44
+ );
45
+ try {
46
+ const ast = acorn.parse(code, {
47
+ ecmaVersion: "latest",
48
+ sourceType: "module",
49
+ locations: true,
50
+ });
51
+
52
+ walk.simple(ast, {
53
+ CallExpression(node) {
54
+ if (
55
+ node.callee.type === "MemberExpression" &&
56
+ node.callee.object.name === "console" &&
57
+ allowedMethods.includes(node.callee.property.name)
58
+ ) {
59
+ const argsSource = code.slice(
60
+ node.arguments[0]?.start ?? node.start,
61
+ node.arguments[node.arguments.length - 1]?.end ?? node.end,
62
+ );
63
+ const risk =
64
+ node.arguments.length > 0 ? detectRisk(argsSource, config) : null;
65
+
66
+ logs.push({
67
+ file,
68
+ line: node.loc.start.line,
69
+ type: node.callee.property.name,
70
+ risk,
71
+ argsSource,
72
+ start: node.start,
73
+ end: node.end,
74
+ }); //added start and end of the console statements just to reuse it in fixer.js;
75
+ }
76
+ },
77
+ });
78
+ } catch (error) {
79
+ // ignore parse errors
80
+ }
81
+
82
+ return logs;
83
+ }
84
+
85
+ async function scanProject({ ci = false, json = false } = {}) {
86
+ const spinner = ora("Scanning project...").start();
87
+
88
+ //real engine for the file scan/
89
+ const config = loadConfig();
90
+ const files = glob.sync("**/*.{js,ts,jsx,tsx}", {
91
+ ignore: config.ignore,
92
+ });
93
+ let results = [];
94
+
95
+ files.forEach((file) => {
96
+ const logs = parseFile(file);
97
+ results.push(...logs);
98
+ });
99
+
100
+ const grouped = {};
101
+
102
+ results.forEach((r) => {
103
+ if (!grouped[r.file]) {
104
+ grouped[r.file] = [];
105
+ }
106
+ grouped[r.file].push(r);
107
+ });
108
+
109
+ if (!json) spinner.succeed(chalk.green("Scan completed"));
110
+ else spinner.stop();
111
+
112
+ if (results.length === 0) {
113
+ if (json) {
114
+ console.log(
115
+ JSON.stringify(
116
+ { total: 0, files: 0, critical: 0, high: 0, results: [] },
117
+ null,
118
+ 2,
119
+ ),
120
+ );
121
+ } else {
122
+ console.log(
123
+ boxen(chalk.green(" ✔ No console statements found"), {
124
+ padding: 1,
125
+ borderColor: "green",
126
+ }),
127
+ );
128
+ }
129
+ return;
130
+ }
131
+ // json output mode
132
+ if (json) {
133
+ const output = {
134
+ total: results.length,
135
+ files: Object.keys(grouped).length,
136
+ critical: results.filter((r) => r.risk === "critical").length,
137
+ high: results.filter((r) => r.risk === "high").length,
138
+ results: results.map((r) => ({
139
+ file: r.file,
140
+ line: r.line,
141
+ type: r.type,
142
+ risk: r.risk || null,
143
+ argsSource: r.argsSource || null,
144
+ })),
145
+ };
146
+ console.log(JSON.stringify(output, null, 2));
147
+ if (ci && results.length > 0) process.exit(1);
148
+ return;
149
+ }
150
+ // risk badge helper
151
+ const riskBadge = (risk) => {
152
+ if (risk === "critical") return chalk.bgRed.white(" CRITICAL ");
153
+ if (risk === "high") return chalk.bgYellow.black(" HIGH ");
154
+ if (risk === "medium") return chalk.bgCyan.black(" MEDIUM ");
155
+ return "";
156
+ };
157
+
158
+ // pull out critical ones first can't miss them
159
+ const criticals = results.filter((r) => r.risk === "critical");
160
+ if (criticals.length > 0) {
161
+ console.log(
162
+ chalk.red.bold("\n CRITICAL RISK potential secret leaks:\n"),
163
+ );
164
+ criticals.forEach((log) => {
165
+ console.log(
166
+ ` ${riskBadge("critical")} ${chalk.gray(log.file)}${chalk.gray(`:${log.line}`)}`,
167
+ );
168
+ console.log(
169
+ ` ${chalk.gray("→")} console.${chalk.yellow(log.type)}(${chalk.red(log.argsSource)})\n`,
170
+ );
171
+ });
172
+ }
173
+
174
+ // then print all files grouped
175
+ console.log("");
176
+ Object.keys(grouped).forEach((file) => {
177
+ console.log(chalk.cyan.bold(` ${file}`));
178
+
179
+ grouped[file].forEach((log) => {
180
+ const badge = riskBadge(log.risk);
181
+ const args = log.argsSource ? chalk.gray(`(${log.argsSource})`) : "";
182
+ console.log(
183
+ ` ${chalk.gray("→")} console.${chalk.yellow(log.type)}${args} ${chalk.gray(`:${log.line}`)} ${badge}`,
184
+ );
185
+ });
186
+
187
+ console.log("");
188
+ });
189
+
190
+ // summary box
191
+ const criticalCount = results.filter((r) => r.risk === "critical").length;
192
+ const highCount = results.filter((r) => r.risk === "high").length;
193
+
194
+ console.log(
195
+ boxen(
196
+ chalk.yellow(
197
+ ` Found ${results.length} console statement${results.length === 1 ? "" : "s"} across ${Object.keys(grouped).length} file${Object.keys(grouped).length === 1 ? "" : "s"}\n`,
198
+ ) +
199
+ (criticalCount > 0
200
+ ? chalk.red(` 🔴 ${criticalCount} critical\n`)
201
+ : "") +
202
+ (highCount > 0 ? chalk.yellow(` 🟡 ${highCount} high risk\n`) : "") +
203
+ chalk.gray(" Run logcop fix to remove them"),
204
+ { padding: 1, borderColor: criticalCount > 0 ? "red" : "yellow" },
205
+ ),
206
+
207
+ // ci mode exit 1 so pipelines fail
208
+ );
209
+ if (ci && results.length > 0) {
210
+ process.exit(1);
211
+ }
212
+ }
213
+ module.exports = { scanProject, parseFile };