Skip to content

Commit 9c06ea2

Browse files
committed
feat(scan): add risk analysis
1 parent 3d2810e commit 9c06ea2

6 files changed

Lines changed: 91 additions & 45 deletions

File tree

src/cli/cli.controller.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import colors from 'colors';
3434
import { homedir } from 'os';
3535
import path from 'path';
3636
import openExplorer from 'open-file-explorer';
37-
import { ScanOptions, Npkill } from '@core/index.js';
37+
import { ScanFoundFolder, Npkill } from '@core/index.js';
3838
import { LoggerService } from '@core/services/logger.service.js';
3939
import { ScanStatus } from '@core/interfaces/search-status.model.js';
4040
import { FileService } from '@core/services/files/files.service.js';
@@ -375,25 +375,27 @@ export class CliController {
375375

376376
const params = this.prepareListDirParams();
377377
const { rootPath } = params;
378-
const isExcludedDangerousDirectory = (path: string): boolean =>
379-
this.config.excludeHiddenDirectories &&
380-
this.fileService.isDangerous(path);
378+
const isExcludedDangerousDirectory = (
379+
scanResult: ScanFoundFolder,
380+
): boolean =>
381+
Boolean(
382+
this.config.excludeHiddenDirectories &&
383+
scanResult.riskAnalysis?.isSensitive,
384+
);
381385

382386
this.searchStart = Date.now();
383-
const results$ = this.npkill
384-
.startScan$(rootPath, params)
385-
.pipe(map((folder) => folder.path));
387+
const results$ = this.npkill.startScan$(rootPath, params);
386388
const nonExcludedResults$ = results$.pipe(
387389
filter((path) => !isExcludedDangerousDirectory(path)),
388390
);
389391

390392
nonExcludedResults$
391393
.pipe(
392-
map<string, CliScanFoundFolder>((path) => ({
394+
map<ScanFoundFolder, CliScanFoundFolder>(({ path, riskAnalysis }) => ({
393395
path,
394396
size: 0,
395397
modificationTime: -1,
396-
isDangerous: this.fileService.isDangerous(path),
398+
isDangerous: riskAnalysis?.isSensitive || false,
397399
status: 'live',
398400
})),
399401
tap((nodeFolder) => {

src/core/interfaces/file-service.interface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FileWorkerService } from '@core/services/files/files.worker.service.js';
2-
import { GetNewestFileResult } from '@core/interfaces';
2+
import { GetNewestFileResult, RiskAnalysis } from '@core/interfaces';
33
import { ScanOptions } from './folder.interface.js';
44
import { Observable } from 'rxjs';
55

@@ -15,7 +15,7 @@ export interface IFileService {
1515
convertGBToMB: (gb: number) => number;
1616
getFileContent: (path: string) => string;
1717
isSafeToDelete: (path: string, targetFolder: string) => boolean;
18-
isDangerous: (path: string) => boolean;
18+
isDangerous: (path: string) => RiskAnalysis;
1919
getRecentModificationInDir: (
2020
path: string,
2121
) => Promise<GetNewestFileResult | null>;

src/core/interfaces/folder.interface.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
export type SizeUnit = 'bytes' | 'kb' | 'mb' | 'gb';
22

3+
export interface RiskAnalysis {
4+
isSensitive: boolean;
5+
reason?: string;
6+
}
7+
38
export interface ScanFoundFolder {
49
path: string;
10+
riskAnalysis?: RiskAnalysis;
511
}
612

713
export interface ScanOptions {
814
target: string;
915
exclude?: string[];
1016
sortBy?: 'path' | 'size' | 'last-mod';
17+
performRiskAnalysis?: boolean; // Default: true
1118
// maxConcurrentScans?: number; // Need to implement this.
1219
}
1320

src/core/npkill.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,19 @@ export class Npkill implements NpkillInterface {
5252
}),
5353
mergeMap((dataFolder) => from(splitData(dataFolder))),
5454
filter((path) => path !== ''),
55-
map((path) => ({ path })),
55+
map((path) => {
56+
if (
57+
options.performRiskAnalysis !== undefined &&
58+
!options.performRiskAnalysis
59+
) {
60+
return { path };
61+
}
62+
const riskAnalysis = fileService.isDangerous(path);
63+
return {
64+
path,
65+
riskAnalysis,
66+
};
67+
}),
5668
tap((nodeFolder) =>
5769
this.logger.info(`Folder found: ${String(nodeFolder.path)}`),
5870
),

src/core/services/files/files.service.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
IFileService,
55
IFileStat,
66
GetNewestFileResult,
7+
RiskAnalysis,
78
} from '@core/index.js';
89
import fs, { accessSync, readFileSync, Stats, statSync } from 'fs';
910
import { readdir, stat } from 'fs/promises';
@@ -87,7 +88,7 @@ export abstract class FileService implements IFileService {
8788
* application-specific data or configurations that should not be tampered with. Deleting node_modules
8889
* from these locations could potentially disrupt the normal operation of these applications.
8990
*/
90-
isDangerous(originalPath: string): boolean {
91+
isDangerous(originalPath: string): RiskAnalysis {
9192
const absolutePath = path.resolve(originalPath);
9293
const normalizedPath = absolutePath.replace(/\\/g, '/').toLowerCase();
9394

@@ -104,31 +105,44 @@ export abstract class FileService implements IFileService {
104105

105106
if (isInHome) {
106107
if (/\/\.config(\/|$)/.test(normalizedPath)) {
107-
return true;
108+
return {
109+
isSensitive: true,
110+
reason: 'Contains user configuration data (~/.config)',
111+
};
108112
}
109113
if (/\/\.local\/share(\/|$)/.test(normalizedPath)) {
110-
return true;
111-
}
112-
if (/\/\.(cache|npm|pnpm)(\/|$)/.test(normalizedPath)) {
113-
return false;
114+
return {
115+
isSensitive: true,
116+
reason: 'User data folder (~/.local/share)',
117+
};
114118
}
119+
if (/\/\.(cache|npm|pnpm)(\/|$)/.test(normalizedPath))
120+
return { isSensitive: false };
115121
}
116122

123+
// macOs
117124
if (/\/applications\/[^/]+\.app\//.test(normalizedPath)) {
118-
return true;
125+
return { isSensitive: true, reason: 'Inside macOS .app package' };
119126
}
120127

128+
// Windows
121129
if (normalizedPath.includes('/appdata/roaming')) {
122-
return true;
130+
return {
131+
isSensitive: true,
132+
reason: 'Inside Windows AppData Roaming folder',
133+
};
123134
}
124135
if (normalizedPath.includes('/appdata/local')) {
125136
if (/\/\.(cache|npm|pnpm)(\/|$)/.test(normalizedPath)) {
126-
return false;
137+
return { isSensitive: false };
127138
}
128-
return true;
139+
return {
140+
isSensitive: true,
141+
reason: 'Inside Windows AppData Local folder',
142+
};
129143
}
130144
if (/program files( \(x86\))?\//.test(normalizedPath)) {
131-
return true;
145+
return { isSensitive: true, reason: 'Inside Program Files folder' };
132146
}
133147

134148
const segments = normalizedPath.split('/');
@@ -141,10 +155,10 @@ export abstract class FileService implements IFileService {
141155
);
142156

143157
if (hasUnsafeHiddenFolder) {
144-
return true;
158+
return { isSensitive: true, reason: 'Contains unsafe hidden folder' };
145159
}
146160

147-
return false;
161+
return { isSensitive: false };
148162
}
149163

150164
async getRecentModificationInDir(

tests/core/services/files/files.service.test.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -167,28 +167,33 @@ describe('File Service', () => {
167167

168168
test('safe relative path', () => {
169169
mockCwd('/safe/path');
170-
expect(fileService.isDangerous('../project/node_modules')).toBe(false);
170+
expect(
171+
fileService.isDangerous('../project/node_modules').isSensitive,
172+
).toBe(false);
171173
});
172174

173175
test('hidden relative path', () => {
174176
mockCwd('/home/user/projects');
175-
expect(fileService.isDangerous('../.config/secret')).toBe(true);
177+
expect(fileService.isDangerous('../.config/secret').isSensitive).toBe(
178+
true,
179+
);
176180
});
177181

178182
test('node_modules in ~/.cache', () => {
179183
expect(
180-
fileService.isDangerous('/home/user/.cache/project/node_modules'),
184+
fileService.isDangerous('/home/user/.cache/project/node_modules')
185+
.isSensitive,
181186
).toBe(false);
182187
});
183188

184189
test('parent relative path (..)', () => {
185190
mockCwd('/home/user');
186-
expect(fileService.isDangerous('..')).toBe(false);
191+
expect(fileService.isDangerous('..').isSensitive).toBe(false);
187192
});
188193

189194
test('current relative path (.)', () => {
190195
mockCwd('/home/user');
191-
expect(fileService.isDangerous('.')).toBe(false);
196+
expect(fileService.isDangerous('.').isSensitive).toBe(false);
192197
});
193198
});
194199

@@ -200,25 +205,29 @@ describe('File Service', () => {
200205

201206
test('safe relative path', () => {
202207
mockCwd('D:\\safe\\path');
203-
expect(fileService.isDangerous('..\\project\\node_modules')).toBe(
204-
false,
205-
);
208+
expect(
209+
fileService.isDangerous('..\\project\\node_modules').isSensitive,
210+
).toBe(false);
206211
});
207212

208213
test('AppData relative path', () => {
209214
mockCwd('C:\\Users\\user\\Documents');
210-
expect(fileService.isDangerous('..\\AppData\\Roaming\\app')).toBe(true);
215+
expect(
216+
fileService.isDangerous('..\\AppData\\Roaming\\app').isSensitive,
217+
).toBe(true);
211218
});
212219

213220
test('Program Files (x86)', () => {
214221
expect(
215-
fileService.isDangerous('C:\\Program Files (x86)\\app\\node_modules'),
222+
fileService.isDangerous('C:\\Program Files (x86)\\app\\node_modules')
223+
.isSensitive,
216224
).toBe(true);
217225
});
218226

219227
test('network paths', () => {
220228
expect(
221-
fileService.isDangerous('\\\\network\\share\\.hidden\\node_modules'),
229+
fileService.isDangerous('\\\\network\\share\\.hidden\\node_modules')
230+
.isSensitive,
222231
).toBe(true);
223232
});
224233
});
@@ -227,31 +236,33 @@ describe('File Service', () => {
227236
test('no home directory', () => {
228237
delete process.env.HOME;
229238
delete process.env.USERPROFILE;
230-
expect(fileService.isDangerous('/some/path')).toBe(false);
239+
expect(fileService.isDangerous('/some/path').isSensitive).toBe(false);
231240
});
232241

233242
test('whitelisted hidden segments', () => {
234243
expect(
235-
fileService.isDangerous('/tmp/.cache/project/node_modules'),
244+
fileService.isDangerous('/tmp/.cache/project/node_modules')
245+
.isSensitive,
246+
).toBe(false);
247+
expect(
248+
fileService.isDangerous('/tmp/.npm/project/node_modules').isSensitive,
236249
).toBe(false);
237-
expect(fileService.isDangerous('/tmp/.npm/project/node_modules')).toBe(
238-
false,
239-
);
240250
});
241251

242252
test('macOS application bundle', () => {
243253
expect(
244254
fileService.isDangerous(
245255
'/Applications/MyApp.app/Contents/node_modules',
246-
),
256+
).isSensitive,
247257
).toBe(true);
248258
});
249259

250260
test('Windows-style path on POSIX', () => {
251261
process.env.HOME = '/home/user';
252-
expect(fileService.isDangerous('/home/user/AppData/Local/.cache')).toBe(
253-
false,
254-
);
262+
expect(
263+
fileService.isDangerous('/home/user/AppData/Local/.cache')
264+
.isSensitive,
265+
).toBe(false);
255266
});
256267
});
257268
});

0 commit comments

Comments
 (0)