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/src/scanner.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { ProcessTree } = require('./process-tree');
4
- const { matchPattern, AI_DIR_REGEX, buildAiDirRegex } = require('./detector/patterns');
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 = matchPattern(proc.cmd, config);
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 };
@@ -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
- { encoding: 'utf-8', timeout: 15000, maxBuffer: 10 * 1024 * 1024 }
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
- { encoding: 'utf-8', timeout: 15000, maxBuffer: 10 * 1024 * 1024 }
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
- { encoding: 'utf-8', timeout: 5000, maxBuffer: 1024 * 1024 }
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
- { encoding: 'utf-8', timeout: 3000, maxBuffer: 1024 * 1024 }
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,