|
1 | 1 | # Changelog |
2 | 2 |
|
3 | | -## Unreleased |
| 3 | +## 0.9.0 — 2026-05-15 |
4 | 4 |
|
5 | 5 | ### Added |
6 | | -- **Per-scan skip summary.** New `skip_report.py` module with a `SkipReport` singleton that the walk / read helpers append to whenever they swallow a `PermissionError` or `OSError`. Instrumented sites: `config.pruned_walk` (with `onerror` callback so depth-N denials are caught, not just the top-level root), `config.read_if_contains`, `git_repo_index._find_repo_roots` and its targeted file readers, `persistence_scanner` (config-dir, shell-rc, /tmp iteration), and `ioc_scanner` (known-paths exists check + walk-files hash-read). A new `print_skip_summary` block at the end of the report tells the operator how many paths were not inspected and shows the first few of each category, so a partial scan from missing privileges is no longer indistinguishable from a clean scan. Silent when no paths were skipped. Test isolation handled by an autouse fixture in `tests/conftest.py` that resets the singleton between tests. |
7 | | - |
8 | | -### Changed |
9 | | -- **Fail loud on malformed threat profiles.** `_load_from_dir` no longer swallows `KeyError` / `tomllib.TOMLDecodeError` / `re.error` from individual profiles. Any broken TOML — missing required field, syntax error, or invalid regex — now raises `InvalidThreatProfileError` with the offending file path and stops the scan. A user who wrote a profile expects it to be active; silently logging at WARNING and continuing was the kind of stealth-failure mode that §8 (Fail Fast, Fail Loud) explicitly warns against. |
10 | | -- **Regex compilation moved from scan time to load time.** `GitArtifactsIOC.workflow_name_regexes` and `branch_name_regexes` are now `tuple[re.Pattern[str], ...]` (was `tuple[str, ...]`). `_parse_git_artifacts` compiles each pattern; a malformed pattern raises `re.error` with the field name and the offending pattern. The `aggregate_indicators` function no longer needs `_compile_into` or any exception handling — patterns are pre-validated. |
11 | | - |
12 | | -### Added |
13 | | -- **`branch_name_regexes`** in `[ioc.git_artifacts]` — symmetric to `workflow_name_regexes`. Lets a profile match generated branch names that carry a timestamp or other variable component, e.g. Sha1-Hulud 2.0's `add-linter-workflow-<Date.now()>` dead-drop branches. Matches are deduplicated against the literal `branch_names` set so a branch covered by both forms emits a single finding. The `sha1-hulud-2025-11` profile now uses this to catch the 2.0-specific branch pattern. |
| 6 | +- **Anti-worm pre-pass** — single filesystem walk before the per-threat pipeline that discovers local git repositories and matches them against worm-class indicators aggregated from every loaded threat profile. Detects Shai-Hulud-style campaigns whose footprint is in `.git/description`, `.github/workflows/*.y[a]ml`, local branch names, and recent commit author emails. New modules: `git_repo_index.py`, `anti_worm_scanner.py`. New `[ioc.git_artifacts]` block in threat profiles (workflow filenames, regex patterns, branch names, commit author emails, repo descriptions). Per-repo corroboration scorer: workflow / description matches alone are HIGH; branch / author matches alone are LOW and escalate to HIGH when combined. |
| 7 | +- **Per-scan skip summary.** New `skip_report.py` module with a `SkipReport` instance that the walk / read helpers append to whenever they swallow a `PermissionError` or `OSError`. Instrumented sites: `config.pruned_walk` (with `onerror` callback so depth-N denials are caught, not just the top-level root), `config.read_if_contains`, `git_repo_index._find_repo_roots` and its targeted file readers, `persistence_scanner` (config-dir, shell-rc, /tmp iteration), and `ioc_scanner` (known-paths exists check + walk-files hash-read). A new `print_skip_summary` block at the end of the report tells the operator how many paths were not inspected and shows the first few of each category, so a partial scan from missing privileges is no longer indistinguishable from a clean scan. Silent when no paths were skipped. |
| 8 | +- **Shai-Hulud threat profile** (`shai-hulud-2025-09`) — covers the September 2025 npm worm wave anchored on `@ctrl/tinycolor@4.1.1` / `4.1.2`. Detects the `bundle.js` payload by SHA-256, the `truffleSecrets.json` / `cloud.json` / `actionsSecrets.json` / `contents.json` / `environment.json` / `format.json` staging dumps under `/tmp` (or `%TEMP%`), the `shai-hulud-workflow.yml` / `discussion.yaml` / `formatter_*.yml` backdoor workflows, the `Shai-Hulud` / `Sha1-Hulud` repo descriptions, the Dune-universe branch fingerprints, the `claude@users.noreply.github.com` commit author, and the `gh-token-monitor` host-side persistence daemon used by later waves. |
14 | 9 | - **Sha1-Hulud 2.0 threat profile** (`sha1-hulud-2025-11`) — November 2025 wave, anchored on `posthog-node` (4.18.1 / 5.11.3 / 5.13.3). Walk-files for `bun_environment.js` (three known SHA-256s) and `setup_bun.js` (one SHA-256). Adds `Sha1-Hulud: The Second Coming` / `Shai-Hulud: The Continued Coming` repo descriptions, `SHA1HULUD` self-hosted-runner keyword, and the Docker-sudoers privilege-escalation check in remediation guidance. |
15 | 10 | - **Mini Shai-Hulud threat profile** (`mini-shai-hulud-2026-05`) — TanStack wave, anchored on `@tanstack/react-router` (1.169.5 / 1.169.8). Walk-files for `router_init.js` and `tanstack_runner.js` (hash-verified). Adds the `git-tanstack.com` typosquat exfil domain, the `Shai-Hulud: Here We Go Again` repo description, and the campaign-unique PBKDF2 salt `svksjrhjkcejg` as a persistence keyword. Remediation includes auditing Claude Code / VS Code extension manifests for the post-reboot persistence hooks the worm installs. |
16 | | -- **Shai-Hulud threat profile** (`shai-hulud-2025-09`) — covers the September 2025 npm worm wave anchored on `@ctrl/tinycolor@4.1.1` / `4.1.2`. Detects the `bundle.js` payload by SHA-256, the `truffleSecrets.json` / `cloud.json` / `actionsSecrets.json` / `contents.json` / `environment.json` / `format.json` staging dumps under `/tmp` (or `%TEMP%`), the `shai-hulud-workflow.yml` / `discussion.yaml` / `formatter_*.yml` backdoor workflows, the `Shai-Hulud` / `Sha1-Hulud` repo descriptions, the Dune-universe branch fingerprints, the `claude@users.noreply.github.com` commit author, and the `gh-token-monitor` host-side persistence daemon used by later waves. Sha1-Hulud 2.0 (Nov 2025) and Mini Shai-Hulud (May 2026) are not yet shipped as separate profiles. |
17 | | -- **Anti-worm pre-pass** — single filesystem walk before the per-threat pipeline that discovers local git repositories and matches them against worm-class indicators aggregated from every loaded threat profile. Detects Shai-Hulud-style campaigns whose footprint is in `.git/description`, `.github/workflows/*.y[a]ml`, local branch names, and recent commit author emails. New modules: `git_repo_index.py`, `anti_worm_scanner.py`. New `[ioc.git_artifacts]` block in threat profiles (workflow filenames, regex patterns, branch names, commit author emails, repo descriptions). Per-repo corroboration scorer: workflow / description matches alone are HIGH; branch / author matches alone are LOW and escalate to HIGH when combined. |
| 11 | +- **`branch_name_regexes`** in `[ioc.git_artifacts]` — symmetric to `workflow_name_regexes`. Lets a profile match generated branch names that carry a timestamp or other variable component, e.g. Sha1-Hulud 2.0's `add-linter-workflow-<Date.now()>` dead-drop branches. Matches are deduplicated against the literal `branch_names` set so a branch covered by both forms emits a single finding. The `sha1-hulud-2025-11` profile now uses this to catch the 2.0-specific branch pattern. |
18 | 12 | - **`[ioc.persistence_keywords].terms`** — extra terms (e.g. standalone daemon names like `gh-token-monitor`) that the persistence scanner matches in addition to the package name. Catches payloads whose persistence artifacts don't carry the parent package's name. |
19 | 13 | - New `FindingCategory.GIT_ARTIFACT`; scoring rules updated so a git artifact alone yields MEDIUM and version + git artifact yields HIGH. |
| 14 | +- `ruff` and `mypy` declared as dev dependencies. |
20 | 15 |
|
21 | 16 | ### Changed |
22 | | -- `search_roots._deduplicate_roots` renamed to `deduplicate_roots` (now used by the anti-worm pre-pass to union ecosystem roots). |
| 17 | +- **Fail loud on malformed threat profiles.** `_load_from_dir` no longer swallows `KeyError` / `tomllib.TOMLDecodeError` / `re.error` from individual profiles. Any broken TOML — missing required field, syntax error, or invalid regex — now raises `InvalidThreatProfileError` with the offending file path and stops the scan. A user who wrote a profile expects it to be active; silently logging at WARNING and continuing was the kind of stealth-failure mode that §8 (Fail Fast, Fail Loud) explicitly warns against. |
| 18 | +- **Regex compilation moved from scan time to load time.** `GitArtifactsIOC.workflow_name_regexes` and `branch_name_regexes` are now `tuple[re.Pattern[str], ...]` (was `tuple[str, ...]`). `_parse_git_artifacts` compiles each pattern; a malformed pattern raises `re.error` with the field name and the offending pattern. The `aggregate_indicators` function no longer needs `_compile_into` or any exception handling — patterns are pre-validated. |
| 19 | +- **Strict unknown-key detection in threat-profile TOML.** Typos like `commands` instead of `command` no longer silently produce a half-loaded profile. |
| 20 | +- **Hardened ss / lsof IPv6 peer parsing** to handle bracketed-port and zone-id forms correctly. |
| 21 | +- **Internal scan plumbing simplified.** `ScanContext` dataclass collapses the prior 6-parameter clump across phase signatures. Module-level mutable state eliminated: `SkipReport` now plumbed explicitly, `_ecosystem_cache` replaced with `functools.lru_cache`, autouse reset fixture removed. Phase-3 IOC dispatch hoisted out of `ioc_scanner` into `scanner._run_phase3_iocs`. Long functions decomposed (`scanner.main`, `_scan_for_c2_connections`, `_scan_walk_files`, `scan_source_and_configs`); `main()` now returns an `int`. The 369-line `report.py` split into a `report/` package with five sub-modules. Lazy `_user_threat_dir()`. `search_roots._deduplicate_roots` renamed to `deduplicate_roots`. |
| 22 | +- **Walker / reader skips routed through `SkipReport` consistently**, and NXDOMAIN coverage surfaced on `--resolve-c2`. |
| 23 | +- **Complete type annotations** on every public entry point and on every plumbed `skip_report` parameter. The production package and the test suite both pass `mypy --strict` (37 + 27 source files, 0 errors). |
| 24 | +- **Cross-platform test portability.** The `os.geteuid()` skipif guards now wrap a `hasattr(os, "geteuid")` check so test collection no longer crashes on Windows. The anti-worm corroboration test compares against `str(Path(...))` rather than literal `/repo/A` strings, matching the scanner's actual output on every OS. The `_check_tmp_scripts` test class is explicitly marked POSIX-only, mirroring the scanner's own `sys.platform != "win32"` guard. |
| 25 | + |
| 26 | +## 0.8.2 — 2026-04-02 |
23 | 27 |
|
24 | 28 | ## 0.8.2 — 2026-04-02 |
25 | 29 |
|
|
0 commit comments