z-clean 0.3.1 → 0.3.3
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.ko.md +25 -3
- package/README.md +24 -7
- package/README.zh.md +25 -3
- package/bin/zclean.js +36 -40
- package/package.json +5 -3
- package/scripts/postinstall.js +7 -0
- package/src/cli-brand.js +129 -0
- package/src/commands/init.js +50 -0
- package/src/commands/scheduler.js +27 -23
- package/src/config.js +24 -14
- package/src/detector/custom-patterns.js +79 -0
- package/src/detector/patterns.js +30 -2
- package/src/installer/launchd.js +64 -15
- package/src/installer/systemd.js +39 -20
- package/src/installer/taskscheduler.js +17 -6
- package/src/killer.js +11 -2
- package/src/process-diagnostic.js +17 -0
- package/src/reporter.js +10 -8
- package/src/scanner.js +6 -2
- package/src/storage-security.js +65 -0
- package/src/terminal-text.js +11 -0
- package/src/windows-processes.js +37 -12
package/src/scanner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { ProcessTree } = require('./process-tree');
|
|
4
|
-
const {
|
|
4
|
+
const { createPatternMatcher, AI_DIR_REGEX, buildAiDirRegex } = require('./detector/patterns');
|
|
5
5
|
const { isWhitelisted } = require('./detector/whitelist');
|
|
6
6
|
const { parseDuration, parseMemory } = require('./config');
|
|
7
7
|
|
|
@@ -18,6 +18,7 @@ const { parseDuration, parseMemory } = require('./config');
|
|
|
18
18
|
function scan(config, opts = {}) {
|
|
19
19
|
const tree = ProcessTree.build();
|
|
20
20
|
const aiDirRegex = buildAiDirRegex(config.customAiDirs);
|
|
21
|
+
const matchConfiguredPattern = createPatternMatcher(config);
|
|
21
22
|
const zombies = [];
|
|
22
23
|
const warnings = Array.isArray(tree.warnings) ? [...tree.warnings] : [];
|
|
23
24
|
const errors = Array.isArray(tree.errors) ? [...tree.errors] : [];
|
|
@@ -35,7 +36,7 @@ function scan(config, opts = {}) {
|
|
|
35
36
|
// Iterate all processes in the tree
|
|
36
37
|
for (const [, proc] of tree.byPid) {
|
|
37
38
|
// Match against known AI tool patterns
|
|
38
|
-
const pattern =
|
|
39
|
+
const pattern = matchConfiguredPattern(proc.cmd);
|
|
39
40
|
if (!pattern) continue;
|
|
40
41
|
|
|
41
42
|
// AI path filter: skip generic patterns if command isn't in an AI tool directory
|
|
@@ -47,6 +48,8 @@ function scan(config, opts = {}) {
|
|
|
47
48
|
? tree.hasAncestorMatching(proc.pid, (p) => p.pid === sessionPid)
|
|
48
49
|
: false;
|
|
49
50
|
|
|
51
|
+
if (pattern.strictOrphan && !orphanResult.isOrphan) continue;
|
|
52
|
+
|
|
50
53
|
// If pattern requires orphan status and process isn't orphaned, skip
|
|
51
54
|
if (pattern.orphanOnly && !orphanResult.isOrphan && !sessionRelated) continue;
|
|
52
55
|
|
|
@@ -90,6 +93,7 @@ function scan(config, opts = {}) {
|
|
|
90
93
|
startTime: proc.startTime,
|
|
91
94
|
reason: reasons.join(', '),
|
|
92
95
|
pattern: pattern.name,
|
|
96
|
+
matchLiteral: pattern.literal || null,
|
|
93
97
|
});
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
const SENSITIVE_HISTORY_FIELDS = ['args', 'argv', 'cmd', 'command', 'commandLine'];
|
|
6
|
+
|
|
7
|
+
function secureDirectory(directory) {
|
|
8
|
+
if (!fs.existsSync(directory)) {
|
|
9
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
10
|
+
}
|
|
11
|
+
if (process.platform !== 'win32') fs.chmodSync(directory, 0o700);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function writePrivateFile(file, content) {
|
|
15
|
+
fs.writeFileSync(file, content, { encoding: 'utf-8', mode: 0o600 });
|
|
16
|
+
secureFile(file);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function appendPrivateFile(file, content) {
|
|
20
|
+
fs.appendFileSync(file, content, { encoding: 'utf-8', mode: 0o600 });
|
|
21
|
+
secureFile(file);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function secureFile(file) {
|
|
25
|
+
if (process.platform !== 'win32') fs.chmodSync(file, 0o600);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sanitizeHistoryEntry(entry) {
|
|
29
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return entry;
|
|
30
|
+
const sanitized = { ...entry };
|
|
31
|
+
for (const field of SENSITIVE_HISTORY_FIELDS) delete sanitized[field];
|
|
32
|
+
return sanitized;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sanitizeHistoryFile(file) {
|
|
36
|
+
if (!fs.existsSync(file)) return;
|
|
37
|
+
|
|
38
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
39
|
+
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
40
|
+
let changed = false;
|
|
41
|
+
const sanitized = lines.map((line) => {
|
|
42
|
+
try {
|
|
43
|
+
const next = JSON.stringify(sanitizeHistoryEntry(JSON.parse(line)));
|
|
44
|
+
if (next !== line) changed = true;
|
|
45
|
+
return next;
|
|
46
|
+
} catch {
|
|
47
|
+
return line;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (changed) {
|
|
52
|
+
writePrivateFile(file, sanitized.join('\n') + (sanitized.length ? '\n' : ''));
|
|
53
|
+
} else {
|
|
54
|
+
secureFile(file);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
appendPrivateFile,
|
|
60
|
+
sanitizeHistoryEntry,
|
|
61
|
+
sanitizeHistoryFile,
|
|
62
|
+
secureDirectory,
|
|
63
|
+
secureFile,
|
|
64
|
+
writePrivateFile,
|
|
65
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function sanitizeTerminalText(value) {
|
|
4
|
+
return String(value ?? '')
|
|
5
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
6
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
7
|
+
.replace(/[\r\n]+/g, ' ')
|
|
8
|
+
.replace(/[\p{Cc}\p{Cf}]/gu, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { sanitizeTerminalText };
|
package/src/windows-processes.js
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { providerDiagnostic } = require('./process-diagnostic');
|
|
4
|
+
|
|
3
5
|
function normalizePid(pid) {
|
|
4
6
|
const parsed = Number(pid);
|
|
5
7
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
function readWMICProcesses(runtime) {
|
|
11
|
+
try {
|
|
12
|
+
runtime.execSync('where wmic', {
|
|
13
|
+
encoding: 'utf-8',
|
|
14
|
+
timeout: 5000,
|
|
15
|
+
maxBuffer: 1024 * 1024,
|
|
16
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
17
|
+
});
|
|
18
|
+
} catch {
|
|
19
|
+
return { processes: [], warnings: [] };
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
let output;
|
|
10
23
|
try {
|
|
11
24
|
output = runtime.execSync(
|
|
12
25
|
'wmic process get ProcessId,ParentProcessId,CommandLine,WorkingSetSize,CreationDate /format:csv',
|
|
13
|
-
{
|
|
26
|
+
{
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
timeout: 15000,
|
|
29
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
30
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
|
+
}
|
|
14
32
|
);
|
|
15
33
|
} catch (err) {
|
|
16
34
|
return {
|
|
@@ -35,7 +53,12 @@ function readCIMProcesses(runtime) {
|
|
|
35
53
|
try {
|
|
36
54
|
output = runtime.execSync(
|
|
37
55
|
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,WorkingSetSize,CreationDate | ConvertTo-Json -Compress"',
|
|
38
|
-
{
|
|
56
|
+
{
|
|
57
|
+
encoding: 'utf-8',
|
|
58
|
+
timeout: 15000,
|
|
59
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
60
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
61
|
+
}
|
|
39
62
|
);
|
|
40
63
|
} catch (err) {
|
|
41
64
|
return {
|
|
@@ -63,7 +86,12 @@ function readWindowsProcess(pid, runtime) {
|
|
|
63
86
|
try {
|
|
64
87
|
output = runtime.execSync(
|
|
65
88
|
`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process -Filter 'ProcessId = ${safePid}' | Select-Object ProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress"`,
|
|
66
|
-
{
|
|
89
|
+
{
|
|
90
|
+
encoding: 'utf-8',
|
|
91
|
+
timeout: 5000,
|
|
92
|
+
maxBuffer: 1024 * 1024,
|
|
93
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
94
|
+
}
|
|
67
95
|
);
|
|
68
96
|
} catch {
|
|
69
97
|
return null;
|
|
@@ -87,7 +115,12 @@ function windowsProcessExists(pid, runtime) {
|
|
|
87
115
|
try {
|
|
88
116
|
const output = runtime.execSync(
|
|
89
117
|
`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process -Filter 'ProcessId = ${safePid}' | Select-Object ProcessId | ConvertTo-Json -Compress"`,
|
|
90
|
-
{
|
|
118
|
+
{
|
|
119
|
+
encoding: 'utf-8',
|
|
120
|
+
timeout: 3000,
|
|
121
|
+
maxBuffer: 1024 * 1024,
|
|
122
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
123
|
+
}
|
|
91
124
|
);
|
|
92
125
|
return parseJsonRows(String(output || '')).some((row) => Number(readField(row, 'ProcessId')) === safePid);
|
|
93
126
|
} catch {
|
|
@@ -238,14 +271,6 @@ function parseCsvLine(line) {
|
|
|
238
271
|
return parts;
|
|
239
272
|
}
|
|
240
273
|
|
|
241
|
-
function providerDiagnostic(provider, code, err) {
|
|
242
|
-
return {
|
|
243
|
-
code,
|
|
244
|
-
provider,
|
|
245
|
-
message: err instanceof Error ? err.message : String(err || ''),
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
274
|
module.exports = {
|
|
250
275
|
normalizePid,
|
|
251
276
|
readCIMProcesses,
|