Back to KB
Difficulty
Intermediate
Read Time
9 min

Phantom Pulse RAT Hits Obsidian Plugins: How to Audit Dev Tool Supply Chains

By Codcompass Team··9 min read

Hardening the Plugin Surface: Auditing Third-Party Extensions in Developer Workspaces

Current Situation Analysis

Modern developer and productivity workspaces have converged on a plugin-driven architecture. Tools like Obsidian, VS Code, Cursor, JetBrains IDEs, Raycast, and Alfred all share a fundamental design choice: they treat third-party extensions as first-class citizens that execute within the host process. This architectural convenience creates a critical threat model gap. Teams routinely classify these applications as content viewers, note-taking utilities, or AI-assisted coding assistants. In reality, they are privileged code execution platforms.

The Phantom Pulse RAT incident exposed this gap at scale. A malicious Obsidian community plugin was distributed through the standard marketplace flow, bypassing the psychological security filters developers apply to traditional software downloads. Once installed, the plugin operated as a standard Node.js module with unrestricted filesystem access, network capabilities, and child-process spawning. The payload targeted high-value developer artifacts: SSH private keys, .env configuration files, browser session cookies, and markdown notes containing API tokens. The attack chain followed a predictable supply chain pattern: plausible metadata, delayed second-stage delivery, persistence installation across operating systems, and persistent command-and-control communication.

This vulnerability is not isolated to note-taking applications. The same dynamics have compromised npm, PyPI, the VS Code marketplace, and browser extension stores. The difference lies in proximity to sensitive material. When a developer installs a plugin, they are not just adding a feature; they are granting a remote script the same permissions as their terminal session. Most organizations lack visibility into this surface area because extension installation happens at the individual workstation level, outside of centralized package management or endpoint detection platforms. The implicit trust transferred from an official directory listing to unvetted community code remains the primary attack vector.

WOW Moment: Key Findings

The fundamental misalignment in how teams evaluate plugin ecosystems versus traditional software distribution creates a measurable security deficit. The table below contrasts the operational characteristics of standard desktop applications against plugin-driven extensions.

DimensionTraditional Desktop AppPlugin/Extension Ecosystem
Execution ContextSandboxed or user-promptedHost process privileges (full FS/network)
Distribution ReviewCode signing, notarization, manual reviewAutomated listing, minimal vetting
Update VerificationCryptographic signatures, changelog auditSilent background updates, diff rarely checked
Persistence MechanismInstaller prompts, OS-level controlsDirect write to LaunchAgents, systemd, Task Scheduler

This comparison reveals why conventional endpoint security strategies fail against plugin-based attacks. Traditional EDR solutions monitor binary execution and installer behavior. Plugin ecosystems bypass these controls by running interpreted code within a trusted host process. The attack surface shifts from the operating system to the application layer, where permission boundaries are virtually nonexistent. Recognizing this shift enables teams to implement application-level auditing, dependency scanning, and secret isolation strategies that actually address the root cause rather than treating symptoms.

Core Solution

Mitigating plugin supply chain risk requires shifting from reactive incident response to proactive surface area management. The following implementation outlines a TypeScript-based audit pipeline that inventory-scans installed extensions, evaluates third-party trust signals, and flags high-risk configurations.

Architecture Decisions

  1. Host-Agnostic Inventory Layer: Instead of hardcoding paths for specific tools, the scanner abstracts extension discovery through a provider interface. This allows the same audit logic to run against Obsidian vaults, VS Code/Cursor extension directories, and JetBrains plugin folders.
  2. GitHub Trust Signal Aggregation: Public repository metrics serve as the primary trust heuristic. We query commit frequency, contributor diversity, and recent activity windows. A repository with a single maintainer and dormant commits carries higher risk than a multi-contributor project with consistent release cycles.
  3. Local Secret Leakage Detection: Plugins often fail because developers store credentials in plaintext within the host application's data directory. The scanner includes a regex-based detector for common secret patterns, cross-referenced against known vault paths.
  4. Risk Scoring Engine: Each extension receives a weighted score based on trust signals, update recency, and local secret proximity. Scores below a configurable threshold trigger quarantine recommendations.

Implementation

import { Octokit } from "@octokit/rest";
import { execSync } from "child_process";
import { readdir, readFile, stat } from "fs/promises";
import { join, resolve } from "path";
import { createHash } from "crypto";

interface ExtensionMetadata {
  id: string;
  version: string;
  sourceUrl?: string;
  installPath: string;
}

interface TrustSignal {
  commitCount: number;
  contributorCount: number;
  lastActivityDays: number;
  hasCodeReview: boolean;
}

interface AuditResult {
  extensionId: string;
  riskScore: number;
  trustSignals: TrustSignal;
  secretExposure: boolean;
  recommendation: "ALLOW" | "REVIEW" | "QUARANTINE";
}

class ExtensionAuditor {
  private octokit: Octokit;
  private threshold: number;

  constructor(githubToken: string, riskThreshold: number = 60) {
    this.octokit = new Octokit({ auth: githubToken });
    this.threshold = riskThreshold;
  }

  async scanWorkspace(workspaceRoot: string): Promise<AuditResult[]> {
    const extensions = await this.discoverExtensions(workspaceRoot);
    const results: AuditResult[] = [];

    for (const ext of extensions) {
      const trustSignals = await this.evaluateTrustSignals(ext);
      const secretExposure = await this.checkLocalSecrets(ext.installPath);
      const riskScore = this.calculateRiskScore(trustSignals, secretExposure);

      results.push({
        extensionId: ext.id,
        riskScore,
        trustSignals,
        secretExposure,
        recommendation: this.deriveRecommendation(riskScore),
      });
    }

    return results;
  }

  private async discoverExtensions(root: string): Promise<ExtensionMetadata[]> {
    const extensionsDir = join(root, "extensions");
    const entries = await readdir(extensionsDir, { withFileTypes: true });
    const metadata: ExtensionMetadata[] = [];

    for (const entry of entr

ies) { if (!entry.isDirectory()) continue; const pkgPath = join(extensionsDir, entry.name, "package.json"); try { const raw = await readFile(pkgPath, "utf-8"); const pkg = JSON.parse(raw); metadata.push({ id: pkg.name || entry.name, version: pkg.version || "unknown", sourceUrl: pkg.repository?.url, installPath: join(extensionsDir, entry.name), }); } catch { continue; } } return metadata; }

private async evaluateTrustSignals(ext: ExtensionMetadata): Promise<TrustSignal> { if (!ext.sourceUrl) { return { commitCount: 0, contributorCount: 0, lastActivityDays: 999, hasCodeReview: false }; }

const repoMatch = ext.sourceUrl.match(/github\.com\/([^/]+)\/([^/.]+)/);
if (!repoMatch) return { commitCount: 0, contributorCount: 0, lastActivityDays: 999, hasCodeReview: false };

const [, owner, repo] = repoMatch;
const [commits, contributors, repoData] = await Promise.all([
  this.octokit.rest.repos.listCommits({ owner, repo, per_page: 100 }),
  this.octokit.rest.repos.listContributors({ owner, repo, per_page: 100 }),
  this.octokit.rest.repos.get({ owner, repo }),
]);

const lastCommitDate = commits.data[0]?.commit.committer?.date;
const lastActivityDays = lastCommitDate
  ? Math.floor((Date.now() - new Date(lastCommitDate).getTime()) / 86400000)
  : 999;

return {
  commitCount: commits.data.length,
  contributorCount: contributors.data.length,
  lastActivityDays,
  hasCodeReview: repoData.data.allow_merge_commit || repoData.data.allow_squash_merge,
};

}

private async checkLocalSecrets(installPath: string): Promise<boolean> { const secretPatterns = [ /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/, /(?:api_key|apikey|secret|token)\s*[:=]\s*["'][A-Za-z0-9_-]{16,}["']/i, /(?:AWS|AZURE|GCP)[A-Z]{3,}=[A-Za-z0-9/+=]{20,}/, ];

const files = await readdir(installPath, { recursive: true });
for (const file of files) {
  if (!file.endsWith(".js") && !file.endsWith(".ts") && !file.endsWith(".json")) continue;
  const content = await readFile(join(installPath, file), "utf-8");
  if (secretPatterns.some((pattern) => pattern.test(content))) {
    return true;
  }
}
return false;

}

private calculateRiskScore(trust: TrustSignal, secretExposure: boolean): number { let score = 0; if (trust.commitCount < 10) score += 25; if (trust.contributorCount < 2) score += 20; if (trust.lastActivityDays > 180) score += 15; if (!trust.hasCodeReview) score += 10; if (secretExposure) score += 30; return Math.min(score, 100); }

private deriveRecommendation(score: number): "ALLOW" | "REVIEW" | "QUARANTINE" { if (score >= this.threshold) return "QUARANTINE"; if (score >= this.threshold * 0.6) return "REVIEW"; return "ALLOW"; } }

export { ExtensionAuditor };


### Why These Choices Matter

- **Provider Abstraction**: Hardcoding tool-specific paths creates maintenance debt. The `discoverExtensions` method reads `package.json` manifests, which is the standard across Node-based plugin ecosystems. This makes the scanner portable across VS Code, Cursor, and Obsidian.
- **GitHub API Over Local Cloning**: Cloning repositories for every installed extension consumes bandwidth and storage. Querying the GitHub REST API for commit history, contributor counts, and repository settings provides sufficient trust signals without local duplication.
- **Weighted Risk Scoring**: Binary allow/deny lists fail in dynamic ecosystems. A weighted score accounts for multiple risk vectors simultaneously. A dormant but widely-used plugin might score differently than an active but single-maintainer plugin with embedded secrets.
- **Secret Pattern Detection**: Regex-based scanning catches plaintext credentials before they become exfiltration vectors. This runs locally and never transmits vault contents externally.

## Pitfall Guide

### 1. Marketplace Trust Fallacy
**Explanation**: Assuming that inclusion in an official directory implies security vetting. Marketplaces typically perform automated syntax checks and malware signature scans, but they do not audit logic, network behavior, or update integrity.
**Fix**: Treat directory listing as a distribution channel, not a verification stamp. Apply independent trust scoring to every installed extension.

### 2. Ignoring Update Diffs
**Explanation**: Plugins can be clean at installation and malicious after an update. Account compromise, maintainer turnover, or deliberate payload injection all occur during the update cycle.
**Fix**: Pin extension versions in configuration files. When updates are available, diff the source repository against the previous release before applying. Automate this with CI pipelines that flag unexpected dependency changes.

### 3. Plaintext Secret Storage in Host Data
**Explanation**: Developers frequently store API keys, SSH keys, and `.env` files directly in note-taking vaults or IDE workspace directories. Plugins inherit read access to these locations by default.
**Fix**: Decouple secrets from the plugin host. Use system keychains, dedicated secret managers (1Password, Bitwarden, HashiCorp Vault), or environment variable injection at session startup. Never commit or store credentials in plaintext within application data directories.

### 4. Overlooking Persistence Artifacts
**Explanation**: Second-stage payloads often install persistence mechanisms that survive plugin removal. macOS uses `~/Library/LaunchAgents`, Windows uses Task Scheduler, and Linux uses `~/.config/systemd/user/`.
**Fix**: After uninstalling suspicious extensions, audit persistence locations for unfamiliar entries. Use OS-native tools (`launchctl`, `schtasks`, `systemctl --user`) to list and verify running services. Remove orphaned configurations immediately.

### 5. Assuming Popularity Equals Security
**Explanation**: Download counts and star ratings measure utility, not safety. A widely installed plugin with a compromised maintainer account becomes a high-value distribution vector.
**Fix**: Evaluate maintainer identity and repository governance over download metrics. Prefer plugins with multi-contributor commit histories, signed releases, and transparent issue tracking. Remove extensions you no longer actively use, regardless of popularity.

### 6. Neglecting AI Tool Extension Surfaces
**Explanation**: AI coding assistants (Cursor, Copilot, Codeium) load community extensions that execute in the same process as your code editor. These tools often have broader filesystem and network access to support context retrieval and code generation.
**Fix**: Apply the same audit standards to AI tool extensions as you would to core IDE plugins. Restrict AI tool permissions to workspace directories only. Disable automatic extension updates until diffs are reviewed.

### 7. Skipping Post-Install Verification
**Explanation**: Installing an extension and immediately using it without verifying its runtime behavior leaves a window for delayed payload execution.
**Fix**: Run extensions in a monitored environment first. Use network monitoring tools (Wireshark, Little Snitch, GlassWire) to detect unexpected outbound connections. Verify that filesystem access aligns with documented functionality before granting long-term trust.

## Production Bundle

### Action Checklist
- [ ] Inventory all installed extensions across Obsidian, VS Code, Cursor, and JetBrains IDEs
- [ ] Run the TypeScript audit scanner against each workspace to generate risk scores
- [ ] Quarantine extensions scoring above the configured threshold until manual review completes
- [ ] Diff source repositories for all pending updates before applying
- [ ] Migrate plaintext credentials from vaults and workspace directories to a dedicated secret manager
- [ ] Audit OS-level persistence locations for orphaned launch agents or scheduled tasks
- [ ] Pin extension versions in configuration files to prevent silent background updates
- [ ] Schedule quarterly extension audits and integrate scanning into CI/CD pipelines

### Decision Matrix

| Scenario | Recommended Approach | Why | Cost Impact |
|----------|---------------------|-----|-------------|
| Solo Developer | Manual audit + secret manager migration | Low overhead, direct control over trust signals | Minimal time investment, high security ROI |
| Small Team (5-20) | Shared audit pipeline + version pinning | Standardizes trust evaluation, prevents drift | Moderate setup time, reduces incident response costs |
| Enterprise (50+) | Centralized extension policy + EDR integration | Enforces compliance, blocks high-risk plugins at scale | Higher infrastructure cost, prevents data exfiltration |
| AI-Heavy Workflow | Restricted AI permissions + isolated extension host | Limits context leakage, contains AI tool attack surface | Requires workflow adjustment, protects IP and credentials |

### Configuration Template

```json
{
  "extensionAudit": {
    "riskThreshold": 60,
    "allowedHosts": ["github.com", "gitlab.com"],
    "secretPatterns": [
      "-----BEGIN.*PRIVATE KEY-----",
      "(?:api_key|secret|token)\\s*[:=]\\s*[\"'][A-Za-z0-9_\\-]{16,}[\"']",
      "(?:AWS|AZURE|GCP)_[A-Z_]{3,}=[A-Za-z0-9/+=]{20,}"
    ],
    "quarantineActions": ["disable", "log", "notify"],
    "updatePolicy": "manual_diff_required",
    "workspacePaths": [
      "~/.obsidian/community-plugins",
      "~/.vscode/extensions",
      "~/.cursor/extensions"
    ]
  }
}

Quick Start Guide

  1. Install Dependencies: Run npm install @octokit/rest in your audit project directory. Ensure Node.js 18+ is available.
  2. Configure Environment: Set GITHUB_TOKEN in your shell or .env file. Adjust riskThreshold in the configuration template to match your security posture.
  3. Execute Scan: Run node audit-runner.js --workspace /path/to/dev/root. The script outputs a JSON report with risk scores and recommendations.
  4. Apply Remediation: Quarantine extensions flagged as QUARANTINE. Migrate plaintext secrets to your preferred secret manager. Pin versions for critical extensions.
  5. Schedule Recurrence: Add the audit script to your weekly maintenance routine or CI pipeline. Review diff reports before applying any extension updates.