20 passive, legal checks that watch your domains for what attackers actually look for first: named CVEs with a public-exploit flag, exposed files/buckets, forgotten subdomains, weak SSL, and more. Only new exposures are reported after the baseline check.
0.1.9 — Deep review (09/09/2026): fewer false alarms, no phantom bills
Every dataset item on this actor is a billed $0.25 exposure, so a false
finding is not just noise — it is an invoice. This pass targeted exactly that.
Fixed — billing
A quiet run no longer produces a billed row. 0.1.8 pushed a status
row to the dataset when nothing was new; on this actor that row cost $0.25
per quiet run. The outcome of a quiet run is now written to the run's
key-value store (LAST_RUN_STATUS) — visible, free.
A domain that could not be checked at all is no longer charged the
domain-scanned event.
Fixed — false positives (each one was a billed CRITICAL/HIGH)
CVE matching now uses NVD's CPE match, not keyword search. Keyword
search returns any CVE whose text contains the words: measured live,
"WordPress 6.4.2" returned 11 CVEs of which 5 were plugin flaws fixed in
a 6.4.2 of that plugin — reported as CRITICAL against a site that is not
affected. Known products (WordPress, Drupal, Joomla, TYPO3, nginx, Apache,
jQuery, Bootstrap, lodash, …) and WordPress plugins are now matched by CPE,
which is version-range aware: 4 real CVEs for WordPress 6.4.2, and 6 for
nginx 1.18.0 where keyword search found none. Unknown products fall back
to keyword search filtered to CVEs that are about that product and not
already fixed in the detected version. Set NVD_API_KEY as an environment
variable to lift NVD's public rate limit.
Sensitive-file probes need the file to look like the file. A site that
stamps a CSRF nonce or timestamp into every page defeated the soft-404
fingerprint and made all 11 probed paths read as exposed (measured 11/11).
/.env must contain KEY=value lines, /.git/config a [core] section,
/backup.sql SQL, /backup.zip a zip header, and so on; an HTML page is
never a config file.
Plain HTTP answered with 4xx/5xx is not "plain HTTP served". A 403 on
port 80 means HTTP is refused, which is fine; only a 2xx is flagged.
A public S3 bucket named after the domain is reported as HIGH, not
CRITICAL, with a note to verify ownership — bucket names are global and
it may be someone else's.
Shodan ports/vulns are skipped for CDN-fronted sites. Behind
Cloudflare/Fastly/Akamai/CloudFront the public IP is the CDN's, so its open
ports and vulns are the CDN's too; they were being attributed to the client.
Fixed — false negatives
Expired, self-signed and wrong-host certificates are now named as
tls_cert_expired (CRITICAL), tls_cert_self_signed, tls_cert_hostname_mismatch
instead of one vague tls_handshake_failed; the expiry branch was
unreachable for the certificates it was written for. Nothing on 443 is
https_unreachable (MEDIUM).
Cookies set on a redirect hop are checked. The session cookie is very
often set on the / → /home redirect; only the final response was read.
An unavailable intel source (NVD rate limit, Certspotter, Shodan
InternetDB, ransomware.live) now marks the scan incomplete instead of
reading as "nothing found" — which, combined with 0.1.8's pruning, would
have dropped every finding from that source and re-alerted them all on
the next successful run.
Three defects found by reviewing this actor against the bug classes that had
just surfaced in Malicious Package Watch. All three affect watch-style
scheduled use — which is how the actor is meant to run.
Fixed
A quiet run was counted as a failed run. When no exposure was new since
the last check, the actor pushed nothing; Apify counts a run with zero
dataset items as failed, which is why the Store showed a 0 % success rate
on a client's perfectly healthy domains. A quiet run now pushes one
category: "status" row (finding: "status:no-new-exposures").
An incomplete scan overwrote the domain's state. If the homepage was
briefly unreachable or any check raised, the (partial or empty) result was
stored as the new baseline, so every finding of a skipped check came back as
new on the following run. The check runner now reports whether the scan
was complete; an incomplete scan merges into the stored state instead of
replacing it, and a first scan that is incomplete defers its baseline.
Exposures held back by max_results were lost. State was written before
the cap was applied, so anything cut was marked as already reported and never
came back. State is now written after push_data, only for the rows it
actually accepted; held-back exposures return on the next run, and the log
says how many were held back.
Changed
Findings are sorted severity-first across all domains before the cap is
applied (previously only within each domain).