critical Threat analysis

Red Hat Cloud Services npm Trusted-Publishing Compromise

Multiple @redhat-cloud-services npm packages were compromised on 2026-06-01 through trusted-publishing abuse tied to the Mini Shai-Hulud Miasma wave. The malicious releases added install-time payload execution, credential collection, destructive fallback behavior, and GitHub workflow tampering risk.

#npm#redhat#supply-chain#ci-cd#oidc#credential-theft#mini-shai-hulud
On this page 0% read

    Executive Summary

    On 2026-06-01, researchers reported a new Mini Shai-Hulud child event affecting the @redhat-cloud-services npm scope. StepSecurity identified 31 malicious package versions across 29 packages, published through trusted publishing after an attacker compromised the RedHatInsights/javascript-clients GitHub workflow path and abused OIDC-based npm publishing [Source 1].

    This is not a normal vulnerable dependency advisory. It is a supply-chain publish compromise against Red Hat Cloud Services client packages that can execute during npm install. The reported payload is called Miasma, also tracked as The Spreading Blight, and it overlaps the same Mini Shai-Hulud operational pattern of CI/CD compromise, credential theft, GitHub workflow tampering, and destructive pressure after token invalidation [Source 1] [Source 2].

    Organizations should search for affected @redhat-cloud-services/* packages in source repositories, lockfiles, package caches, build logs, CI artifacts, and container layers. Any confirmed install on a developer workstation or runner with GitHub, npm, cloud, or deployment credentials should trigger credential rotation from a clean environment.

    Source-Watcher Candidate Queue

    candidate_id: "redhat-cloud-services-npm-miasma-compromise"
    first_seen: "2026-06-01"
    decision: "publish_ready"
    relationship: "child_event_of_mini_shai_hulud_worm"
    dedupe_keys:
      - "npm:@redhat-cloud-services"
      - "repo:RedHatInsights/javascript-clients"
      - "package:@redhat-cloud-services/patch-client@4.0.4"
      - "payload:miasma"
      - "campaign:mini-shai-hulud"
    starting_sources:
      - "StepSecurity primary research"
      - "OX Security primary research"
      - "Mend primary research"
      - "BleepingComputer Red Hat statement"

    Key Facts

    threat_type: "malicious npm package publish compromise"
    ecosystem: "npm"
    registry: "npm"
    campaign_context: "Mini Shai-Hulud / Miasma / The Spreading Blight"
    affected_scope: "@redhat-cloud-services"
    source_repository: "RedHatInsights/javascript-clients"
    reported_publish_date: "2026-06-01"
    reported_package_count: 29
    reported_malicious_version_count: 31
    execution_trigger: "npm install lifecycle script"
    publish_path: "GitHub Actions trusted publishing / npm OIDC"
    credential_risk:
      - "GitHub tokens"
      - "npm publishing tokens"
      - "cloud credentials"
      - "CI/CD deployment credentials"
      - "developer workstation secrets"

    Source Confidence and Claim Ledger

    ClaimStatusEvidence
    Multiple @redhat-cloud-services npm packages were maliciously published on 2026-06-01.confirmedStepSecurity reports 31 malicious versions across 29 packages and lists exact package/version pairs [Source 1].
    The compromise path involved the RedHatInsights/javascript-clients GitHub repository and OIDC trusted publishing.confirmedStepSecurity describes the attacker-created pull request, merged branch, workflow execution, and trusted-publishing abuse path [Source 1].
    The payload is tracked as Miasma or The Spreading Blight and overlaps Mini Shai-Hulud behavior.likelyStepSecurity and OX both connect the behavior to the Mini Shai-Hulud family and describe destructive or worm-like behavior [Source 1] [Source 2].
    The malicious code targets credentials and can tamper with GitHub repository state.confirmedStepSecurity and Mend describe credential theft, GitHub workflow manipulation, and repository-level changes [Source 1] [Source 3].
    Red Hat says it removed the compromised package versions and rotated exposed tokens.confirmedBleepingComputer published a Red Hat statement saying affected versions were removed and exposed tokens were rotated [Source 4].
    Public sources currently prove downstream victim count or successful cloud abuse.not_observedNone of the reviewed public sources provides a verified victim count or confirmed downstream cloud-control-plane abuse list.

    Impact Determination

    ClassificationCriteriaRequired evidenceHandling decisionClosure condition
    Confirmed compromiseAn affected package/version was installed or executed and endpoint, process, network, or GitHub audit telemetry shows Miasma selectors or credential collection behavior.Lockfile/cache hit plus package install logs, process telemetry, shell history, endpoint telemetry, proxy logs, or GitHub audit activity.Isolate the host or runner, preserve package artifacts, rotate reachable credentials, and audit follow-on GitHub/npm/cloud activity.Affected packages are removed, credentials are rotated, and downstream audits show no unexplained write activity.
    Presumed exposedAn affected package/version was installed on a developer workstation, CI runner, image build, or release environment but runtime telemetry is incomplete.Dependency lock, package cache, build log, container layer, or npm install record.Treat GitHub, npm, cloud, registry, and deployment credentials reachable from that environment as exposed.Risk owners confirm clean rebuilds and credential rotation or accept residual risk.
    Potentially exposedRepositories or builds use @redhat-cloud-services/*, but exact resolved versions or install execution is not established.Repository manifests, lockfiles, private registry logs, CI logs, and package-cache inventory.Reconstruct package resolution and install execution before narrowing scope.Each hit is dispositioned as confirmed compromise, presumed exposed, or not exposed.
    Not exposedNo affected package names, affected versions, package-cache artifacts, tarballs, or runtime selectors appear in complete evidence.Negative search across repos, CI logs, package caches, endpoint telemetry, proxy logs, and registry mirrors.Preserve negative evidence and keep lifecycle-script controls in place.Evidence coverage includes developer endpoints, CI runners, production builds, and internal mirrors.
    UnknownDependency inventory, package-cache data, runner telemetry, endpoint telemetry, or audit logs are unavailable.Named gap with system owner and retention window.Keep credentials in scope until evidence is recovered or rotation closes the gap.Missing evidence is recovered or the risk owner accepts residual uncertainty.

    Timeline

    • 2026-06-01: StepSecurity reports malicious @redhat-cloud-services package versions and ties the activity to a trusted-publishing workflow compromise [Source 1].
    • 2026-06-01: OX publishes its analysis of the Red Hat Cloud Services npm compromise and frames the activity as a Mini Shai-Hulud return wave [Source 2].
    • 2026-06-01: Mend publishes a companion analysis of the Miasma payload and highlights malicious behavior in the npm package set [Source 3].
    • 2026-06-02: This Halting Problems refresh found no existing local post for the @redhat-cloud-services Miasma wave and created this child event report.

    Technical Analysis

    The material supply-chain failure is not the mere existence of vulnerable package code. It is the use of trusted CI/CD publishing to push malicious package versions into the public npm registry. StepSecurity reports that the attacker opened a pull request in RedHatInsights/javascript-clients, got workflow-controlled publishing execution, and used the resulting OIDC trusted-publishing path to publish malicious versions [Source 1].

    The payload belongs in the same response family as Mini Shai-Hulud because the defensive questions are the same: which package versions resolved, which install hooks executed, which credentials were reachable, and what GitHub/npm/cloud activity followed. OX and Mend both describe the payload as a new wave using Miasma or The Spreading Blight naming, with destructive and credential-theft behavior [Source 2] [Source 3].

    Treat successful installation on a runner as credential exposure unless telemetry proves the lifecycle script did not run. Lockfile-only evidence is not enough for confirmed compromise, but it is enough to keep the environment in scope until package-cache and install-log evidence is collected.

    Machine-Readable Event Profile

    {
      "event_id": "redhat-cloud-services-npm-miasma-compromise",
      "title": "Red Hat Cloud Services npm Trusted-Publishing Compromise",
      "first_seen": "2026-06-01",
      "published": "2026-06-02",
      "severity": "critical",
      "ecosystem": ["npm", "GitHub Actions", "trusted publishing"],
      "campaign_context": "Mini Shai-Hulud / Miasma / The Spreading Blight",
      "affected_repository": "RedHatInsights/javascript-clients",
      "affected_scope": "@redhat-cloud-services",
      "affected_packages": [
        "@redhat-cloud-services/patch-client",
        "@redhat-cloud-services/insights-client",
        "@redhat-cloud-services/host-inventory-client",
        "@redhat-cloud-services/vulnerabilities-client",
        "@redhat-cloud-services/remediations-client",
        "@redhat-cloud-services/sources-client",
        "@redhat-cloud-services/compliance-client",
        "@redhat-cloud-services/rbac-client",
        "@redhat-cloud-services/advisor-client",
        "@redhat-cloud-services/notifications-client",
        "@redhat-cloud-services/integrations-client",
        "@redhat-cloud-services/drift-client",
        "@redhat-cloud-services/content-sources-client",
        "@redhat-cloud-services/approval-client",
        "@redhat-cloud-services/topms-client",
        "@redhat-cloud-services/ros-client",
        "@redhat-cloud-services/cost-management-client",
        "@redhat-cloud-services/subscriptions-client",
        "@redhat-cloud-services/swatch-client",
        "@redhat-cloud-services/image-builder-client",
        "@redhat-cloud-services/vulnerability-client",
        "@redhat-cloud-services/provisioning-client",
        "@redhat-cloud-services/patch-advisory-client",
        "@redhat-cloud-services/quickstarts-client",
        "@redhat-cloud-services/notifications-backend-client",
        "@redhat-cloud-services/landing-page-frontend",
        "@redhat-cloud-services/frontend-components",
        "@redhat-cloud-services/frontend-components-utilities",
        "@redhat-cloud-services/frontend-components-notifications"
      ],
      "credential_risk": ["GitHub", "npm", "cloud", "CI/CD", "developer endpoint"],
      "known_behaviors": [
        "npm install lifecycle script execution",
        "GitHub workflow tampering",
        "credential harvesting",
        "destructive fallback behavior after token invalidation"
      ],
      "primary_sources": [
        "https://www.stepsecurity.io/blog/multiple-redhat-cloud-services-npm-packages-compromised",
        "https://www.ox.security/blog/new-npm-supply-chain-attack-redhat-cloud-services-compromised",
        "https://www.mend.io/blog/miasma-malware-analysis-a-deep-dive-into-the-red-hat-npm-supply-chain-attack/",
        "https://www.bleepingcomputer.com/news/security/red-hat-npm-packages-compromised-to-steal-developer-credentials/"
      ]
    }

    Indicators of Compromise

    packages:
      - "@redhat-cloud-services/patch-client@4.0.4"
      - "@redhat-cloud-services/insights-client@3.0.3"
      - "@redhat-cloud-services/host-inventory-client@2.0.4"
      - "@redhat-cloud-services/vulnerabilities-client@2.0.3"
      - "@redhat-cloud-services/vulnerabilities-client@2.0.4"
      - "@redhat-cloud-services/remediations-client@4.0.3"
      - "@redhat-cloud-services/sources-client@3.0.4"
      - "@redhat-cloud-services/compliance-client@3.0.4"
      - "@redhat-cloud-services/rbac-client@2.0.3"
      - "@redhat-cloud-services/advisor-client@4.0.3"
      - "@redhat-cloud-services/notifications-client@3.0.3"
      - "@redhat-cloud-services/integrations-client@2.0.4"
      - "@redhat-cloud-services/drift-client@3.0.3"
      - "@redhat-cloud-services/content-sources-client@4.0.4"
      - "@redhat-cloud-services/approval-client@2.0.3"
      - "@redhat-cloud-services/topms-client@2.0.4"
      - "@redhat-cloud-services/ros-client@2.0.4"
      - "@redhat-cloud-services/cost-management-client@3.0.4"
      - "@redhat-cloud-services/subscriptions-client@3.0.4"
      - "@redhat-cloud-services/swatch-client@2.0.3"
      - "@redhat-cloud-services/image-builder-client@3.0.3"
      - "@redhat-cloud-services/vulnerability-client@2.0.4"
      - "@redhat-cloud-services/provisioning-client@2.0.3"
      - "@redhat-cloud-services/patch-advisory-client@2.0.3"
      - "@redhat-cloud-services/quickstarts-client@2.0.3"
      - "@redhat-cloud-services/notifications-backend-client@2.0.4"
      - "@redhat-cloud-services/landing-page-frontend@2.0.3"
      - "@redhat-cloud-services/frontend-components@6.0.4"
      - "@redhat-cloud-services/frontend-components-utilities@4.0.4"
      - "@redhat-cloud-services/frontend-components-notifications@3.0.4"
    files:
      - "package.json"
      - "package-lock.json"
      - "pnpm-lock.yaml"
      - "yarn.lock"
      - "bun.lock"
      - "index.js"
      - ".github/workflows/codeql.yml"
    paths:
      - "RedHatInsights/javascript-clients"
      - ".github/workflows"
      - "node_modules/@redhat-cloud-services"
    domains:
      - "registry[.]npmjs[.]org"
      - "api[.]github[.]com"
      - "github[.]com"
    urls:
      - "hxxps://github[.]com/RedHatInsights/javascript-clients"
    telemetry_selectors:
      - "Miasma"
      - "The Spreading Blight"
      - "IfYouInvalidateThisTokenItWillNukeTheComputerOfTheOwner"
      - "firedalazer"
      - "chore/add-codeql-static-analysis"
      - "BatchedCreateCommitOnBranch"
      - "bypass_2fa"
      - "Runner.Worker"
      - "/proc/*/mem"
      - "trusted publishing"
      - "id-token: write"
    process_patterns:
      - "npm install executing lifecycle script from @redhat-cloud-services package"
      - "node or bun process launched from package lifecycle hook"
      - "workflow run using id-token: write and npm trusted publishing"
    network_patterns:
      - "GitHub API activity from developer or CI host after package install"
      - "npm publish or dist-tag activity tied to trusted-publishing workflow"

    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-redhat-cloud-services-npm-miasma-compromise-scope"))
    SINCE = "2026-06-02T00:00:00Z"
    UNTIL = "2026-06-02T23:59:59Z"
    
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    FILES = [
      "package.json",
      "package-lock.json",
      "pnpm-lock.yaml",
      "yarn.lock",
      "bun.lock",
      "index.js",
      ".github/workflows/codeql.yml",
    ]
    DOMAINS = [
      "registry.npmjs.org",
      "api.github.com",
      "github.com",
    ]
    URLS = [
      "https://github.com/RedHatInsights/javascript-clients",
    ]
    IPS = [
    ]
    HASHES = [
    ]
    PROCESS_PATTERNS = [
      "npm install executing lifecycle script from @redhat-cloud-services package",
      "node or bun process launched from package lifecycle hook",
      "workflow run using id-token: write and npm trusted publishing",
    ]
    NETWORK_PATTERNS = [
      "GitHub API activity from developer or CI host after package install",
      "npm publish or dist-tag activity tied to trusted-publishing workflow",
    ]
    
    # 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)
            for package in PACKAGES:
                if not package: continue
                safe_name = package.replace("/", "__")
                print(f"[+] Querying npm view for {package}...")
                res = subprocess.run(["npm", "view", package, "name", "version", "time", "versions", "dist-tags", "maintainers", "dist.tarball", "dist.integrity", "scripts", "--json"], capture_output=True, text=True)
                if res.returncode == 0:
                    (registry_dir / f"npm-{safe_name}.json").write_text(res.stdout)
    
    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-06-02T00:00:00Z"
    UNTIL = "2026-06-02T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-redhat-cloud-services-npm-miasma-compromise-github-audit"))
    
    SELECTORS = [
      "package.json",
      "package-lock.json",
      "pnpm-lock.yaml",
      "yarn.lock",
      "bun.lock",
      "index.js",
      ".github/workflows/codeql.yml",
      "registry.npmjs.org",
      "api.github.com",
      "github.com",
      "https://github.com/RedHatInsights/javascript-clients",
    ]
    
    # 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-06-02T00:00:00Z"
    UNTIL = "2026-06-02T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-redhat-cloud-services-npm-miasma-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-06-02T00:00:00Z"
    OUT = Path(os.environ.get("OUT", "hp-redhat-cloud-services-npm-miasma-compromise-registry-audit"))
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    
    # Positive signal: registry metadata, package tarballs, or cached artifacts contain the exact affected package/version values.
    # Remediation trigger: any internal package cache, build artifact, or deployment using these package/version values requires exposure scoping.
    
    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. Audit npm dependencies in lockfiles/package.json
        print("[+] Scanning lockfiles for npm selectors...")
        for file in ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "package.json"]:
            if Path(file).exists():
                subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), file])
    
        # 2. Query registry metadata and fetch tarballs for local analysis
        metadata_dir = OUT / "metadata"
        tarballs_dir = OUT / "tarballs"
        metadata_dir.mkdir(exist_ok=True)
        tarballs_dir.mkdir(exist_ok=True)
        for package in PACKAGES:
            if not package: continue
            safe_name = package.replace("/", "__")
            print(f"[+] Querying npm view for {package}...")
            res = subprocess.run(["npm", "view", package, "time", "versions", "dist-tags", "maintainers", "dist.tarball", "dist.integrity", "scripts", "--json"], capture_output=True, text=True)
            if res.returncode == 0:
                (metadata_dir / f"npm-{safe_name}.json").write_text(res.stdout)
            subprocess.run(["npm", "pack", package, "--pack-destination", str(tarballs_dir)], capture_output=True)
    
        # 3. HOW TO REVOKE AND ROTATE EXPOSED NPM PUBLISHING TOKENS:
        # Revoke all compromised tokens via npm CLI:
        # subprocess.run(["npm", "token", "list"])
        # subprocess.run(["npm", "token", "revoke", "123456"])
        # Or logout to revoke the current session:
        # subprocess.run(["npm", "logout"])
        # Generate a new publishing token with MFA protection:
        # subprocess.run(["npm", "token", "create", "--read-only=false", "--cidr=0.0.0.0/0"])
    
    print(f"[+] Wrote registry audit artifacts under {OUT}")

    Remediation and Closure

    Remove affected packages from lockfiles, purge package-manager caches, rebuild containers from clean dependency state, and block affected package/version pairs in private registry policy. For environments that installed the malicious versions, rotate credentials from a clean host before reconnecting the original asset.

    Closure requires negative evidence across repositories, package caches, build logs, CI runner artifacts, endpoint telemetry, and internal npm mirrors. If telemetry is unavailable, close the gap with broad credential rotation and a documented residual-risk decision.

    Sources

    1. StepSecurity: Multiple Red Hat Cloud Services npm Packages Compromised
    2. OX Security: New npm Supply Chain Attack - Red Hat Cloud Services Compromised
    3. Mend: Miasma Malware Analysis - Red Hat npm Supply Chain Attack
    4. BleepingComputer: Red Hat npm packages compromised to steal developer credentials

    IOC Clipboard

    14 IOCs
    Defang IOCs
    domain registry.npmjs.org registry[.]npmjs[.]org
    domain api.github.com api[.]github[.]com
    domain github.com github[.]com
    url https://github.com/RedHatInsights/javascript-clients hxxps://github[.]com/RedHatInsights/javascript-clients
    file package.json package.json
    file package-lock.json package-lock.json
    file pnpm-lock.yaml pnpm-lock.yaml
    file yarn.lock yarn.lock
    file bun.lock bun.lock
    file index.js index.js
    file .github/workflows/codeql.yml .github/workflows/codeql.yml
    path RedHatInsights/javascript-clients RedHatInsights/javascript-clients
    path .github/workflows .github/workflows
    path node_modules/@redhat-cloud-services node_modules/@redhat-cloud-services