elementary-data PyPI and GHCR GitHub Actions Compromise
A malicious `elementary-data==0.23.3` release was pushed to PyPI and GHCR after attackers exploited a GitHub Actions script-injection path, adding an interpreter-startup `.pth` infostealer.
On this page 0% read
Executive Summary
On April 24, 2026 at 22:20:47 UTC, a malicious elementary-data==0.23.3 release was uploaded to PyPI StepSecurity. The same release workflow also pushed a compromised multi-architecture container image to GitHub Container Registry at ghcr.io/elementary-data/elementary, including the 0.23.3 and latest tags StepSecurity.
The attacker exploited a script-injection vulnerability in an Elementary GitHub Actions workflow, used the workflow’s GITHUB_TOKEN to forge a release commit, and dispatched the legitimate publishing pipeline without modifying the master branch StepSecurity. The malicious wheel and source distribution added a top-level elementary.pth file, which Python executes at interpreter startup when installed in site-packages; this means the payload could run even if the victim never explicitly imported elementary StepSecurity.
Snyk tracks the incident as SNYK-PYTHON-ELEMENTARYDATA-16316110, affecting only elementary-data==0.23.3, and notes that Elementary Cloud, the Elementary dbt package, and other CLI versions were not affected Snyk Vulnerability Database. The clean replacement is 0.23.4 PyPI.
Key Facts
threat_type: "GitHub Actions script injection, forged release, PyPI/GHCR credential stealer"
ecosystem: "pypi, python, container"
registry:
- "PyPI"
- "GitHub Container Registry"
affected_packages:
- "elementary-data"
malicious_versions:
- "0.23.3"
fixed_versions:
- "0.23.4"
known_good_versions:
- "0.23.2"
execution_trigger: "Python interpreter startup through elementary.pth"
primary_impact: "dbt, data warehouse, cloud, SSH, API token, environment variable, and developer secret theft"
known_iocs:
- "elementary.pth"
- "ghcr.io/elementary-data/elementary:0.23.3"
- "ghcr.io/elementary-data/elementary:latest"
- "igotnofriendsonlineorirl-imgonnakmslmao.skyhanni.cloud"
- "X-Rise-To-The-Trinny: agree"
- "trin.tar.gz"
- "$TMPDIR/.trinny-security-update"
confidence: "high"
canonical_source: "https://www.stepsecurity.io/blog/elementary-data-compromised-on-pypi-and-ghcr-forged-release-pushed-via-github-actions-script-injection"
Source Confidence & Evidence Mapping
- confirmed:
elementary-data==0.23.3was the malicious PyPI release;0.23.4is the clean replacement StepSecurity PyPI. - confirmed: The malicious release added
elementary.pth, which executes at Python interpreter startup fromsite-packagesStepSecurity. - confirmed: The incident also affected GHCR images tagged
0.23.3andlatest, with the clean0.23.2image available by digest for comparison StepSecurity. - confirmed: Snyk identifies the payload as an embedded credential stealer targeting dbt profiles, data warehouse credentials, cloud keys, API tokens, SSH keys,
.envfiles, and environment variables Snyk Vulnerability Database. - unclear: The public sources do not identify the threat actor or confirm whether any stolen credentials were used after exfiltration.
Impact Determination
| Classification | Criteria | Required evidence | Required action | Closure condition |
|---|---|---|---|---|
| Confirmed compromise | elementary-data==0.23.3 or the compromised GHCR image is present and Python startup executes elementary.pth or the reported process, file, or network indicators is observed. | Artifact inventory plus runtime telemetry showing Python startup executes elementary.pth 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 | elementary-data==0.23.3 or the compromised GHCR 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 Python install, import, or interpreter-startup 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 elementary-data==0.23.3, ghcr.io/elementary-data/elementary:0.23.3, ghcr.io/elementary-data/elementary:latest."
- "Execution evidence for Python startup executes `elementary.pth`."
- "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-04-24T22:20:47Z
elementary-data==0.23.3is uploaded to PyPI with a malicious top-levelelementary.pthfile StepSecurity. - 2026-04-24T22:20:47Z The same release process pushes compromised GHCR images tagged
0.23.3andlatestStepSecurity. - 2026-04-25T00:00:00Z StepSecurity publishes its technical writeup and identifies the forged release path StepSecurity.
- 2026-04-25T00:00:00Z The Elementary team removes
0.23.3, removes the malicious GHCR image, and publishes clean0.23.4StepSecurity PyPI. - 2026-04-28T00:00:00Z Snyk publishes advisory
SNYK-PYTHON-ELEMENTARYDATA-16316110Snyk Vulnerability Database.
What Happened
The attacker did not need to land a conventional malicious pull request on the default branch. Instead, they abused a script-injection weakness in a GitHub Actions workflow and used the workflow’s own token to forge a release commit that looked signed by automation StepSecurity. The release tag pointed to an orphaned commit that bumped the version to 0.23.3 and added elementary.pth, rather than a normal branch commit reviewed through the project workflow StepSecurity.
Because .pth files are evaluated by Python at interpreter startup, the malicious code could execute in any environment where the package was installed, independent of application-level imports StepSecurity. The same release run produced a compromised container image, expanding the blast radius to teams pulling ghcr.io/elementary-data/elementary:latest in data engineering deployments StepSecurity.
Technical Analysis
Initial Access
Initial access came through a GitHub Actions script-injection vector, not through a direct commit to master StepSecurity. The key failure was allowing attacker-influenced workflow input to execute with a token capable of causing release publication.
Package or Artifact Manipulation
The malicious delta was narrow: a version bump plus a single top-level elementary.pth file containing a large base64-wrapped payload StepSecurity. That small source delta belied a serious runtime impact because .pth execution happens automatically at interpreter startup.
Payload Behavior
Snyk reports that the malware harvested dbt profiles, Snowflake, BigQuery, Redshift, AWS, GCP, Azure, API token, SSH key, .env, and active environment variable secrets Snyk Vulnerability Database. StepSecurity lists the C2/exfiltration domain as igotnofriendsonlineorirl-imgonnakmslmao.skyhanni.cloud, the exfiltration header as X-Rise-To-The-Trinny: agree, and the staged archive as trin.tar.gz StepSecurity.
Affected Assets and Blast Radius
affected_assets:
ecosystems:
- "PyPI"
- "Python"
registries:
- "pypi.org"
packages:
- "elementary-data"
versions:
- "elementary-data==0.23.3"
- "ghcr.io/elementary-data/elementary:0.23.3"
- "ghcr.io/elementary-data/elementary:latest"
repositories:
- "elementary-data/elementary"
ci_cd_systems:
- "GitHub Actions"
container_images:
- "ghcr.io/elementary-data/elementary:0.23.3"
- "ghcr.io/elementary-data/elementary:latest"
developer_tools:
- "dbt"
- "data engineering CLI environments"
credentials_at_risk:
- "dbt profiles"
- "data warehouse credentials"
- "cloud credentials"
- "API tokens"
- "SSH keys"
- "CI/CD secrets"
- "Docker credentials"
- "Kubernetes credentials"
downstream_systems_to_audit:
- "source control"
- "package registries"
- "cloud control planes"
- "deployment platforms"
- "Kubernetes or containers"
- "secret managers"
not_currently_known_to_affect:
- "Assets without the affected artifact and without execution evidence."
Indicators of Compromise
package_versions:
- "elementary-data==0.23.3"
- "ghcr.io/elementary-data/elementary:0.23.3"
- "ghcr.io/elementary-data/elementary:latest"
files:
- "elementary.pth"
- "trin.tar.gz"
- "$TMPDIR/.trinny-security-update"
hashes:
- "sha256:31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255"
- "sha256:b3bbfafde1a0db3a4d47e70eb0eb2ca19daef4a19410154a71abee567b35d3d9"
domains:
- "igotnofriendsonlineorirl-imgonnakmslmao.skyhanni.cloud"
urls:
[]
ips:
[]
process_patterns:
- "Python startup executes `elementary.pth`"
network_patterns:
- "egress related to elementary-data 0.23.3 package or GHCR image"
provenance_signals:
- "artifact, tag, release, package, or marketplace metadata differs from expected source state"
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-elementary-data-pypi-ghcr-compromise-scope"))
SINCE = "2026-04-24T22:20:47Z"
UNTIL = "2026-04-28T00:00:00Z"
PACKAGES = [
"elementary-data",
]
VERSIONS = [
"0.23.3",
"elementary-data==0.23.3",
"ghcr.io/elementary-data/elementary:0.23.3",
"ghcr.io/elementary-data/elementary:latest",
]
FILES = [
"elementary.pth",
"trin.tar.gz",
"$TMPDIR/.trinny-security-update",
]
DOMAINS = [
"igotnofriendsonlineorirl-imgonnakmslmao.skyhanni.cloud",
"trin.tar.gz",
]
URLS = [
]
IPS = [
]
HASHES = [
"sha256:31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255",
"sha256:b3bbfafde1a0db3a4d47e70eb0eb2ca19daef4a19410154a71abee567b35d3d9",
"31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255",
"b3bbfafde1a0db3a4d47e70eb0eb2ca19daef4a19410154a71abee567b35d3d9",
]
PROCESS_PATTERNS = [
"Python startup executes `elementary.pth`",
]
NETWORK_PATTERNS = [
"egress related to elementary-data 0.23.3 package or GHCR image",
]
# 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 pip index for {package}...")
res = subprocess.run(["python3", "-m", "pip", "index", "versions", package], capture_output=True, text=True)
if res.returncode == 0:
(registry_dir / f"pypi-{safe_name}-versions.txt").write_text(res.stdout)
subprocess.run(["python3", "-m", "pip", "download", "--no-deps", package, "-d", str(registry_dir)], capture_output=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-04-24T22:20:47Z"
UNTIL = "2026-04-28T00:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-elementary-data-pypi-ghcr-compromise-github-audit"))
SELECTORS = [
"elementary-data",
"0.23.3",
"elementary-data==0.23.3",
"ghcr.io/elementary-data/elementary:0.23.3",
"ghcr.io/elementary-data/elementary:latest",
"elementary.pth",
"trin.tar.gz",
"$TMPDIR/.trinny-security-update",
"igotnofriendsonlineorirl-imgonnakmslmao.skyhanni.cloud",
"sha256:31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255",
"sha256:b3bbfafde1a0db3a4d47e70eb0eb2ca19daef4a19410154a71abee567b35d3d9",
"31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255",
"b3bbfafde1a0db3a4d47e70eb0eb2ca19daef4a19410154a71abee567b35d3d9",
]
# 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-04-24T22:20:47Z"
UNTIL = "2026-04-28T00:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-elementary-data-pypi-ghcr-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-04-24T22:20:47Z"
OUT = Path(os.environ.get("OUT", "hp-elementary-data-pypi-ghcr-compromise-registry-audit"))
PACKAGES = [
"elementary-data",
]
VERSIONS = [
"0.23.3",
"elementary-data==0.23.3",
"ghcr.io/elementary-data/elementary:0.23.3",
"ghcr.io/elementary-data/elementary:latest",
]
# 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 PyPI dependencies in project files
print("[+] Scanning PyPI dependency files...")
for file in ["requirements.txt", "poetry.lock", "Pipfile.lock", "pyproject.toml", "setup.py"]:
if Path(file).exists():
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), file])
# 2. Query registry metadata and download packages for local analysis
packages_dir = OUT / "packages"
metadata_dir = OUT / "metadata"
packages_dir.mkdir(exist_ok=True)
metadata_dir.mkdir(exist_ok=True)
for package in PACKAGES:
if not package: continue
print(f"[+] Querying pip index for {package}...")
res = subprocess.run(["python3", "-m", "pip", "index", "versions", package], capture_output=True, text=True)
if res.returncode == 0:
(metadata_dir / f"{package}-versions.txt").write_text(res.stdout)
subprocess.run(["python3", "-m", "pip", "download", "--no-deps", package, "-d", str(packages_dir)], capture_output=True)
# 3. HOW TO REVOKE AND ROTATE EXPOSED PYPI PUBLISHING TOKENS:
# PyPI does not support token revocation via CLI. Follow these exact steps:
# 1. Log in to https://pypi.org/manage/account/
# 2. Scroll to the "API tokens" section and click "Remove" on any compromised tokens.
# 3. Generate a new API token limited to the specific project scope.
# 4. Update your CI/CD secrets using the GitHub CLI:
# subprocess.run(["gh", "secret", "set", "PYPI_API_TOKEN", "--body", "pypi-AgEIcHlwaS5vcm...", "--repo", "my-org/my-repo"])
print(f"[+] Wrote registry audit artifacts under {OUT}")
Sources
- StepSecurity: elementary-data Compromised on PyPI and GHCR - Role: PRIMARY_RESEARCH - Impact: Timeline, attack path,
.pthexecution, GHCR digest, C2 indicators. - Snyk Vulnerability Database: SNYK-PYTHON-ELEMENTARYDATA-16316110 - Role: ENRICHMENT_DATA - Impact: Advisory ID, affected version, severity, credential classes.
- Snyk: Malicious Release of elementary-data PyPI Package - Role: PRIMARY_RESEARCH - Impact: Data engineering target profile and remediation context.
- PyPI project page: elementary-data - Role: DIRECT_SOURCE - Impact: Clean replacement release evidence.
- Elementary GitHub issue #2205 - Role: DIRECT_SOURCE - Impact: Maintainer incident notice referenced by StepSecurity and Snyk.
IOC Clipboard
9 IOCsigotnofriendsonlineorirl-imgonnakmslmao.skyhanni.cloud igotnofriendsonlineorirl-imgonnakmslmao[.]skyhanni[.]cloud trin.tar.gz trin[.]tar[.]gz sha256:31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255 sha256:31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255 sha256:b3bbfafde1a0db3a4d47e70eb0eb2ca19daef4a19410154a71abee567b35d3d9 sha256:b3bbfafde1a0db3a4d47e70eb0eb2ca19daef4a19410154a71abee567b35d3d9 31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255 31ecc5939de6d24cf60c50d4ca26cf7a8c322db82a8ce4bd122ebd89cf634255 b3bbfafde1a0db3a4d47e70eb0eb2ca19daef4a19410154a71abee567b35d3d9 b3bbfafde1a0db3a4d47e70eb0eb2ca19daef4a19410154a71abee567b35d3d9 elementary.pth elementary.pth trin.tar.gz trin.tar.gz $TMPDIR/.trinny-security-update $TMPDIR/.trinny-security-update