src / riskScanner.ts
export type RiskLevel = "low" | "medium" | "high" | "blocked";
export type RiskResult = {
riskLevel: RiskLevel;
reason?: string;
};
export function scanPowerShellRisk(script: string): RiskResult {
const normalized = script.toLowerCase();
const compact = normalized.replace(/\s+/g, " ");
const pathNormalized = compact.replace(/\//g, "\\");
const blockedPatterns: Array<[RegExp, string]> = [
[/\bclear-disk\b/, "Clear-Disk is blocked because it can wipe disks."],
[/\bformat-volume\b/, "Format-Volume is blocked because it can format volumes."],
[/\bremove-partition\b/, "Remove-Partition is blocked because it can remove disk partitions."],
[/\bbcdedit\b/, "bcdedit is blocked because it can alter boot configuration."],
[/\bcipher\s+\/w\b/, "cipher /w is blocked because it wipes free space."],
[/\bdisable-netfirewallprofile\b/, "Disable-NetFirewallProfile is blocked because it disables the firewall."],
[
/\bset-mppreference\b[^\n;|&]*\s-disablerealtimemonitoring\b(?:\s+\$?true)?/,
"Set-MpPreference -DisableRealtimeMonitoring is blocked because it disables Defender real-time monitoring.",
],
[/\bmimikatz\b/, "mimikatz references are blocked as credential dumping indicators."],
[/\blsass\b/, "lsass access references are blocked as credential dumping indicators."],
[/\bntds\.dit\b/, "ntds.dit access is blocked as a credential-store indicator."],
[
/(hklm:\\sam|hkey_local_machine\\sam|\\sam\b|system32\\config\\sam|\breg\s+save\b[^\n;|&]*\bsam\b)/,
"SAM credential-store access/export patterns are blocked.",
],
];
for (const [pattern, reason] of blockedPatterns) {
if (pattern.test(pathNormalized)) {
return { riskLevel: "blocked", reason };
}
}
if (isRecursiveForceRemovalAtDangerousRoot(pathNormalized)) {
return {
riskLevel: "blocked",
reason:
"Recursive force deletion at or near a drive root, Windows directory, Users directory, user profile, $HOME, or ~ is blocked.",
};
}
if (hasRemovalCommand(compact) && hasRecurseAndForce(compact)) {
return {
riskLevel: "high",
reason: "Remove-Item with both -Recurse and -Force requires explicit confirmation.",
};
}
const highPatterns: Array<[RegExp, string]> = [
[/\bset-executionpolicy\b/, "Set-ExecutionPolicy requires explicit confirmation."],
[/\bstop-service\b/, "Stop-Service requires explicit confirmation."],
[/\brestart-service\b/, "Restart-Service requires explicit confirmation."],
[/\bnew-localuser\b/, "New-LocalUser requires explicit confirmation."],
[/\badd-localgroupmember\b/, "Add-LocalGroupMember requires explicit confirmation."],
[/\bregister-scheduledtask\b/, "Register-ScheduledTask requires explicit confirmation."],
[/\bnew-scheduledtask\b/, "New-ScheduledTask requires explicit confirmation."],
[/\bset-mppreference\b/, "Set-MpPreference requires explicit confirmation."],
[
/\b(new-item|set-content|add-content|copy-item|move-item|new-itemproperty|set-itemproperty|reg\s+add)\b[^\n;|&]*(hklm:\\|hkey_local_machine\\|hklm\\)/,
"Registry writes under HKLM require explicit confirmation.",
],
[/shell:startup\b/, "Startup folder writes require explicit confirmation."],
[/\\microsoft\\windows\\start menu\\programs\\startup\\/, "Startup folder writes require explicit confirmation."],
];
for (const [pattern, reason] of highPatterns) {
if (pattern.test(pathNormalized)) {
return { riskLevel: "high", reason };
}
}
const mediumPatterns: Array<[RegExp, string]> = [
[/\binvoke-webrequest\b/, "Network download command detected."],
[/\bcurl\b/, "Network download command detected."],
[/\bwget\b/, "Network download command detected."],
[/\bstart-bitstransfer\b/, "Network transfer command detected."],
[/\bnpm\s+install\b/, "Package installation command detected."],
[/\bwinget\s+install\b/, "Package installation command detected."],
[/\bchoco\s+install\b/, "Package installation command detected."],
[/\bscoop\s+install\b/, "Package installation command detected."],
[/\bnew-item\b/, "File or directory creation command detected."],
[/\bset-content\b/, "File write command detected."],
[/\badd-content\b/, "File append command detected."],
[/\bcopy-item\b/, "File copy command detected."],
[/\bmove-item\b/, "File move command detected."],
[/\bstop-process\b/, "Process termination command detected."],
[/\b(remove-item|rm|del|erase|rmdir|rd)\b/, "Deletion command detected."],
];
for (const [pattern, reason] of mediumPatterns) {
if (pattern.test(compact)) {
return { riskLevel: "medium", reason };
}
}
return { riskLevel: "low" };
}
function isRecursiveForceRemovalAtDangerousRoot(command: string): boolean {
if (!hasRemovalCommand(command) || !hasRecurseAndForce(command)) {
return false;
}
const userProfile = process.env.USERPROFILE?.toLowerCase().replace(/\//g, "\\");
const dangerousPathPatterns = [
/(^|[\s"'=])([a-z]:)?\\($|[\s"';])/,
/(^|[\s"'=])[a-z]:\\($|[\s"';])/,
/(^|[\s"'=])[a-z]:\\windows($|\\|[\s"';])/,
/(^|[\s"'=])[a-z]:\\users($|\\|[\s"';])/,
/(^|[\s"'=])\$env:systemdrive\\($|[\s"';])/,
/(^|[\s"'=])\$env:userprofile($|\\|[\s"';])/,
/(^|[\s"'=])%userprofile%($|\\|[\s"';])/,
/(^|[\s"'=])\$home($|\\|[\s"';])/,
/(^|[\s"'=])~($|\\|[\s"';])/,
];
if (userProfile) {
dangerousPathPatterns.push(new RegExp(`(^|[\\s"'=])${escapeRegExp(userProfile)}($|\\\\|[\\s"';])`));
}
return dangerousPathPatterns.some((pattern) => pattern.test(command));
}
function hasRemovalCommand(command: string): boolean {
return /\b(remove-item|rm|del|erase|rmdir|rd)\b/.test(command);
}
function hasRecurseAndForce(command: string): boolean {
return /(-recurse\b|-r\b|\/s\b)/.test(command) && /(-force\b|-fo\b|\/q\b)/.test(command);
}
function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}