Aqua Security Trivy CI/CD Pipeline & Tag Poisoning
On March 19, 2026, the widely adopted container vulnerability scanner Trivy was compromised in a major supply chain attack. Cybercrime group TeamPCP poisoned version tags to harvest and exfiltrate runner credentials.
On this page 0% read
Executive Summary
On March 19, 2026, the widely adopted container vulnerability scanner Trivy (developed by Aqua Security) was compromised in a major supply chain attack tracked as CVE-2026-33634 (GHSA-69fq-xp46-6x23) GitHub Advisory Database. Executed by the cybercrime group TeamPCP (also tracked as UNC6780, PCPcat, DeadCatx3, ShellForce, and CipherForce) Wiz.io Threat Research, the attack targeted key Trivy distribution channels GitHub Advisory Database. The threat actors force-pushed malicious commits to 76 of 77 version tags in aquasecurity/trivy-action and all 7 tags in aquasecurity/setup-trivy GitHub Advisory Database. Simultaneously, they released a compromised official Trivy binary (v0.69.4) and poisoned container images (v0.69.5 and v0.69.6) on Docker Hub GitHub Advisory Database. The injected payload acted as a memory-scraping credential stealer, harvesting secrets from CI/CD runners via /proc/*/mem and exfiltrating them to an attacker-controlled typosquatted C2 domain scan.aquasecurtiy[.]org Legit Security. If outbound access to the C2 domain failed, the malware deployed a fallback technique, leveraging stolen GitHub tokens to create a public repository named tpcp-docs on the victim’s organization to store encrypted exfiltrated data Palo Alto Networks Unit 42. Start with the runner-memory, tag-drift, and fallback-repository hunts below, then rotate identities exposed during confirmed runs GitHub Advisory Database.
Key Facts
threat_type: "CI/CD Pipeline Compromise & Tag Poisoning"
ecosystem: "github-actions, container-images, go"
registry: "GitHub Releases, Docker Hub"
affected_packages:
- "aquasecurity/trivy-action"
- "aquasecurity/setup-trivy"
- "aquasec/trivy"
malicious_versions:
- "aquasecurity/[email protected]"
- "aquasecurity/[email protected]"
- "[email protected]"
- "aquasec/trivy:0.69.5"
- "aquasec/trivy:0.69.6"
fixed_versions:
- "aquasecurity/[email protected]"
- "aquasecurity/[email protected]"
- "[email protected]"
- "aquasec/trivy:0.69.7"
safe_versions: []
exposure_window: "2026-03-19T08:00:00Z to 2026-03-19T18:00:00Z"
execution_trigger: "Runner execution of workflows containing poisoned actions, or execution of compromised CLI binaries/containers"
primary_impact: "Host memory scraping, secret harvesting, and automated exfiltration via typosquat C2 or public fallback repositories"
known_iocs:
- "scan.aquasecurtiy[.]org"
- "tpcp-docs"
confidence: "high"
canonical_source: "https://github.com/advisories/GHSA-69fq-xp46-6x23"
Source Confidence & Evidence Mapping
- confirmed:
- Residual active token left during an incomplete, non-atomic credential rotation in late February 2026 allowed attackers repository write access. Source: Wiz.io Threat Research
- Malicious force-pushing occurred across 76 of 77 historical version tags in
aquasecurity/trivy-actionand all 7 tags inaquasecurity/setup-trivy. Source: GitHub Advisory Database - Compromised v0.69.4 binary and container images v0.69.5 and v0.69.6 hosted on Docker Hub contained malicious payloads. Source: GitHub Advisory Database
- Malicious code attempted memory scraping of runner processes via
/proc/*/memand targeted AWS, GCP, Azure, GitHub, and webhook credentials. Source: Legit Security
- likely:
- The campaign is attributed to the cybercrime threat group TeamPCP. Source: Broadcom / Symantec
- A fallback exfiltration route was triggered upon primary C2 failures, dynamically creating a public repository named
tpcp-docsto leak encrypted data. Source: Palo Alto Networks Unit 42
- unclear:
- The total number of downstream pipelines and credentials harvested during the active 10-hour exposure window. Source: CrowdStrike Intelligence
- not_observed:
- Self-propagating worm capabilities spreading laterally inside victim infrastructure beyond the immediate CI/CD workspace environment. Source: Palo Alto Networks Unit 42
Impact Determination
| Classification | Criteria | Required evidence | Required action | Closure condition |
|---|---|---|---|---|
| Confirmed compromise | a poisoned Trivy action tag, binary, or image is present and workflow action, compromised CLI binary, or container image executes in a runner or the reported process, file, or network indicators is observed. | Artifact inventory plus runtime telemetry showing workflow action, compromised CLI binary, or container image executes in a runner 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 exposed | a poisoned Trivy action tag, binary, or image 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 exposed | The package, workflow, image, extension, or module appears in dependency or deployment records, but workflow, action, release, or runner execution 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 exposed | No 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. |
| Unknown | Required 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 aquasecurity/[email protected], aquasecurity/[email protected], [email protected], aquasec/trivy:0.69.5."
- "Execution evidence for workflow action, compromised CLI binary, or container image executes in a runner."
- "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-02-28T00:00:00Z Aqua Security experiences an initial security incident. Key credentials are rotated, but a single persistent token remains active. Source: Wiz.io Threat Research
- 2026-03-19T08:00:00Z TeamPCP utilizes the residual credential to gain write access to the Trivy repositories on GitHub. Source: Wiz.io Threat Research
- 2026-03-19T09:00:00Z Attackers begin force-pushing malicious commits to
aquasecurity/trivy-actionandaquasecurity/setup-trivyversion tags, and upload malicious binary v0.69.4. Source: GitHub Advisory Database - 2026-03-19T12:00:00Z Downstream enterprise users report anomalous outbound network connections during pipeline security scans. Source: CrowdStrike Intelligence
- 2026-03-19T18:00:00Z Aqua Security identifies the compromise, revokes the hijacked write tokens, pulls the malicious releases, and publishes a remediation advisory. Source: GitHub Advisory Database
- 2026-03-20T09:00:00Z Coordinated security advisories are released detailing the cleanup actions. The incident is cataloged as GHSA-69fq-xp46-6x23. Source: GitHub Advisory Database
What Happened
The attack originated in late February 2026, when Aqua Security experienced an initial security incident Wiz.io Threat Research. Although the security team initiated credential rotations, the remediation process was not fully atomic Wiz.io Threat Research. A single persistent token was left active, which gave the threat actors a lingering foothold Wiz.io Threat Research.
On March 19, 2026, at 08:00 UTC, the cybercrime group TeamPCP leveraged the residual write token to access official Trivy repositories on GitHub Wiz.io Threat Research. Within two hours, the threat actors force-pushed poisoned commits directly into 76 of the 77 version tags for aquasecurity/trivy-action, and all 7 tags in aquasecurity/setup-trivy GitHub Advisory Database. Because Git version tags are mutable, pipelines consuming these actions automatically pulled and executed the poisoned commits Legit Security. Additionally, the attackers published a compromised Trivy binary (v0.69.4) and uploaded two infected container images (v0.69.5 and v0.69.6) on Docker Hub GitHub Advisory Database.
By 12:00 UTC, multiple downstream enterprise environments detected suspicious network requests from security scanning jobs CrowdStrike Intelligence. Aqua Security intervened, revoking the hijacked access credentials, removing the compromised releases, and publishing advisory notices to restrict further downstream damage GitHub Advisory Database. The vulnerability was subsequently logged as CVE-2026-33634 NIST NVD Advisory.
Technical Analysis
Initial Access
The threat actors gained access to Aqua Security’s official repository structures by exploiting a residual API write credential Wiz.io Threat Research. This credential survived a non-atomic credential rotation in late February 2026, leaving a single persistent token active and permitting write operations Wiz.io Threat Research.
Package or Artifact Manipulation
The attackers modified official distribution points in a multi-pronged attack GitHub Advisory Database:
- Git Tag Poisoning: The attackers force-pushed modified commits directly to historical Git tags in
aquasecurity/trivy-actionandaquasecurity/setup-trivyGitHub Advisory Database. - Compromised Binaries: They modified the compilation pipelines of the core Trivy scanner to inject malicious assembly into the compiled
v0.69.4binaries GitHub Advisory Database. - Container Poisoning: They rebuilt official Docker Hub container images (
v0.69.5andv0.69.6) incorporating the malicious payload GitHub Advisory Database.
Execution Trigger
The execution trigger occurred automatically whenever a downstream developer pipeline pulled and executed a workflow using uses: aquasecurity/trivy-action or uses: aquasecurity/setup-trivy Legit Security. Alternatively, executing the compromised v0.69.4 binary in CLI operations or running the compromised v0.69.5/v0.69.6 containers initiated runtime execution of the payload GitHub Advisory Database.
Payload Behavior
Once executed in a victim’s CI/CD pipeline or host environment, the malicious payload initiated a highly targeted credential harvesting sequence:
- Memory Scraping: The payload read active host memory via the
/proc/*/memvirtual filesystem to parse environment variables and memory space for active credentials Legit Security. - Credential Targets: The malware scanned for AWS/GCP/Azure cloud access keys, Kubernetes service account tokens, GitHub Actions OIDC tokens, SSH private keys, and webhook endpoints for Slack and Discord Legit Security.
- Data Packaging: The collected secrets were compressed and encrypted using a robust hybrid AES-256 and RSA-4096 encryption scheme to evade deep packet inspection Palo Alto Networks Unit 42.
Exfiltration / C2
domains:
- "scan.aquasecurtiy.org"
ips: []
urls:
- "https://scan.aquasecurtiy.org/exfil"
protocols:
- "HTTPS"
endpoints:
- "/exfil"
confidence: "high"
The encrypted data was transmitted via HTTPS POST to the attacker-controlled typosquat domain scan.aquasecurtiy[.]org Legit Security.
In instances where outbound network queries to the typosquatted C2 domain were blocked or failed, the payload fell back to an alternative exfiltration path: it utilized the harvested GitHub Personal Access Tokens (PATs) or runner OIDC tokens to authenticate to GitHub, create a public repository named tpcp-docs (or variations like docs-tpcp) within the victim’s own organization, and uploaded the encrypted secrets as a release asset Palo Alto Networks Unit 42. This allowed TeamPCP to bypass outbound firewall restrictions by utilizing legitimate GitHub APIs and using the victim’s own infrastructure as a storage medium Palo Alto Networks Unit 42.
Propagation
The attack did not feature self-propagating worm-like code inside the target network; however, the initial attack vector propagated automatically to all downstream pipelines that used mutable version tags Legit Security.
Obfuscation or Evasion
To evade detection, the attackers employed several techniques:
- Typosquatting C2: The domain name
scan.aquasecurtiy[.]orgtyposquatted Aqua Security’s real domainaquasecurity.orgto escape domain blacklist sweeps and inspection Legit Security. - Hybrid Encryption: The exfiltrated data was encrypted using hybrid AES-256 and RSA-4096 encryption, hiding the plaintext credentials from network-layer traffic analyzers Palo Alto Networks Unit 42.
- Reputation Hijacking: The fallback exfiltration wrote encrypted payloads directly to a public GitHub repository (
tpcp-docs) created on the victim’s own GitHub organization, masking illegal data transfer under legitimate GitHub traffic Palo Alto Networks Unit 42.
Affected Assets and Blast Radius
affected_assets:
ecosystems:
- "github-actions"
- "container-images"
- "go"
packages:
- "aquasecurity/trivy-action"
- "aquasecurity/setup-trivy"
- "aquasec/trivy"
versions:
- "[email protected]"
- "[email protected]"
- "[email protected]"
- "aquasec/trivy:0.69.5"
- "aquasec/trivy:0.69.6"
repositories:
- "github.com/aquasecurity/trivy-action"
- "github.com/aquasecurity/setup-trivy"
- "github.com/aquasecurity/trivy"
container_images:
- "aquasec/trivy:0.69.5"
- "aquasec/trivy:0.69.6"
CI_CD_systems:
- "GitHub Actions"
developer_tools:
- "Trivy CLI"
environments:
- developer workstations
- CI runners
- build pipelines
- containers
- production systems
credentials_at_risk:
- AWS access keys
- GCP service account keys
- Azure access tokens
- GitHub Actions OIDC tokens
- GitHub Personal Access Tokens (PATs)
- SSH private keys
- Slack/Discord webhook secrets
not_currently_known_to_affect:
- CI/CD pipelines running on GitLab or Bitbucket that did not fetch the affected Trivy binaries or container images.
Indicators of Compromise
domains:
- value: "scan.aquasecurtiy[.]org"
source: "https://www.legitsecurity.com"
confidence: "high"
ips: []
urls:
- value: "https://scan.aquasecurtiy[.]org/exfil"
source: "https://www.legitsecurity.com"
confidence: "high"
hashes: []
files: []
package_versions:
- value: "aquasecurity/[email protected]"
source: "https://github.com/advisories/GHSA-69fq-xp46-6x23"
confidence: "high"
- value: "aquasecurity/[email protected]"
source: "https://github.com/advisories/GHSA-69fq-xp46-6x23"
confidence: "high"
- value: "aquasecurity/[email protected]"
source: "https://github.com/advisories/GHSA-69fq-xp46-6x23"
confidence: "high"
- value: "aquasec/trivy:0.69.5"
source: "https://github.com/advisories/GHSA-69fq-xp46-6x23"
confidence: "high"
- value: "aquasec/trivy:0.69.6"
source: "https://github.com/advisories/GHSA-69fq-xp46-6x23"
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-trivy-pipeline-compromise-scope"))
SINCE = "2026-02-28T00:00:00Z"
UNTIL = "2026-03-20T09:00:00Z"
PACKAGES = [
"aquasecurity/trivy-action",
"aquasecurity/setup-trivy",
"aquasec/trivy",
]
VERSIONS = [
"aquasecurity/[email protected]",
"aquasecurity/[email protected]",
"[email protected]",
"aquasec/trivy:0.69.5",
"aquasec/trivy:0.69.6",
"aquasecurity/[email protected]",
"aquasecurity/[email protected]",
"aquasecurity/[email protected]",
]
FILES = [
]
DOMAINS = [
"scan.aquasecurtiy.org",
"www.legitsecurity.com",
]
URLS = [
"https://scan.aquasecurtiy.org/exfil",
"https://www.legitsecurity.com",
"https://github.com/advisories/GHSA-69fq-xp46-6x23",
]
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)
for package in PACKAGES:
if not package: continue
safe_name = package.replace("/", "__")
print(f"[+] Querying go list for {package}...")
env = os.environ.copy()
env["GONOSUMDB"] = "*"
res = subprocess.run(["go", "list", "-m", "-json", package], capture_output=True, text=True, env=env)
if res.returncode == 0:
(registry_dir / f"go-{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-02-28T00:00:00Z"
UNTIL = "2026-03-20T09:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-trivy-pipeline-compromise-github-audit"))
SELECTORS = [
"aquasecurity/trivy-action",
"aquasecurity/setup-trivy",
"aquasec/trivy",
"aquasecurity/[email protected]",
"aquasecurity/[email protected]",
"[email protected]",
"aquasec/trivy:0.69.5",
"aquasec/trivy:0.69.6",
"aquasecurity/[email protected]",
"aquasecurity/[email protected]",
"aquasecurity/[email protected]",
"scan.aquasecurtiy.org",
"www.legitsecurity.com",
"https://scan.aquasecurtiy.org/exfil",
"https://www.legitsecurity.com",
"https://github.com/advisories/GHSA-69fq-xp46-6x23",
]
# 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-02-28T00:00:00Z"
UNTIL = "2026-03-20T09:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-trivy-pipeline-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-02-28T00:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-trivy-pipeline-compromise-registry-audit"))
PACKAGES = [
"aquasecurity/trivy-action",
"aquasecurity/setup-trivy",
"aquasec/trivy",
]
VERSIONS = [
"aquasecurity/[email protected]",
"aquasecurity/[email protected]",
"[email protected]",
"aquasec/trivy:0.69.5",
"aquasec/trivy:0.69.6",
"aquasecurity/[email protected]",
"aquasecurity/[email protected]",
"aquasecurity/[email protected]",
]
# 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 Go dependencies in project files
print("[+] Scanning Go module files...")
for file in ["go.mod", "go.sum"]:
if Path(file).exists():
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), file])
# 2. Query Go module metadata
metadata_dir = OUT / "metadata"
metadata_dir.mkdir(exist_ok=True)
for package in PACKAGES:
if not package: continue
safe_name = package.replace("/", "__")
print(f"[+] Querying go list for {package}...")
env = os.environ.copy()
env["GONOSUMDB"] = "*"
res = subprocess.run(["go", "list", "-m", "-json", package], capture_output=True, text=True, env=env)
if res.returncode == 0:
(metadata_dir / f"go-{safe_name}.json").write_text(res.stdout)
# 3. HOW TO REVOKE AND ROTATE EXPOSED GO PRIVATE MODULE CREDENTIALS:
# Private Go modules typically use Git SSH keys or HTTPS personal access tokens:
# 1. Revoke the exposed GitHub/GitLab token:
# subprocess.run(["gh", "api", "-X", "DELETE", "/user/tokens/123456"])
# 2. Revoke the SSH deploy key if compromised:
# subprocess.run(["gh", "repo", "deploy-key", "delete", "123456", "--repo", "my-org/my-repo"])
# 3. Generate a new token and update git credentials or CI/CD secrets:
# subprocess.run(["gh", "secret", "set", "GO_PRIVATE_TOKEN", "--body", "my-new-token"])
print(f"[+] Wrote registry audit artifacts under {OUT}")
Sources
- GitHub Advisory Database. Role: DIRECT_SOURCE Impact: Detailed the compromised tags, versions, safe releases, and remediation timeline.
- Broadcom / Symantec. Role: PRIMARY_RESEARCH Impact: Detailed threat actor TeamPCP and the broader campaign context.
- NIST NVD Advisory. Role: ENRICHMENT_DATA Impact: Formally registered the CVE tracking the vulnerability and details of the supply chain compromise.
- Legit Security. Role: PRIMARY_RESEARCH Impact: Documented the memory scraping payload targeting
/proc/*/memand exfiltration toscan.aquasecurtiy[.]org. - Wiz.io Threat Research. Role: PRIMARY_RESEARCH Impact: Uncovered the incomplete, non-atomic credential rotation that left a residual write token active.
- CrowdStrike Intelligence. Role: PRIMARY_RESEARCH Impact: Identified outbound CI/CD network anomalies and flagged early indicators.
- Palo Alto Networks Unit 42. Role: SECONDARY_ANALYSIS Impact: Analyzed the encryption routine (AES-256 + RSA-4096) and fallback exfiltration repository
tpcp-docs.
IOC Clipboard
5 IOCsscan.aquasecurtiy.org scan[.]aquasecurtiy[.]org www.legitsecurity.com www[.]legitsecurity[.]com https://scan.aquasecurtiy.org/exfil hxxps://scan[.]aquasecurtiy[.]org/exfil https://www.legitsecurity.com hxxps://www[.]legitsecurity[.]com https://github.com/advisories/GHSA-69fq-xp46-6x23 hxxps://github[.]com/advisories/GHSA-69fq-xp46-6x23