ShakerScan 2.10.0

The client for this release is shakerscan 0.10.0 (shakerscan secrets).

Exposure is the service view

The Exposure page now shows only its service view: per target, each listener, the product and version observed on it, and the weaknesses known for that version. The Triage, Map and Attack paths views are removed: they restated what the Targets and Findings pages show, and their rankings were not decisions an operator could act on. An old /exposure?lens=... link opens the service view.

  • Editing a target's owner, environment and reporting cohort moved to the target page (/targets/{id}, Edit details). It was only possible from the removed triage drawer. The target detail API (GET /targets/{id}/asset) now returns owner and cohort.
  • The dashboard's "What changed" links for new and stale assets open the Targets page (newest first, or scanned targets) instead of the removed triage view; a domain scope becomes the Targets search.
  • API: GET /exposure/nodes and GET /exposure/attack-paths are removed (only the removed views called them). GET /exposure/graph (an Arsenal read for agents), GET /exposure/assets (the dashboard's posture), GET /exposure/changes and GET /exposure/services are unchanged.

Known vulnerabilities per service version

  • A real vulnerability snapshot. The pinned snapshot the service view matches against now covers the network and web services the scanner fingerprints (OpenSSH, Apache httpd, nginx, IIS, Tomcat, PHP, MySQL/MariaDB, PostgreSQL, Redis, Elasticsearch, Jenkins, mail, DNS and file servers, common edge appliances, and the embedded products it already covered), generated from NVD at release time. Before, it held 24 curated records, so almost every listener showed no candidates.
  • Exploitation context on each candidate: the CVSS base score, whether CISA lists it as known exploited (with the date added and ransomware use), the EPSS likelihood of exploitation in the next 30 days, and the nuclei checks this release ships for that CVE. Candidates are ordered known exploited first, then likelihood, then severity, and each listener shows how many are known exploited. All of it is metadata in the pinned snapshot: nothing is fetched at run time and nothing is executed from it.
  • A product named without a CPE is matched when its version is known (for example nginx 1.18.0 from HTTP fingerprinting); a product with no usable version is not.
  • Less noise: listeners whose probe got no answer and that have nothing linked are folded behind "show N inconclusive" on each target.
  • A version match is still a candidate, never a finding: confirm the exact build and its backports, or run the check as part of an authorized Scan.

Model Intake is an optional module

Model Intake (scanning AI models before deployment) ships as two images of about 4.8 GB per release, more than half of what an install downloads. A new install now leaves it off; an existing install keeps it exactly as it was (an install that predates the setting keeps Model Intake on, and nothing is removed by the upgrade).

  • shakerscan model-intake enable pulls its two images (from the same verified release lock) and starts its services; shakerscan model-intake disable stops and removes them (--remove-images also deletes its images); shakerscan model-intake status shows the setting. The setting is SHAKERSCAN_MODEL_INTAKE_ENABLED in ~/.shakerscan/.env.
  • With the module off, the Model Intake page explains what it does and shows the command to install it, and makes no Model Intake request. Its sidebar item is under Show all sidebar sections.
  • The API refuses changes to Model Intake (409, reason_code: module_not_installed, naming the enable command); existing Model Intake records stay readable. /health reports model_intake_worker: {"enabled": false, "status": "disabled"} instead of a missing worker.
  • Compose: the four Model Intake services are in the model-intake profile; nothing else depends on them.

Hunt can use secrets found earlier on a target

A value a Hunt withholds from its output (a password in a configuration file, an API key in a response) used to be usable only in that Hunt, for at most 24 hours. It is now also kept for the target, so a later Hunt can use it too, still without ever seeing it.

  • Kept per target, encrypted under the same key as credential profiles, once per value (repeat sightings update when it was last seen), with a 30-day expiry that a new sighting extends. Scans keep the secrets they prove as well: a verified exposure (an .env or other configuration file, a credential store, a SQL dump) keeps the values it found.
  • Used by reference. secrets.inspect lists a target's usable secrets for the Hunt (reference, kind, field name, length, when seen; never the value), and an http.request binding takes {"secret_ref": "secret://<id>"} beside withheld_ref. The worker sends the value; every later response of that Hunt that echoes it has it withheld. A reference resolves only for a live Hunt of the secret's own target while it is active and unexpired; another target, a revoked or expired secret, or a malformed reference is refused with a reason code and no value. Every use is recorded with the action and the destination host.
  • Operator routes: GET /targets/{id}/discovered-secrets (metadata only), POST /targets/{id}/discovered-secrets/{secret}/state (revoke or reactivate), POST /targets/{id}/discovered-secrets/{secret}/expiry (1–365 days) and DELETE /targets/{id}/discovered-secrets (purge). No route returns a value, and a Hunt planner cannot use them. An operator's revocation or expiry stands: later sightings do not undo it.
  • Granting to another target: an operator can let Hunts of another target use a secret (POST .../{secret}/grants {"target_id": ...}, DELETE .../grants/{target}). Only the target it was found on grants it, at most 50 targets per secret; a grant never widens a Hunt's scope (the value is sent only where that Hunt may already go).
  • Promoting and revealing: POST .../{secret}/promote moves the value into a new credential profile of the target (validated like any typed profile; the discovered secret becomes superseded and points at it). POST .../{secret}/reveal shows the value to a person (cache-control: no-store). Both are recorded with who did it, apart from Hunt uses. Behind the Enterprise gateway, revealing, granting and promoting each need the person's step-up for that one decision.
  • In the Hunt briefing: the live sections say how many usable secrets the target holds, how many were never tried, by kind, and how to list and use them.
  • Terminal: shakerscan secrets list <target> [--usable], revoke, reactivate, expiry --days, ungrant --to, and grant --to, promote --name --auth-kind, reveal, the last three decided in your own terminal (a keypress on an open-source engine, your step-up on Enterprise).
  • Target page: a Discovered secrets card lists them (never a value) with the same actions; a revealed value is shown in its dialog only. Behind the Enterprise gateway the card names the terminal command for a step-up decision.
  • Lifecycle: an hourly sweep marks expired secrets and deletes their ciphertext 30 days after they stopped being usable; a target keeps at most 2,000. Deleting a Hunt record deletes the secrets it found (use records stay); deleting a target deletes its secrets; merging duplicate targets moves them to the surviving target.

Secrets no longer stay in finding evidence

The JavaScript-secrets and API data-exposure checks of the standalone scanner (scanner.py, its CLI and --server mode) put the matched text into a finding's evidence: 100 characters around a JavaScript secret (the whole value), or the raw response snippet. The JavaScript context now shows the value only as its preview (AIza...4567) with any other token-like run near it masked, and the data-exposure snippet keeps no response text (the finding family, the match's length and offset).

As a second line, before the worker writes a Scan's result anywhere (the result artifact, a scan's stored result, the findings table, a fleet node's posted result), every self-evident provider secret still in a finding's evidence (AWS, Stripe, Slack, GitHub, npm and SendGrid keys, credentialed database URLs, password hashes) is kept for the target as above and replaced by its secret:// reference, or by [REDACTED:<kind>] when it cannot be kept.

Upgrades look after themselves

An upgrade on a host short of disk space could leave Redis unable to start ("dependency failed to start: redis is unhealthy") and its operator choosing Docker volumes to delete. The launcher now handles it, and never asks you to manage Docker:

  • Disk space first. Before it downloads or stops anything, shakerscan start checks that the images this release still needs (plus room for the databases) fit in Docker's storage. If not, it removes ShakerScan release images that no container uses (running or stopped), never a volume, never another product's image, never this install's current or previous release (kept for rollback), and, if that is still not enough, stops with the numbers and changes nothing. A start with nothing to download never needs this room. Docker Desktop, whose disk the host cannot see, skips the check.
  • The database is saved before an upgrade. A start that changes the release dumps PostgreSQL to ~/.shakerscan/backups/pre-upgrade-<time>-<from>-to-<to>/ first (the newest two are kept; SHAKERSCAN_PRE_UPGRADE_BACKUP=0 turns this off).
  • Redis is repaired, not deleted. Redis must be healthy before anything that depends on it starts. A damaged append-only file (an interrupted write, typically a full disk) is copied to backups/redis-<time>/ and repaired; a Redis that cannot write gets room from old images. Only if repair fails does it start empty, which loses queues and job status only: scans, findings and evidence are in PostgreSQL. The API and workers are then restarted onto it. A Redis that is still loading is waited for; one that fails for a reason the launcher does not recognise is left as it is, with its last messages.
  • Old images go after an upgrade. After a successful start, this install's own ShakerScan images of releases older than the previous one are removed. Images it never recorded stay, and so do third-party images (PostgreSQL, Redis) and an optional module's images while it is off (SHAKERSCAN_PRUNE_OLD_IMAGES=0 keeps them all). A second, stopped install on the same host that still uses one of those old releases pulls its images again when it starts.
  • Warnings ahead of time. shakerscan status and doctor warn when Docker has less than 20 GB free, or when the database volume is missing on an install that has run before. Nothing in ShakerScan suggests docker system prune --volumes, which deletes the database.
  • Scans lost from the queue run. A Scan that was waiting when Redis lost its queue used to stay "pending" forever. One that provably never started (no start, no worker, no step begun) is now queued again from its saved job, with the routing it was first queued with, within about ten minutes, at most three times. A Scan that may have run keeps the "outcome unknown" flag, so nothing runs twice.

Rolling back to 2.9.x

Nothing this release writes is read differently by 2.9.x: the owner and cohort were already stored in the target's metadata, and 2.9.x ignores the discovered_secrets, discovered_secret_uses and discovered_secret_grants tables (they stay, unused, until 2.10 runs again). A finding's evidence that holds a [secret://...] reference shows it as text. A 2.9.x launcher ignores SHAKERSCAN_MODEL_INTAKE_ENABLED and starts Model Intake again (pulling its images if they were removed).

This page is rendered from docs/releases/2.10.0.md in the open-source repository at v2.10.0. When documentation and implementation disagree, the code, database schema, and tests at that tag are authoritative.