Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 12 additions & 35 deletions dist/bin/cc-safety-net.js
Original file line number Diff line number Diff line change
Expand Up @@ -2865,6 +2865,7 @@ function normalizePathForComparison(p) {
}
var REASON_RM_RF = "rm -rf outside cwd is blocked. Use explicit paths within the current directory, or delete manually.";
var REASON_RM_RF_ROOT_HOME = "rm -rf targeting root or home directory is extremely dangerous and always blocked.";
var REASON_RM_HOME_CWD = "rm -rf in home directory is dangerous. Change to a project directory first.";
function analyzeRm(tokens, options = {}) {
const {
cwd,
Expand Down Expand Up @@ -2921,18 +2922,16 @@ function classifyTarget(target, ctx) {
if (isDangerousRootOrHomeTarget(target)) {
return { kind: "root_or_home_target" };
}
const anchoredCwd = ctx.anchoredCwd;
if (anchoredCwd) {
if (isCwdSelfTarget(target, anchoredCwd)) {
return { kind: "cwd_self_target" };
}
}
if (isTempTarget(target, ctx.trustTmpdirVar)) {
return { kind: "temp_target" };
}
const anchoredCwd = ctx.anchoredCwd;
if (anchoredCwd) {
if (isCwdHomeForRmPolicy(anchoredCwd, ctx.homeDir)) {
return { kind: "root_or_home_target" };
return { kind: "home_cwd_target" };
}
if (isCwdSelfTarget(target, anchoredCwd)) {
return { kind: "cwd_self_target" };
}
if (isTargetWithinCwd(target, anchoredCwd, ctx.resolvedCwd ?? anchoredCwd)) {
return { kind: "within_anchored_cwd" };
Expand All @@ -2944,10 +2943,12 @@ function reasonForClassification(classification, ctx) {
switch (classification.kind) {
case "root_or_home_target":
return REASON_RM_RF_ROOT_HOME;
case "cwd_self_target":
return REASON_RM_RF;
case "temp_target":
return null;
case "home_cwd_target":
return REASON_RM_HOME_CWD;
case "cwd_self_target":
return REASON_RM_RF;
case "within_anchored_cwd":
if (ctx.paranoid) {
return `${REASON_RM_RF} (SAFETY_NET_PARANOID_RM enabled)`;
Expand Down Expand Up @@ -3069,14 +3070,6 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
return false;
}
}
function isHomeDirectory(cwd) {
const home = process.env.HOME ?? homedir3();
try {
return normalizePathForComparison(cwd) === normalizePathForComparison(home);
} catch {
return false;
}
}

// src/core/analyze/parallel.ts
var REASON_PARALLEL_RM = "parallel rm -rf with dynamic input is dangerous. Use explicit file list instead.";
Expand Down Expand Up @@ -3497,7 +3490,6 @@ function matchesBlockArgs(tokens, blockArgs, shortOpts) {
// src/core/analyze/segment.ts
var REASON_INTERPRETER_DANGEROUS = "Detected potentially dangerous command in interpreter code.";
var REASON_INTERPRETER_BLOCKED = "Interpreter one-liners are blocked in paranoid mode.";
var REASON_RM_HOME_CWD = "rm -rf in home directory is dangerous. Change to a project directory first.";
function deriveCwdContext(options) {
const cwdUnknown = options.effectiveCwd === null;
const cwdForRm = cwdUnknown ? undefined : options.effectiveCwd ?? options.cwd;
Expand Down Expand Up @@ -3561,11 +3553,6 @@ function analyzeSegment(tokens, depth, options) {
}
}
if (isRm) {
if (cwdForRm && isHomeDirectory(cwdForRm)) {
if (hasRecursiveForceFlags(stripped)) {
return REASON_RM_HOME_CWD;
}
}
const rmResult = analyzeRm(stripped, {
cwd: cwdForRm,
originalCwd,
Expand Down Expand Up @@ -4682,19 +4669,9 @@ function explainSegment(tokens, depth, options, steps) {
return { reason };
}
if (isRm) {
if (effectiveCwd && isHomeDirectory(effectiveCwd) && hasRecursiveForceFlags(strippedTokens)) {
const reason2 = "rm -rf in home directory is dangerous. Change to a project directory first.";
steps.push({
type: "rule-check",
ruleModule: "rules-rm.ts",
ruleFunction: "isHomeDirectory",
matched: true,
reason: reason2
});
return { reason: reason2 };
}
const reason = analyzeRm(strippedTokens, {
cwd: effectiveCwd ?? undefined,
cwd: cwdForRm,
originalCwd,
paranoid: options.paranoidRm,
allowTmpdirVar
});
Expand Down
1 change: 1 addition & 0 deletions dist/core/rules-rm.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export interface AnalyzeRmOptions {
tmpdirOverridden?: boolean;
}
export declare function analyzeRm(tokens: string[], options?: AnalyzeRmOptions): string | null;
/** @internal Exported for testing */
export declare function isHomeDirectory(cwd: string): boolean;
33 changes: 10 additions & 23 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1724,6 +1724,7 @@ function normalizePathForComparison(p) {
}
var REASON_RM_RF = "rm -rf outside cwd is blocked. Use explicit paths within the current directory, or delete manually.";
var REASON_RM_RF_ROOT_HOME = "rm -rf targeting root or home directory is extremely dangerous and always blocked.";
var REASON_RM_HOME_CWD = "rm -rf in home directory is dangerous. Change to a project directory first.";
function analyzeRm(tokens, options = {}) {
const {
cwd,
Expand Down Expand Up @@ -1780,18 +1781,16 @@ function classifyTarget(target, ctx) {
if (isDangerousRootOrHomeTarget(target)) {
return { kind: "root_or_home_target" };
}
const anchoredCwd = ctx.anchoredCwd;
if (anchoredCwd) {
if (isCwdSelfTarget(target, anchoredCwd)) {
return { kind: "cwd_self_target" };
}
}
if (isTempTarget(target, ctx.trustTmpdirVar)) {
return { kind: "temp_target" };
}
const anchoredCwd = ctx.anchoredCwd;
if (anchoredCwd) {
if (isCwdHomeForRmPolicy(anchoredCwd, ctx.homeDir)) {
return { kind: "root_or_home_target" };
return { kind: "home_cwd_target" };
}
if (isCwdSelfTarget(target, anchoredCwd)) {
return { kind: "cwd_self_target" };
}
if (isTargetWithinCwd(target, anchoredCwd, ctx.resolvedCwd ?? anchoredCwd)) {
return { kind: "within_anchored_cwd" };
Expand All @@ -1803,10 +1802,12 @@ function reasonForClassification(classification, ctx) {
switch (classification.kind) {
case "root_or_home_target":
return REASON_RM_RF_ROOT_HOME;
case "cwd_self_target":
return REASON_RM_RF;
case "temp_target":
return null;
case "home_cwd_target":
return REASON_RM_HOME_CWD;
case "cwd_self_target":
return REASON_RM_RF;
case "within_anchored_cwd":
if (ctx.paranoid) {
return `${REASON_RM_RF} (SAFETY_NET_PARANOID_RM enabled)`;
Expand Down Expand Up @@ -1928,14 +1929,6 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
return false;
}
}
function isHomeDirectory(cwd) {
const home = process.env.HOME ?? homedir();
try {
return normalizePathForComparison(cwd) === normalizePathForComparison(home);
} catch {
return false;
}
}

// src/core/analyze/parallel.ts
var REASON_PARALLEL_RM = "parallel rm -rf with dynamic input is dangerous. Use explicit file list instead.";
Expand Down Expand Up @@ -2356,7 +2349,6 @@ function matchesBlockArgs(tokens, blockArgs, shortOpts) {
// src/core/analyze/segment.ts
var REASON_INTERPRETER_DANGEROUS = "Detected potentially dangerous command in interpreter code.";
var REASON_INTERPRETER_BLOCKED = "Interpreter one-liners are blocked in paranoid mode.";
var REASON_RM_HOME_CWD = "rm -rf in home directory is dangerous. Change to a project directory first.";
function deriveCwdContext(options) {
const cwdUnknown = options.effectiveCwd === null;
const cwdForRm = cwdUnknown ? undefined : options.effectiveCwd ?? options.cwd;
Expand Down Expand Up @@ -2420,11 +2412,6 @@ function analyzeSegment(tokens, depth, options) {
}
}
if (isRm) {
if (cwdForRm && isHomeDirectory(cwdForRm)) {
if (hasRecursiveForceFlags(stripped)) {
return REASON_RM_HOME_CWD;
}
}
const rmResult = analyzeRm(stripped, {
cwd: cwdForRm,
originalCwd,
Expand Down
17 changes: 3 additions & 14 deletions src/bin/explain/segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { dangerousInText } from '@/core/analyze/dangerous-text';
import { analyzeFind } from '@/core/analyze/find';
import { containsDangerousCode, extractInterpreterCodeArg } from '@/core/analyze/interpreters';
import { analyzeParallel } from '@/core/analyze/parallel';
import { hasRecursiveForceFlags } from '@/core/analyze/rm-flags';
import {
REASON_INTERPRETER_BLOCKED,
REASON_INTERPRETER_DANGEROUS,
Expand All @@ -25,7 +24,7 @@ import { isTmpdirOverriddenToNonTemp } from '@/core/analyze/tmpdir';
import { analyzeXargs } from '@/core/analyze/xargs';
import { checkCustomRules } from '@/core/rules-custom';
import { analyzeGit } from '@/core/rules-git';
import { analyzeRm, isHomeDirectory } from '@/core/rules-rm';
import { analyzeRm } from '@/core/rules-rm';
import {
normalizeCommandToken,
splitShellCommands,
Expand Down Expand Up @@ -302,19 +301,9 @@ export function explainSegment(
}

if (isRm) {
if (effectiveCwd && isHomeDirectory(effectiveCwd) && hasRecursiveForceFlags(strippedTokens)) {
const reason = 'rm -rf in home directory is dangerous. Change to a project directory first.';
steps.push({
type: 'rule-check',
ruleModule: 'rules-rm.ts',
ruleFunction: 'isHomeDirectory',
matched: true,
reason,
});
return { reason };
}
const reason = analyzeRm(strippedTokens, {
cwd: effectiveCwd ?? undefined,
cwd: cwdForRm,
originalCwd,
paranoid: options.paranoidRm,
allowTmpdirVar,
});
Expand Down
10 changes: 1 addition & 9 deletions src/core/analyze/segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { DISPLAY_COMMANDS } from '@/core/analyze/constants';
import { analyzeFind } from '@/core/analyze/find';
import { containsDangerousCode, extractInterpreterCodeArg } from '@/core/analyze/interpreters';
import { analyzeParallel } from '@/core/analyze/parallel';
import { hasRecursiveForceFlags } from '@/core/analyze/rm-flags';
import { extractDashCArg } from '@/core/analyze/shell-wrappers';
import { isTmpdirOverriddenToNonTemp } from '@/core/analyze/tmpdir';
import { analyzeXargs } from '@/core/analyze/xargs';
import { checkCustomRules } from '@/core/rules-custom';
import { analyzeGit } from '@/core/rules-git';
import { analyzeRm, isHomeDirectory } from '@/core/rules-rm';
import { analyzeRm } from '@/core/rules-rm';
import {
getBasename,
normalizeCommandToken,
Expand All @@ -27,8 +26,6 @@ import {
export const REASON_INTERPRETER_DANGEROUS =
'Detected potentially dangerous command in interpreter code.';
export const REASON_INTERPRETER_BLOCKED = 'Interpreter one-liners are blocked in paranoid mode.';
const REASON_RM_HOME_CWD =
'rm -rf in home directory is dangerous. Change to a project directory first.';

export type InternalOptions = AnalyzeOptions & {
config: Config;
Expand Down Expand Up @@ -123,11 +120,6 @@ export function analyzeSegment(
}

if (isRm) {
if (cwdForRm && isHomeDirectory(cwdForRm)) {
if (hasRecursiveForceFlags(stripped)) {
return REASON_RM_HOME_CWD;
}
}
const rmResult = analyzeRm(stripped, {
cwd: cwdForRm,
originalCwd,
Expand Down
26 changes: 15 additions & 11 deletions src/core/rules-rm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const REASON_RM_RF =
'rm -rf outside cwd is blocked. Use explicit paths within the current directory, or delete manually.';
const REASON_RM_RF_ROOT_HOME =
'rm -rf targeting root or home directory is extremely dangerous and always blocked.';
const REASON_RM_HOME_CWD =
'rm -rf in home directory is dangerous. Change to a project directory first.';

export interface AnalyzeRmOptions {
cwd?: string;
Expand All @@ -55,8 +57,9 @@ interface RmContext {

type TargetClassification =
| { kind: 'root_or_home_target' }
| { kind: 'cwd_self_target' }
| { kind: 'temp_target' }
| { kind: 'home_cwd_target' }
| { kind: 'cwd_self_target' }
| { kind: 'within_anchored_cwd' }
| { kind: 'outside_anchored_cwd' };

Expand Down Expand Up @@ -127,20 +130,18 @@ function classifyTarget(target: string, ctx: RmContext): TargetClassification {
return { kind: 'root_or_home_target' };
}

const anchoredCwd = ctx.anchoredCwd;
if (anchoredCwd) {
if (isCwdSelfTarget(target, anchoredCwd)) {
return { kind: 'cwd_self_target' };
}
}

if (isTempTarget(target, ctx.trustTmpdirVar)) {
return { kind: 'temp_target' };
}

const anchoredCwd = ctx.anchoredCwd;
if (anchoredCwd) {
if (isCwdHomeForRmPolicy(anchoredCwd, ctx.homeDir)) {
return { kind: 'root_or_home_target' };
return { kind: 'home_cwd_target' };
}

if (isCwdSelfTarget(target, anchoredCwd)) {
return { kind: 'cwd_self_target' };
}

if (isTargetWithinCwd(target, anchoredCwd, ctx.resolvedCwd ?? anchoredCwd)) {
Expand All @@ -158,10 +159,12 @@ function reasonForClassification(
switch (classification.kind) {
case 'root_or_home_target':
return REASON_RM_RF_ROOT_HOME;
case 'cwd_self_target':
return REASON_RM_RF;
case 'temp_target':
return null;
case 'home_cwd_target':
return REASON_RM_HOME_CWD;
case 'cwd_self_target':
return REASON_RM_RF;
case 'within_anchored_cwd':
if (ctx.paranoid) {
return `${REASON_RM_RF} (SAFETY_NET_PARANOID_RM enabled)`;
Expand Down Expand Up @@ -319,6 +322,7 @@ function isTargetWithinCwd(target: string, originalCwd: string, effectiveCwd?: s
}
}

/** @internal Exported for testing */
export function isHomeDirectory(cwd: string): boolean {
const home = process.env.HOME ?? homedir();
try {
Expand Down
17 changes: 14 additions & 3 deletions tests/bin/explain/command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,23 @@ describe('explainCommand rm with home directory', () => {
const allSteps = result.trace.segments.flatMap((s) => s.steps);
const ruleStep = allSteps.find(
(s) =>
s.type === 'rule-check' &&
s.ruleModule === 'rules-rm.ts' &&
s.ruleFunction === 'isHomeDirectory',
s.type === 'rule-check' && s.ruleModule === 'rules-rm.ts' && s.ruleFunction === 'analyzeRm',
);
expect(ruleStep).toBeDefined();
});

test('temp-target rm in home directory cwd is allowed', () => {
const homeDir = process.env.HOME;
if (!homeDir) return;
const result = explainCommand('rm -rf /tmp/test-dir', { cwd: homeDir });
expect(result.result).toBe('allowed');
const allSteps = result.trace.segments.flatMap((s) => s.steps);
const analyzeRmStep = allSteps.find(
(s) =>
s.type === 'rule-check' && s.ruleModule === 'rules-rm.ts' && s.ruleFunction === 'analyzeRm',
);
expect(analyzeRmStep).toBeDefined();
});
});

describe('explainCommand parallel with nested blocked', () => {
Expand Down
Loading
Loading