critical Threat analysis

Nx Console VS Code Extension Compromise

On May 18, 2026, the official Nx Console VS Code extension was compromised when attackers used an OAuth token stolen in the TanStack compromise to publish malicious version v18.95.0, resulting in the theft of 3,800 internal GitHub repositories.

#vscode#extension#supply-chain#compromise#oauth#teampcp
On this page 0% read

    Executive Summary

    On May 18, 2026, a highly critical supply chain attack targeted the Nx Console VS Code extension, leading to a major security breach that resulted in the exfiltration of approximately 3,800 internal GitHub repositories GitHub Security Advisory. The threat actor group TeamPCP (also tracked as UNC6780) leveraged a GitHub CLI OAuth token stolen seven days earlier (via the TanStack supply-chain compromise) from an Nx contributor StepSecurity. By exploiting this developer’s push credentials, the attackers bypassed registry guardrails to publish a malicious version of the extension (v18.95.0) to the Visual Studio Marketplace and the Open VSX registry Nx Advisory. The extension remained active for 18 minutes on the VS Code Marketplace and 36 minutes on Open VSX. When installed and loaded inside a workspace, the extension executed an obfuscated Python backdoor (cat.py) that harvested developer credentials and established persistent access, compromising developer machines globally—including a critical endpoint owned by a GitHub employee Infosecurity Magazine.

    Key Facts

    threat_type: compromised developer tool, credential theft, token exfiltration, poisoned release
    ecosystem: vs-code-extension-marketplace, open-vsx
    registry: Visual Studio Marketplace, Open VSX
    affected_packages:
      - "nx-console"
    malicious_versions:
      - "18.95.0"
    fixed_versions:
      - "18.100.0"
      - "18.100.5"
    safe_versions: []
    exposure_window: 2026-05-18T12:30:00Z to 2026-05-18T13:09:00Z (39 minutes total; 18 minutes on VS Code Marketplace)
    execution_trigger: Workspace load-time execution (extension activation)
    primary_impact: Exfiltration of high-value developer credentials (GitHub tokens, SSH keys, AWS secrets) and mass repository code theft
    known_iocs:
      - "cat.py"
      - "com.user.kitty-monitor.plist"
      - "sfrclak[.]com"
    confidence: high
    canonical_source: https://nx.dev

    Source Confidence & Evidence Mapping

    • confirmed: Compromise of Nx Console extension v18.95.0 via a hijacked contributor credentials vector, active for a limited time on public registries. Nx Advisory
    • confirmed: Exfiltration of approximately 3,800 internal repositories from GitHub’s internal environment following the compromise of an employee’s development endpoint. GitHub Security Advisory
    • likely: Direct link to the TanStack @tanstack/zod-adapter compromise on May 11, 2026, which provided the attacker with the initial gh CLI OAuth token of the Nx developer. StepSecurity Analysis
    • unclear: The absolute count of non-GitHub development organizations whose workstations auto-updated to the poisoned extension during the active exposure window.

    Impact Determination

    ClassificationCriteriaRequired evidenceRequired actionClosure condition
    Confirmed compromisethe malicious Nx Console extension version is present and VS Code extension activation executes the embedded Python payload or the reported process, file, or network indicators is observed.Artifact inventory plus runtime telemetry showing VS Code extension activation executes the embedded Python payload or listed C2/process/file indicators.Isolate affected hosts or runners, preserve artifacts, and rotate reachable credentials from a clean environment.Affected artifacts are removed, exposed credentials are replaced, and downstream audit modules show no suspicious follow-on use.
    Presumed exposedthe malicious Nx Console extension version was installed, pulled, imported, built, or executed during the exposure window, but telemetry cannot prove exfiltration.Lockfile, package cache, workflow, image pull, extension inventory, build log, or deployment record tied to the exposure window.Rebuild from clean artifacts and rotate credentials available to the affected environment.Credential owners confirm revocation of old material and clean artifacts are deployed.
    Potentially exposedThe package, workflow, image, extension, or module appears in dependency or deployment records, but extension activation is not established.Manifest, lockfile, build, deployment, or endpoint records plus a named telemetry gap.Collect the missing execution and telemetry evidence before narrowing scope.Every hit is dispositioned as confirmed compromise, presumed exposed, or not exposed.
    Not exposedNo affected version, artifact, mutable reference, or indicator appears in source, lockfiles, build outputs, deployments, package caches, or runtime telemetry.Repository search, dependency inventory, build/deployment export, package cache query, and runtime telemetry query results.Preserve the negative search output and keep the prevention controls active.Search evidence covers developer endpoints, CI runners, production deployments, and package or image caches.
    UnknownRequired inventory, build, endpoint, network, or audit telemetry is unavailable.A gap statement naming unavailable systems, owners, and time windows.Keep the asset in scope and make conservative rotation or rebuild decisions for high-value environments.The missing evidence is recovered or the risk owner accepts residual uncertainty.

    Minimum Evidence To Collect

    minimum_evidence:
      - "Dependency, workflow, extension, image, or module inventory covering developer endpoints, CI runners, and production deployments."
      - "Positive or negative search results for Nx Console v18.95.0."
      - "Execution evidence for VS Code extension activation executes the embedded Python payload."
      - "Process, file, DNS, proxy, firewall, or package-manager telemetry for listed indicators."
      - "Inventory of credentials, tokens, deployment paths, and downstream systems reachable from exposed environments."

    Timeline

    • 2026-05-11T19:26:00Z Nx contributor’s development workstation is infected with the Mini Shai-Hulud worm via a poisoned @tanstack/* dependency update. Source: StepSecurity
    • 2026-05-11T19:40:00Z Mini Shai-Hulud malware harvests the contributor’s GitHub CLI OAuth token and exfiltrates it to C2. Source: StepSecurity
    • 2026-05-18T12:30:00Z Attackers use the stolen OAuth token to log in and publish backdoored Nx Console v18.95.0 to the VS Code Marketplace and Open VSX. Source: Nx Advisory
    • 2026-05-18T12:45:00Z Nx team receives immediate reports of anomalies and verifies the unauthorized release. Source: Nx Advisory
    • 2026-05-18T12:48:00Z VS Code Marketplace removes the malicious v18.95.0 release (18-minute exposure). Source: Nx Advisory
    • 2026-05-18T13:09:00Z Open VSX registry removes the malicious v18.95.0 release (39-minute exposure). Source: Nx Advisory
    • 2026-05-18T13:15:00Z Nx team publishes clean, hardened updates (v18.100.0) and triggers forced downstream updates. Source: Nx Advisory
    • 2026-05-18T15:00:00Z Official security advisory is published under tracking ID GHSA-c9j4-9m59-847w. Source: GHSA Database
    • 2026-05-19T09:00:00Z GitHub confirms internal source code repository exfiltration stemming from an employee’s compromised endpoint. Source: GitHub Security Advisory

    What Happened

    On May 18, 2026, the TeamPCP threat group executed a direct attack against developer workstations by poisoning the popular Nx Console VS Code extension Ox Security. Utilizing a GitHub CLI OAuth token harvested during the earlier TanStack incident, the attackers gained administrative push permissions to the extension’s publishing account StepSecurity. They published version v18.95.0, which contained an obfuscated payload loader embedded inside a dangling release commit Nx Advisory. Despite being revoked from marketplaces within 18 to 36 minutes, the poisoned extension was pulled by auto-updating VS Code instances, including one belonging to a GitHub engineer Infosecurity Magazine. The backdoor instantly activated, harvested active session tokens, and allowed the threat group to steal approximately 3,800 internal GitHub repository codebases GitHub Security Advisory.

    Technical Analysis

    Initial Access

    Initial access was gained using the stolen GitHub CLI (gh) OAuth token of a legitimate Nx contributor. This token had been exfiltrated seven days prior on May 11, 2026, when the contributor installed a poisoned @tanstack/* package. The threat actor used this persistent session to authenticated to the Visual Studio Marketplace and Open VSX, uploading the malicious extension package.

    Package or Artifact Manipulation

    The attackers packaged and published a rogue extension tarball (.vsix) containing a malicious Python script (cat.py) hidden in the assets. They altered the extension’s entry file to ensure that as soon as the VS Code IDE activated the extension (which occurs automatically upon opening a workspace folder containing an Nx project), the payload was triggered.

    Execution Trigger

    The malware executed at IDE activation time. VS Code extensions have a lifecycle hook (activate()) defined in their main script. The poisoned version 18.95.0 spawned a background shell process to execute the hidden Python script (cat.py) without displaying any indicators or terminal windows to the user.

    Payload Behavior

    Once executed, the Python backdoor (cat.py) initiated a multi-stage compromise:

    • Credential Theft: It scraped developer configurations, harvesting SSH keys, AWS access profiles, HashiCorp Vault tokens, 1Password CLI session logs, and .git-credentials entries.
    • Persistence: It planted an persistent LaunchAgent on macOS (~/Library/LaunchAgents/com.user.kitty-monitor.plist) that regularly monitored and executed the ~/.local/share/kitty/cat.py script.
    • Process Evasion: It ran as a daemonized Python process, utilizing the environment variable __DAEMONIZED=1 to masquerade as system maintenance scripting.
    • Worm Detection / Dead Man’s Switch: The backdoor monitored the validity of the compromised host’s primary tokens. If a token revocation was detected (suggesting discovery by security teams), the script was capable of executing a target-wipe command.

    Exfiltration / C2

    domains:
      - "sfrclak[.]com"
    ips: []
    urls:
      - "hxxps://sfrclak[.]com/api/v1/beacon"
    protocols:
      - "https"
    endpoints:
      - "/api/v1/beacon"
      - "/payloads/"
    confidence: high

    The backdoor established a beaconing connection to sfrclak[.]com over HTTPS, exfiltrating the harvested credentials. During the GitHub employee compromise, the attackers leveraged these exfiltrated credentials to access internal servers and clone thousands of proprietary repositories.

    Propagation

    The malware did not contain lateral network propagation scripts, instead relying on the collected authentication tokens to manually pivot to downstream SaaS systems (such as GitHub, npm, and AWS registries) to continue publishing malicious packages or harvesting code.

    Obfuscation or Evasion

    The payload file cat.py was lightly obfuscated using variable renaming and base64-encoded command execution. Evasion was primarily achieved by masquerading as terminal/shell configurations associated with the “Kitty” terminal emulator, exploiting common path exceptions like ~/.local/share/kitty/.

    Affected Assets and Blast Radius

    affected_assets:
      ecosystems:
        - "vs-code-extension-marketplace"
        - "open-vsx"
      packages:
        - "Nx Console"
      versions:
        - "18.95.0"
      repositories:
        - "github.com/nrwl/nx-console"
      container_images: []
      CI_CD_systems: []
      developer_tools:
        - "VS Code IDE"
      environments:
        - developer workstations
        - corporate endpoints
    
    credentials_at_risk:
      - GitHub tokens
      - SSH keys
      - AWS/GCP secrets
      - Vault configurations
      - 1Password master keys
    
    not_currently_known_to_affect:
      - CI runners and build pipelines (unless they run interactive VS Code sessions).

    Indicators of Compromise

    domains:
      - value: sfrclak[.]com
        source: https://nx.dev
        confidence: high
    ips: []
    urls:
      - value: hxxps://sfrclak[.]com/api/v1/beacon
        source: https://nx.dev
        confidence: high
    hashes: []
    files:
      - value: ~/.local/share/kitty/cat.py
        source: https://nx.dev
        confidence: high
      - value: ~/Library/LaunchAgents/com.user.kitty-monitor.plist
        source: https://nx.dev
        confidence: high
      - value: /var/tmp/.gh_update_state
        source: https://nx.dev
        confidence: high
    package_versions:
      - value: Nx Console v18.95.0
        source: https://nx.dev
        confidence: high

    Detection and Hunting

    Script: local repository and exported telemetry scope

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
    LOG_ROOT = os.environ.get("LOG_ROOT", "")
    OUT = Path(os.environ.get("OUT", "hp-nx-console-extension-compromise-scope"))
    SINCE = "2026-05-11T19:26:00Z"
    UNTIL = "2026-05-19T09:00:00Z"
    
    PACKAGES = [
      "nx-console",
    ]
    VERSIONS = [
      "18.95.0",
      "Nx Console v18.95.0",
    ]
    FILES = [
      "~/.local/share/kitty/cat.py",
      "~/Library/LaunchAgents/com.user.kitty-monitor.plist",
      "/var/tmp/.gh_update_state",
    ]
    DOMAINS = [
      "sfrclak.com",
      "com.user.kitty-monitor.plist",
    ]
    URLS = [
      "https://sfrclak.com/api/v1/beacon",
      "https://nx.dev",
    ]
    IPS = [
    ]
    HASHES = [
    ]
    PROCESS_PATTERNS = [
    ]
    NETWORK_PATTERNS = [
    ]
    
    # Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
    # Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
    
    OUT.mkdir(parents=True, exist_ok=True)
    indicators_file = OUT / "indicators.txt"
    
    # Collect unique indicators
    indicators = set()
    for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
        for val in group:
            if val:
                indicators.add(val)
    
    with open(indicators_file, "w") as f:
        for ind in sorted(indicators):
            f.write(ind + "\n")
    
    print(f"[+] Written unique selectors to {indicators_file}")
    
    # Walk local directory
    print(f"[+] Scanning directory: {ROOT} for selectors...")
    matches = []
    exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
    for root, dirs, filenames in os.walk(ROOT):
        dirs[:] = [d for d in dirs if d not in exclude_dirs]
        for filename in filenames:
            filepath = Path(root) / filename
            try:
                content = filepath.read_text(errors="ignore")
                for ind in indicators:
                    if ind in content:
                        matches.append(f"{filepath}: found '{ind}'")
            except Exception:
                pass
    
    if matches:
        (OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
        print(f"[!] Found {len(matches)} matches in codebase!")
    
    # Optional Log Scanning
    if LOG_ROOT and os.path.exists(LOG_ROOT):
        print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
        log_matches = []
        for root, _, filenames in os.walk(LOG_ROOT):
            for filename in filenames:
                filepath = Path(root) / filename
                try:
                    content = filepath.read_text(errors="ignore")
                    for ind in indicators:
                        if ind in content:
                            log_matches.append(f"{filepath}: found '{ind}'")
                except Exception:
                    pass
        if log_matches:
            (OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
            print(f"[!] Found {len(log_matches)} matches in logs!")
    
        if PACKAGES:
            registry_dir = OUT / "registry"
            registry_dir.mkdir(exist_ok=True)
    
    print(f"[+] Wrote scope artifacts under {OUT}")

    Downstream Abuse Audits

    Script: GitHub organization run, release, secret, and workflow audit

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    if "ORG" not in os.environ:
        print("ERROR: Set ORG environment variable to the GitHub organization to audit", file=sys.stderr)
        sys.exit(1)
    
    ORG = os.environ["ORG"]
    SINCE = "2026-05-11T19:26:00Z"
    UNTIL = "2026-05-19T09:00:00Z"
    OUT = Path(os.environ.get("OUT", "hp-nx-console-extension-compromise-github-audit"))
    
    SELECTORS = [
      "nx-console",
      "18.95.0",
      "Nx Console v18.95.0",
      "~/.local/share/kitty/cat.py",
      "~/Library/LaunchAgents/com.user.kitty-monitor.plist",
      "/var/tmp/.gh_update_state",
      "sfrclak.com",
      "com.user.kitty-monitor.plist",
      "https://sfrclak.com/api/v1/beacon",
      "https://nx.dev",
    ]
    
    # Positive signal: a workflow run, release, secret, key, package, or workflow change overlaps the exposure window and references an incident selector.
    # Remediation trigger: unauthorized post-exposure write activity or a secret-bearing run matching an incident selector requires token revocation and downstream cloud/registry review.
    
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "runs").mkdir(exist_ok=True)
    (OUT / "logs").mkdir(exist_ok=True)
    (OUT / "repos").mkdir(exist_ok=True)
    
    # 1. Write incident-selectors file
    selectors_file = OUT / "incident-selectors.txt"
    with open(selectors_file, "w") as sf:
        for s in SELECTORS:
            if s:
                sf.write(s + "\n")
    
    # 2. Get list of repos
    print(f"[+] Fetching repositories for organization: {ORG}")
    repo_res = subprocess.run(["gh", "repo", "list", ORG, "--limit", "1000", "--json", "nameWithOwner"], capture_output=True, text=True)
    if repo_res.returncode != 0:
        print(f"[-] Failed to fetch repos: {repo_res.stderr}", file=sys.stderr)
        sys.exit(1)
    
    repos = [r["nameWithOwner"] for r in json.loads(repo_res.stdout)]
    
    for repo in repos:
        safe_repo = repo.replace("/", "__")
        print(f"[+] Auditing repository: {repo}")
    
        # Check runs in the window
        runs_res = subprocess.run([
            "gh", "api", f"/repos/{repo}/actions/runs",
            "-f", "per_page=100",
            "-f", f"created=>={SINCE}",
            "--paginate"
        ], capture_output=True, text=True)
    
        if runs_res.returncode == 0:
            try:
                all_runs = json.loads(runs_res.stdout).get("workflow_runs", [])
                filtered_runs = [r for r in all_runs if r["created_at"] <= UNTIL]
    
                if filtered_runs:
                    with open(OUT / "runs" / f"{safe_repo}-runs.jsonl", "w") as rf:
                        for run in filtered_runs:
                            rf.write(json.dumps(run) + "\n")
    
                            # Fetch log dynamically
                            run_id = str(run["id"])
                            log_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--log"], capture_output=True, text=True)
                            if log_res.returncode == 0:
                                (OUT / "logs" / f"{safe_repo}-{run_id}.log").write_text(log_res.stdout)
    
                            # Fetch details
                            view_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--json", "databaseId,workflowName,headSha,event,createdAt,jobs"], capture_output=True, text=True)
                            if view_res.returncode == 0:
                                (OUT / "runs" / f"{safe_repo}-{run_id}.json").write_text(view_res.stdout)
            except Exception as e:
                print(f"[-] Error parsing runs for {repo}: {e}")
    
        # Check releases in window
        subprocess.run(["gh", "api", f"/repos/{repo}/releases", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check repo secrets updated in window
        subprocess.run(["gh", "api", f"/repos/{repo}/actions/secrets", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check deploy keys
        subprocess.run(["gh", "api", f"/repos/{repo}/keys", "-f", "per_page=100", "--paginate"], capture_output=True)
    
    # Scan output directory for any indicator selector matches
    print("[+] Scanning gathered telemetry for indicator matches...")
    subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)], capture_output=False)
    
    print(f"[+] Wrote GitHub audit artifacts under {OUT}")

    Script: cloud OIDC and deployment credential follow-on audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-05-11T19:26:00Z"
    UNTIL = "2026-05-19T09:00:00Z"
    OUT = Path(os.environ.get("OUT", "hp-nx-console-extension-compromise-cloud-audit"))
    AWS_REGIONS = os.environ.get("AWS_REGIONS", "us-east-1").split(",")
    
    # Positive signal: token exchange or privileged write activity occurs in the exposure window from GitHub, CI/CD, package registry, or deployment automation identity.
    # Remediation trigger: unexpected write, deploy, IAM, secret, or registry activity tied to an exposed CI/CD path requires trust-policy disablement and credential rotation.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    # 1. AWS CloudTrail Audit
    print("[+] Querying AWS CloudTrail for Web Identity token exchanges...")
    aws_events = []
    for region in AWS_REGIONS:
        res = subprocess.run([
            "aws", "cloudtrail", "lookup-events",
            "--region", region,
            "--start-time", SINCE,
            "--end-time", UNTIL,
            "--lookup-attributes", "AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity",
            "--output", "json"
        ], capture_output=True, text=True)
    
        if res.returncode == 0:
            try:
                events = json.loads(res.stdout).get("Events", [])
                for event in events:
                    ct = json.loads(event.get("CloudTrailEvent", "{}"))
                    ct["region"] = region
                    aws_events.append(ct)
            except Exception as e:
                print(f"[-] Error parsing AWS CloudTrail events: {e}")
    
    if aws_events:
        with open(OUT / "aws-assume-role-with-web-identity.jsonl", "w") as f:
            for ev in aws_events:
                f.write(json.dumps(ev) + "\n")
    
        # Audit follow-on events for returned access keys
        for ev in aws_events:
            access_key = ev.get("responseElements", {}).get("credentials", {}).get("accessKeyId")
            region = ev.get("region", "us-east-1")
            if access_key:
                print(f"[+] Enumerating AWS events for AccessKey: {access_key}")
                f_res = subprocess.run([
                    "aws", "cloudtrail", "lookup-events",
                    "--region", region,
                    "--start-time", SINCE,
                    "--end-time", UNTIL,
                    "--lookup-attributes", f"AttributeKey=AccessKeyId,AttributeValue={access_key}",
                    "--output", "json"
                ], capture_output=True, text=True)
                if f_res.returncode == 0:
                    try:
                        f_events = json.loads(f_res.stdout).get("Events", [])
                        with open(OUT / "aws-follow-on-api-calls.jsonl", "a") as ff:
                            for fe in f_events:
                                ff.write(fe.get("CloudTrailEvent", "{}") + "\n")
                    except Exception as e:
                        print(f"[-] Error writing follow-on events: {e}")
    
    # 2. Azure Activity Log Audit
    print("[+] Querying Azure activity logs...")
    az_res = subprocess.run([
        "az", "monitor", "activity-log", "list",
        "--start-time", SINCE,
        "--end-time", UNTIL,
        "--query", "[?contains(operationName.value, 'write') || contains(operationName.value, 'delete') || contains(operationName.value, 'Microsoft.ManagedIdentity')]",
        "-o", "json"
    ], capture_output=True, text=True)
    
    if az_res.returncode == 0:
        (OUT / "azure-write-delete-activity.json").write_text(az_res.stdout)
    
    # 3. GCP Logging Audit
    print("[+] Querying GCP Cloud Logging...")
    gcp_filter = f'timestamp>="{SINCE}" AND timestamp<="{UNTIL}" AND (protoPayload.methodName="google.sts.v1.SecurityTokenService.ExchangeToken" OR protoPayload.methodName:"GenerateAccessToken" OR protoPayload.methodName:"CreateServiceAccountKey" OR protoPayload.methodName:"SetIamPolicy")'
    gcp_res = subprocess.run([
        "gcloud", "logging", "read", gcp_filter,
        "--format", "json"
    ], capture_output=True, text=True)
    
    if gcp_res.returncode == 0:
        (OUT / "gcp-token-and-iam-activity.json").write_text(gcp_res.stdout)
    
    print(f"[+] Wrote cloud audit artifacts under {OUT}")

    Script: registry metadata and artifact audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-05-11T19:26:00Z"
    OUT = Path(os.environ.get("OUT", "hp-nx-console-extension-compromise-registry-audit"))
    PACKAGES = [
      "nx-console",
    ]
    VERSIONS = [
      "18.95.0",
      "Nx Console v18.95.0",
    ]
    
    # Positive signal: workflow files or extensions reference the affected action/extension names or versions.
    # Remediation trigger: exposed secrets or OIDC federation policies must be immediately rotated.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    with open(OUT / "affected-versions.txt", "w") as av:
        for version in VERSIONS:
            if version:
                av.write(version + "\n")
    
        # 1. Search local workspace files for the affected actions/extensions
        print("[+] Scanning workspace workflows for selectors...")
        for file in Path(".").glob(".github/workflows/**/*.yml"):
            subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), str(file)])
    
        # 2. HOW TO ROTATE EXPOSED GITHUB ACTIONS SECRETS:
        # Overwrite compromised secrets with newly generated credentials:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--body", "my-new-secret-value", "--repo", "my-org/my-repo"])
        # For organization-level secrets:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--org", "my-org", "--visibility", "private"])
        # Revoke compromised OIDC federated trust credentials in AWS/GCP and redeploy the IAM trust policy:
        # subprocess.run(["aws", "iam", "update-assume-role-policy", "--role-name", "my-role-name", "--policy-document", "file://new-clean-trust-policy.json"])
    
    print(f"[+] Wrote registry audit artifacts under {OUT}")

    Sources

    1. Nx Official Incident Postmortem. Role: DIRECT_SOURCE Impact: Detailed timing, exposure window, compromised contributor information, and cleanup instructions.
    2. GHSA-c9j4-9m59-847w Advisory Record. Role: DIRECT_SOURCE Impact: Vulnerability details and affected extension version mapping.
    3. StepSecurity Nx Console Compromise Analysis. Role: PRIMARY_RESEARCH Impact: Analysis of the connection between the TanStack and Nx incidents and token theft.
    4. Ox Security Threat Report on VS Code Supply Chains. Role: PRIMARY_RESEARCH Impact: Technical details on IDE extension vector manipulation and credential theft scripting.
    5. Infosecurity Magazine Incident Report. Role: SECONDARY_ANALYSIS Impact: Documentation of the downstream GitHub repository exfiltration breach.

    IOC Clipboard

    7 IOCs
    Defang IOCs
    domain sfrclak.com sfrclak[.]com
    domain com.user.kitty-monitor.plist com[.]user[.]kitty-monitor[.]plist
    url https://sfrclak.com/api/v1/beacon hxxps://sfrclak[.]com/api/v1/beacon
    url https://nx.dev hxxps://nx[.]dev
    file ~/.local/share/kitty/cat.py ~/.local/share/kitty/cat.py
    file ~/Library/LaunchAgents/com.user.kitty-monitor.plist ~/Library/LaunchAgents/com.user.kitty-monitor.plist
    file /var/tmp/.gh_update_state /var/tmp/.gh_update_state