minAncestryDepth: number;
constructor(config: { allowedBranches: string[]; minAncestryDepth: number }) {
this.allowedBranches = config.allowedBranches;
this.minAncestryDepth = config.minAncestryDepth;
}
validatePushEvent(ref: string, commitHash: string): CommitValidationResult {
if (!this.allowedBranches.some(branch => ref.includes(branch))) {
return { valid: false, reason: 'Push target not in allowed branches', ancestryDepth: 0 };
}
try {
const ancestryOutput = execSync(
`git rev-list --count ${commitHash}^..HEAD`,
{ encoding: 'utf-8' }
).trim();
const depth = parseInt(ancestryOutput, 10);
if (depth < this.minAncestryDepth) {
return { valid: false, reason: 'Orphan or shallow commit detected', ancestryDepth: depth };
}
return { valid: true, reason: 'Valid ancestry chain', ancestryDepth: depth };
} catch {
return { valid: false, reason: 'Failed to resolve commit ancestry', ancestryDepth: 0 };
}
}
}
**Architecture Rationale:** Validating ancestry at the CI entry point prevents orphan commits from triggering builds. The `minAncestryDepth` threshold ensures only commits with established parent history proceed. This blocks the initial attack vector without impacting normal PR merges.
### Step 2: Audit Lifecycle Hooks Across Dependencies
Malicious payloads in this attack family rely on `preinstall` or `postinstall` hooks to execute obfuscated decryption routines before the package is fully resolved. Static lockfile scanning misses these hooks if they are introduced dynamically during publish.
```typescript
// lifecycle-hook-auditor.ts
import { readFileSync } from 'fs';
import { join } from 'path';
interface HookAuditReport {
packageName: string;
version: string;
hasPreinstall: boolean;
hasPostinstall: boolean;
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH';
}
export class LifecycleHookAuditor {
private readonly lockfilePath: string;
constructor(lockfilePath: string) {
this.lockfilePath = lockfilePath;
}
scanDependencies(): HookAuditReport[] {
const lockfile = JSON.parse(readFileSync(this.lockfilePath, 'utf-8'));
const packages = lockfile.packages || {};
const reports: HookAuditReport[] = [];
for (const [pkgPath, pkgData] of Object.entries(packages)) {
const name = pkgPath.split('/').pop() || 'unknown';
const version = (pkgData as any).version || 'unknown';
const scripts = (pkgData as any).dependencies?.scripts || {};
const hasPreinstall = 'preinstall' in scripts;
const hasPostinstall = 'postinstall' in scripts;
const riskLevel = hasPreinstall ? 'HIGH' : hasPostinstall ? 'MEDIUM' : 'LOW';
reports.push({ packageName: name, version, hasPreinstall, hasPostinstall, riskLevel });
}
return reports;
}
generateBaseline(reports: HookAuditReport[]): Map<string, string> {
const baseline = new Map<string, string>();
for (const report of reports) {
baseline.set(
`${report.packageName}@${report.version}`,
`${report.hasPreinstall ? 'P' : '-'}${report.hasPostinstall ? 'I' : '-'}`
);
}
return baseline;
}
}
Architecture Rationale: Hook execution order in npm is deterministic but often overlooked. preinstall runs before node_modules is fully populated, making it ideal for payload delivery. By scanning the lockfile and maintaining a baseline, teams can detect sudden hook introductions across hundreds of dependencies in milliseconds.
The malware in this incident included a dead man's switch that wipes the user's home directory if stolen tokens are revoked before persistence artifacts are removed. Premature revocation triggers destructive cleanup.
// safe-remediation-runner.ts
import { existsSync, unlinkSync, readdirSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
interface PersistenceArtifact {
name: string;
path: string;
type: 'systemd' | 'launchd' | 'config';
}
export class SafeRemediationRunner {
private readonly persistenceTargets: PersistenceArtifact[] = [
{ name: 'kitty-monitor.service', path: join(homedir(), '.config', 'systemd', 'user'), type: 'systemd' },
{ name: 'com.user.kitty-monitor.plist', path: join(homedir(), 'Library', 'LaunchAgents'), type: 'launchd' },
{ name: '.claude', path: homedir(), type: 'config' },
{ name: '.copilot', path: homedir(), type: 'config' },
{ name: '.gemini', path: homedir(), type: 'config' }
];
async executeSafeCleanup(): Promise<{ success: boolean; removed: string[]; warnings: string[] }> {
const removed: string[] = [];
const warnings: string[] = [];
for (const target of this.persistenceTargets) {
const fullPath = join(target.path, target.name);
if (existsSync(fullPath)) {
try {
if (target.type === 'systemd') {
// Disable user service before removal
require('child_process').execSync(`systemctl --user stop ${target.name}`);
}
unlinkSync(fullPath);
removed.push(target.name);
} catch (err) {
warnings.push(`Failed to remove ${target.name}: ${(err as Error).message}`);
}
}
}
return { success: warnings.length === 0, removed, warnings };
}
}
Architecture Rationale: Remediation must precede credential revocation. The dead man's switch monitors for token invalidation and triggers destructive routines if persistence remains active. By systematically disabling services and removing configuration hooks before revoking tokens, teams neutralize the trigger mechanism and prevent data loss.
Pitfall Guide
1. Provenance Complacency
Explanation: Teams treat valid SLSA attestations as absolute proof of safety. When a pipeline is weaponized, the attestation remains cryptographically valid because the build environment was not tampered with; it was legitimately triggered by an attacker.
Fix: Treat provenance as a baseline verification step, not a security guarantee. Combine attestation checks with behavioral delta monitoring and commit ancestry validation.
2. Premature Token Revocation
Explanation: Revoking compromised credentials immediately after detection triggers the malware's dead man's switch, which wipes the home directory or destroys critical configuration files.
Fix: Always audit and remove persistence artifacts (kitty-monitor.service, com.user.kitty-monitor.plist, AI assistant config injections) before revoking tokens or rotating secrets.
3. Ignoring Orphan Commit Topology
Explanation: CI/CD pipelines typically trigger on any push event, including orphan commits that lack parent history. Attackers exploit this to bypass code review and trigger builds with malicious source code.
Fix: Implement commit ancestry validation at the CI entry point. Reject pushes that do not meet minimum ancestry depth or originate from unexpected refs.
4. Lifecycle Hook Blindness
Explanation: preinstall and postinstall hooks execute automatically during package installation. Many security scanners ignore lockfile scripts, assuming they are benign configuration metadata.
Fix: Scan lockfiles for unexpected lifecycle hooks. Maintain a baseline of known hook patterns and alert on sudden introductions, especially in packages that previously had none.
5. Overly Broad OIDC Permissions
Explanation: GitHub Actions workflows configured with id-token: write without branch or commit constraints allow any push to generate OIDC tokens for publishing. This grants attackers direct access to package registries.
Fix: Scope OIDC permissions to specific branches, require PR merge events, and enforce environment protection rules. Use conditional permissions that only activate after successful code review.
6. Static-Only Scanning Reliance
Explanation: Tools that only check static attributes (provenance, scorecard, download volume) miss behavioral changes. The compromised packages scored highly on static metrics but exhibited clear behavioral anomalies.
Fix: Implement continuous delta monitoring. Track publish cadence, artifact size changes, hook introductions, and commit topology shifts. Alert on deviations from established baselines.
7. Exfiltration Traffic Misclassification
Explanation: The malware disguised credential theft as requests to api.anthropic.com/v1/api. Network security tools often whitelist AI API endpoints, allowing exfiltration to bypass detection.
Fix: Implement strict egress filtering for package installation environments. Block outbound connections during preinstall/postinstall execution. Log and alert on unexpected API calls from build or install contexts.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-volume dependency graph with frequent updates | Behavioral delta monitoring + lockfile hook scanning | Static attestation checks miss sudden lifecycle changes and batch publish anomalies | Low (automated scanning, minimal CI overhead) |
| Strict compliance environment requiring cryptographic proof | SLSA provenance verification + commit ancestry validation | Provenance satisfies audit requirements; ancestry validation blocks orphan commit triggers | Medium (requires CI pipeline modifications) |
| Active incident with suspected pipeline weaponization | Safe remediation workflow + egress filtering | Premature token revocation triggers destructive cleanup; network controls block exfiltration | High (requires incident response coordination) |
| Internal monorepo with controlled CI/CD | OIDC permission scoping + environment protection | Limits blast radius by restricting token generation to approved refs and merge events | Low (native GitHub Actions configuration) |
Configuration Template
# .github/workflows/supply-chain-gate.yml
name: Supply Chain Validation Gate
on:
push:
branches: [main, release/**]
pull_request:
branches: [main]
permissions:
contents: read
id-token: write
jobs:
validate-and-build:
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate commit ancestry
run: |
ANCESTRY_DEPTH=$(git rev-list --count HEAD^..HEAD)
if [ "$ANCESTRY_DEPTH" -lt 2 ]; then
echo "::error::Orphan or shallow commit detected. Ancestry depth: $ANCESTRY_DEPTH"
exit 1
fi
- name: Scan lifecycle hooks
run: |
npx --yes @internal/hook-auditor scan --lockfile package-lock.json --baseline .hook-baseline.json
if [ $? -ne 0 ]; then
echo "::error::Unexpected lifecycle hooks detected"
exit 1
fi
- name: Build and publish
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
npm ci
npm run build
npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Quick Start Guide
- Install the hook auditor: Run
npm install --save-dev @internal/hook-auditor in your project root. Generate a baseline with npx hook-auditor baseline --lockfile package-lock.json.
- Add ancestry validation: Insert the commit ancestry check step into your CI pipeline before any build or publish actions. Set
fetch-depth: 0 to ensure full history is available.
- Scope OIDC permissions: Update your GitHub Actions workflow to restrict
id-token: write to specific branches and require environment approval. Remove unconditional OIDC grants.
- Deploy delta monitoring: Configure your CI to run the hook auditor on every push. Compare results against the baseline and block merges if unexpected
preinstall or postinstall scripts appear.
- Prepare remediation runbook: Document the safe cleanup sequence: disable services β remove persistence files β verify AI assistant configs β revoke tokens. Store this runbook in your incident response repository.