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 +286 -0
- package/bin/logcop.js +108 -0
- package/package.json +38 -0
- package/src/core/config.js +81 -0
- package/src/core/fixer.js +179 -0
- package/src/core/scanner.js +213 -0
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 };
|