add recon modules for self-hosted training and experiment-tracking
platforms reachable without auth: mlflow, tensorboard, aim, and
determined disclose experiments, the artifact store, training run paths,
and cluster topology over unauthenticated apis.
add recon modules for self-hosted image generation servers reachable
without auth: comfyui, automatic1111, fooocus-api, and iopaint each
expose unauthenticated generation or editing and disclose the installed
models.
a detector accuracy audit surfaced two classes of bug in the framework
detectors.
bare-brand header false positives: header-only signatures matched a
brand name as a substring across every header name and value, so a
detector fired on any response that merely referenced the brand (a
vendor cdn named in a link or csp value, a cookie sharing the prefix).
add an optional Header field to Signature that scopes a header-only
match to one named header's value, and apply it (or a structural
anchor) per detector:
- express: "Express" scoped to x-powered-by, was firing on an
express_checkout cookie.
- flask: "Werkzeug" scoped to the server header.
- symfony: dropped the bare "symfony" word (symfony sets no such
header, it fired on symfony.com links); the x-debug-token header is
the marker.
- shopify: key on the x-shopify response headers instead of the bare
"Shopify" word, which fired on a cdn.shopify.com link.
- remix: dropped the bare "remix"/"_remix" substrings that fired on a
track_remix.mp3 asset; window.__remixContext is the definitive
marker.
- spring boot: anchor the whitelabel title in its h1 tag context so a
tutorial discussing the error does not fire.
the gin and fastapi detectors are removed: gin keyed on the
"gin-gonic" import-path string (appears in tutorials, never in a real
gin response) and fastapi on bare words matching the projects' doc
domains. neither framework advertises itself in a response header or a
non-prose body marker, so there is no clean passive signal to anchor
on.
version mis-extraction: drop the low-confidence ".*?" version
fallbacks (rails, django, laravel, spring), whose unbounded gap
grabbed the first version-shaped number after the framework word and
reported an unrelated asset's cache-buster when no real version was
present. let isValidVersionString accept a single integer so a bare
major such as drupal's "Drupal 10" is no longer rejected as "unknown".
each false positive and version bug is covered by a regression test.
Ran `gofmt -w .` accross the repo to fix formatting drift.
Mechanical `gofmt -w .` only. No functional or behavioural changes.
CONTRIBUTING.md requires gofmt-clean code; these files had slipped.
rename the go module path from github.com/dropalldatabases/sif to
github.com/vmfunc/sif across go.mod, all imports, the golangci exclude
list, release install docs and docs. pure string rename, no logic change.
#180 added the calibrate bool to SQL but the integration-tagged test
(only built under -tags=integration, outside normal CI) still called the
4-arg form. pass false (no calibration) to restore behavior.
a wordlist file fuzzes the {{word}} path placeholder, one request per
non-empty line, reusing the dirlist scanner's line reader. composes with
the existing attack modes and matchers-condition; updates attack-mode
tests for the new signature.
#189 added the condition param to checkMatchers; the favicon matcher test
(#183) still called the 3-arg form, breaking the build once both landed.
pass "" (default and-condition) to restore single-matcher semantics.
replace bare gin/api/cake/svelte/ember/backbone/meteor substrings with
specific markers (Backbone.Model, ember-application, __meteor_runtime_config__,
CAKEPHP cookie, svelte/internal) so prose and CORS headers stop matching.
adds paired false-positive/positive coverage per framework.
calibrate against reflecting catch-alls whose body size tracks path
length so exact-shape calibration no longer misses them; -ac now drives
both dirlist and sql. updates the admin-panel query test to the new SQL
signature. adds soft-404 + calibration coverage.
CMS only flagged Drupal on X-Drupal-Cache: HIT or the Drupal 7 Drupal.settings
marker, so Drupal 8-11 went undetected: a MISS cache header was ignored, and
cdn-fronted sites serve none of those markers in the body at all. verified live
that london.gov.uk and georgia.gov (both Drupal) were missed.
key on the Drupal-specific headers instead: any X-Drupal-Cache or
X-Drupal-Dynamic-Cache, plus X-Generator naming Drupal. these survive cdn
caching. drupalSettings (8+) and Drupal.settings (7) cover uncached bodies.
modules/recon/netdata-api-exposure.yaml flags an exposed Netdata agent through its
unauthenticated /api/v1/info endpoint, keyed on the mirrored_hosts and cores_total
fields a generic info response does not carry, then extracts the agent version.
modules/recon/cadvisor-api-exposure.yaml flags an exposed cAdvisor container monitor
through its /api/v1.3/machine endpoint, keyed on the machine_id and cpu_frequency_khz
fields, then extracts the machine id.
internal/modules/metrics_exposure_test.go drives both modules through
ExecuteHTTPModule and asserts the leak alongside the near misses a strict review
wants pinned: each service with one keying field missing, a generic json, a plain 200
and a 404.
verify: go test ./internal/modules, each matcher and extractor proven to bite
(break -> red, restore -> green).
modules/recon/docker-api-exposure.yaml flags an unauthenticated Docker Engine
api, keyed on the api version paired with the minimum api version that a generic
version endpoint does not carry, then extracts the engine version.
modules/recon/kubernetes-api-exposure.yaml flags an internet reachable Kubernetes
api server through its anonymous version endpoint, keyed on the git version
paired with a build field, then extracts the version.
modules/recon/kubelet-api-exposure.yaml flags an exposed kubelet whose pod list
leaks the cluster workload, keyed on the PodList kind paired with an api version,
then extracts a pod namespace.
internal/modules/runtime_api_exposure_test.go drives the three modules end to end
through ExecuteHTTPModule and asserts the leak alongside the near misses a strict
review wants pinned: a generic version response, each service with one keying
field missing, a service list that is not a pod list, a plain 200 and a 404.
verify: go test ./internal/modules, each matcher and extractor proven to bite
(break -> red, restore -> green).
modules/recon/maven-settings-exposure.yaml flags an exposed settings.xml through
the settings or servers structure paired with a password element, so a mirror
only config is not reported, then extracts the server username.
modules/recon/gradle-properties-exposure.yaml flags an exposed gradle.properties
through a password, secret or token property with a value on the same line,
skipping comments and empty assignments, then extracts the property name.
modules/recon/nuget-config-exposure.yaml flags an exposed nuget.config through a
packageSourceCredentials section paired with a cleartext password key, so a
plain package source list or an appsettings password is not reported, then
extracts the feed username.
internal/modules/buildtool_credential_exposure_test.go drives the three modules
end to end through ExecuteHTTPModule and asserts the leak alongside the near
misses a strict review wants pinned: a mirror only settings, a non credential
properties file, a commented password, an empty value, a plain source list, an
appsettings password, an html tutorial for each file, a plain 200 and a 404.
verify: go test ./internal/modules, each matcher and extractor proven to bite
(break -> red, restore -> green).
modules/recon/terraform-state-exposure.yaml flags an exposed terraform state
file on the terraform_version key paired with a state structure key, then
extracts the version. the structure key keeps a document that merely mentions
terraform_version from matching.
modules/recon/kubeconfig-exposure.yaml flags an exposed kubeconfig on the
kind: Config marker paired with a cluster or credential key, then extracts the
cluster api endpoint. it catches an exec auth kubeconfig with no embedded key
since the cluster block alone is a leak.
modules/recon/docker-compose-exposure.yaml flags an exposed compose file on the
services key paired with a service definition key, then extracts the first
image reference to surface the stack and its versions.
each module pairs a unique marker with a structure key and rejects an html
body, so a page that only names the marker is not a leak.
internal/modules/infra_config_exposure_test.go drives the three modules end to
end through ExecuteHTTPModule and asserts the leak alongside the near misses a
strict review wants pinned: a bare terraform_version mention, a bare
kind: Config mention, a bare services key, an html page carrying the markers, a
plain 200 body and a 404, none of which may match.
verify: go test ./internal/modules, each marker, structure gate, guard and
extractor proven to bite (break -> red, restore -> green).
modules/recon/laravel-ignition-exposure.yaml probes the live
/_ignition/health-check endpoint and extracts can_execute_commands, the flag
that marks the CVE-2021-3129 remote code execution surface. this is an active
probe, complementary to the version based ignition entry in the framework cve
map.
modules/recon/symfony-profiler-exposure.yaml flags an exposed web profiler on
its structural markers and extracts a request token to pivot to a captured
request.
modules/recon/spring-heapdump-exposure.yaml flags an exposed actuator heap
dump on the hprof magic anchored at the start of the body, which a json marker
module cannot see because the dump is binary, and extracts the hprof version.
the anchor keeps a page that merely quotes the magic from matching.
internal/modules/debug_exposure_test.go drives the three modules end to end
through ExecuteHTTPModule and asserts the leak alongside the near misses a
strict review wants pinned: a prose mention of ignition, the hprof magic away
from the start, a plain 200 body and a 404, none of which may match, plus an
exposed ignition with command execution disabled that still flags and reports
the false flag.
verify: go test ./internal/modules, each matcher, anchor and extractor proven
to bite (break -> red, restore -> green).
load yaml-defined detectors from ~/.config/sif/signatures (AppData\Local
on windows), mirroring the user-modules convention, so a framework sif
does not ship can be detected without a rebuild. they load lazily once
per run from DetectFramework and register alongside the built-ins.
each file is one detector, scored by the same weighted signature match as
the built-ins. confidence is linear rather than their sigmoid (importing
it would cycle), so a detector clears the 0.5 threshold once its matched
weights pass half. a name matching a built-in overrides it and inherits
that built-in's version patterns and cves, the same as a user module. a
single unparseable file warns and is skipped rather than failing the scan.
implements the custom signature support help-wanted item in contributing.
* refactor(config): extract registerFlags from Parse
split flag registration into a registerFlags helper so callers, including
tests, can build and inspect the flag set without parsing os.Args. pure
move, no behavior change.
* feat(config): add scan templates via -template
wire up the existing -template flag to load a batch of scan settings from
a built-in preset or a local yaml file, so a run does not have to pass
every flag by hand.
a value is a named preset (minimal, recon, full) embedded in the binary,
or a path to a local yaml file keyed by flag long-names. it merges as the
goflags config before parsing, so command-line flags still win and it
replaces the ambient config for that run.
implements #5.
modules/recon/joomla-config-exposure.yaml flags an exposed configuration.php
backup through the JConfig class paired with the password property, so a generic
php class is not reported, then extracts the database password.
modules/recon/drupal-config-exposure.yaml flags an exposed settings.php backup
through the databases array paired with a literal password value, so an array
that lacks the marker or resolves the password from the environment is not
reported, then extracts the password.
modules/recon/magento-config-exposure.yaml flags an exposed app/etc/env.php
backup through the crypt or mode marker paired with a literal key or password
value, so a generic return array or a cloud placeholder is not reported, then
extracts the crypt key.
internal/modules/cms_config_exposure_test.go drives the three modules end to end
through ExecuteHTTPModule and asserts the leak alongside the near misses a strict
review wants pinned: a config missing its password, a generic php class, an array
without the databases marker, a databases array with no password, an env
indirection password, a return array without a magento marker, a magento config
with no credential, a cloud placeholder key, an html tutorial for each file, a
plain 200 and a 404.
verify: go test ./internal/modules, each matcher and extractor proven to bite
(break -> red, restore -> green).
detectWordPress treated any 2xx on /wp-login.php, /wp-admin/ or /wp-config.php
as wordpress, but the http client follows redirects and many sites soft-404
(200 for every path) or catch-all redirect to their homepage. a probed path
then returns 200 without being wordpress at all, so plain static sites and
marketing pages were flagged as wordpress.
read the probed response and require one of the existing wordpress markers in
its body before counting it. a real /wp-login.php still references wp-includes
assets, so genuine detection holds.
classifyXSSContext defaulted any non-html, non-script reflection to attribute context. a reflection echoed into element text with the angle brackets escaped but quotes left raw (common for encoders limited to < > &) was then reported as a high-severity attribute injection, even though a quote in text content has no delimiter to break out of.
classify a reflection as attribute only when the canary actually lands inside an open tag; everything else is inert element text and yields no finding. the in-tag test is a byte-scan, not a parser, so rare malformed markup can still mis-bucket.
the ghost and pantheon entries keyed on each platform's older unclaimed-domain
copy, which the live pages no longer serve, so a real dangling subdomain went
undetected. verified against live unclaimed subdomains: <random>.ghost.io now
returns "Failed to resolve DNS path for this host" and <random>.pantheonsite.io
returns a "404 - Unknown site" page. key on those instead.
ghost also serves a bare "Site unavailable." on some hosts; the body match holds
one substring per service, so it keys on the distinctive arm and a page showing
only "Site unavailable." is not detected.
modules/recon/svn-exposure.yaml flags an exposed .svn working copy through the
wc.db sqlite header anchored at the first byte paired with a working copy table
name, so a generic sqlite database is not reported, then extracts the
repository url.
modules/recon/mercurial-exposure.yaml flags an exposed .hg repository through
the revlog format requirements that the requires file lists, so prose that
names mercurial is not reported, then extracts the requirement.
modules/recon/bazaar-exposure.yaml flags an exposed .bzr repository through the
Bazaar meta directory signature, so a page that names a bazaar is not reported,
then extracts the format.
internal/modules/vcs_metadata_exposure_test.go drives the three modules end to
end through ExecuteHTTPModule and asserts the leak alongside the near misses a
strict review wants pinned: a generic sqlite database, an unanchored magic,
prose naming mercurial, a marketplace page, an html tutorial for the text
formats, a plain 200 and a 404.
verify: go test ./internal/modules, each matcher and extractor proven to bite
(break -> red, restore -> green).
The cors probe followed up to three redirects, then read the cors headers off the final response. Its own comment said the cap existed so the headers came from the host we asked about, so the code contradicted its intent: a target that only redirects to a permissive third party had that party's misconfiguration reported against it.
Stop following redirects, matching the open-redirect scanner. The redirect cap was a const shared with the xss probe, so xss now owns its own xssMaxRedirects with its follow behavior unchanged.
modules/recon/netrc-exposure.yaml flags an exposed .netrc through the machine
login password grammar, requiring the keywords in order so prose that names
them out of order does not match, then extracts the machine host.
modules/recon/pgpass-exposure.yaml flags an exposed .pgpass through a single
line host:port:database:user:password record with a numeric or wildcard port,
which a yaml config or a multi line body does not satisfy, then extracts the
host.
modules/recon/mysql-client-config-exposure.yaml flags an exposed .my.cnf
through a client section paired with a cleartext password key, so a section
without a credential is not reported, then extracts the client user.
internal/modules/dotfile_credential_exposure_test.go drives the three modules
end to end through ExecuteHTTPModule and asserts the leak alongside the near
misses a strict review wants pinned: out of order prose, a yaml db config, a
non numeric port, a multi line body, a section without a password, a password
without a section, an html tutorial for each file, a plain 200 and a 404.
verify: go test ./internal/modules, each matcher and extractor proven to bite
(break -> red, restore -> green).
modules/recon/docker-registry-api-exposure.yaml flags a Docker registry reachable
anonymously through its /v2/ base, keyed on a 200 paired with the
Docker-Distribution-Api-Version: registry/2.0 response header (the header rides on a
401 too, so the 200 gate is what proves anonymous reach), then extracts the api
version.
modules/recon/harbor-api-exposure.yaml flags an exposed Harbor registry through its
unauthenticated /api/v2.0/systeminfo endpoint, keyed on the harbor_version and
auth_mode fields, then extracts the harbor version.
internal/modules/registry_exposure_test.go drives both modules through
ExecuteHTTPModule and asserts the leak alongside the near misses: docker registry on
a header-less 200 and on a 401 that still carries the header, harbor with one keying
field missing, a plain 200 and a 404.
verify: go test ./internal/modules, each matcher and extractor proven to bite
(break -> red, restore -> green).
the extractPotentialBuckets TODO asked to handle non-adjacent label
combos and strip the tld. strip the trailing tld label so we stop
guessing it ("com", "com-s3", "example-com"), and pair every label
with every other rather than only adjacent ones, so a deep host like
shop.cdn.example yields shop-example too.
this widens the candidate set (and the s3 probes) per host. multi-part
suffixes like .co.uk still leave a junk label; publicsuffix would
refine that as a follow-up.
scanLines used a default bufio.Scanner (64k token cap) and never
checked scanner.Err(), so a wordlist line past the cap silently halted
the scan and dropped that line plus everything after it. return the
error so the wordlist loaders fail loud instead of running against a
truncated list.
move FaviconHash and its base64 chunking into a leaf package so the scan check and the module engine share one implementation instead of each carrying a copy. no behavior change; the golden test moves with it.
detectWordPress flagged any page whose body merely contained the word
"wordpress", so sites that only mention it (wp-hosting marketing, comparison
pages) were misreported: acquia.com, a Drupal site, was detected as WordPress.
wp-content, wp-includes and wp-json already identify a real install and appear
on every wordpress page, so the bare word adds only false positives. drop it.
specToResult computed an operation's auth status with len(op.Security) == 0,
which a value slice cannot tell apart from an absent block: an operation that
overrides the global requirement with an empty array (security: []) or an
empty requirement object (security: [{}]) decoded the same as one that simply
inherits the global default, so those deliberately-public endpoints were
reported as authenticated. decode the operation security into a pointer to
keep absent distinct from explicit-empty, and treat both empty forms as
anonymous-reachable.
the page scanner and the next.js manifest parser both reassembled the
response body line by line with a default bufio.Scanner (64k token
cap), so a line past the cap (common with minified or inlined js)
silently halted the read and dropped every script and route reference
after it. read the whole body instead, capped at 5mb.
parsing the raw page bytes also fixes script tags split across a
newline, which the old line-joined reassembly merged and missed.
the asp.net detector carried {Pattern: "X-Powered-By: ASP.NET", HeaderOnly:
true}, but containsHeader matches a signature against each header name and each
value separately, never against a joined "name: value" string. so that pattern
could never match anything. it only added its 0.4 to the detector's total
weight, diluting every real signal: a genuine response (an X-AspNetMvc-Version
header, an X-Powered-By: ASP.NET header, an .aspx link) scored 1.1/3.8 = 0.47,
just under the 0.5 threshold, and went undetected.
the live "ASP.NET" header signature already matches the X-Powered-By value, so
removing the dead one loses no coverage and lifts that same response to
1.1/3.4 = 0.56. adds a regression test for it.