scanner

package
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Overview

Package scanner implements the read-only HTTP scan engine: WordPress detection, plugin/theme enumeration via readme.txt/style.css/wp-json, and version comparison against an indexed local database.

Index

Constants

This section is empty.

Variables

View Source
var ErrBlocked = errors.New("blocked by WAF or error page")

ErrBlocked is returned when the homepage HTML matches --exclude-content-based (WAF or error page).

View Source
var ErrNotWordPress = errors.New("target does not appear to be a WordPress site")

ErrNotWordPress is returned when a scan target shows no WordPress signs.

View Source
var ErrOutOfScope = errors.New("target out of scope")

ErrOutOfScope is returned when --scope does not match the target URL.

View Source
var ErrRedirectBlocked = errors.New("redirect blocked")

ErrRedirectBlocked is wrapped by the redirect guard when a Location would cross the scanned target's host:port authority or exceed the hop limit. It is not a transport failure, so it is never retried by sendRequest.

Functions

func ExtractCoreVersionFromAssets added in v0.6.0

func ExtractCoreVersionFromAssets(html string) (version string, found bool)

ExtractCoreVersionFromAssets parses the WordPress core version from the ?ver= query string on core-released asset references (see coreAssetVerRe). found is false when no such asset reference carries a version.

func ExtractCoreVersionFromReadmeHTML added in v0.9.0

func ExtractCoreVersionFromReadmeHTML(body string) (string, bool)

ExtractCoreVersionFromReadmeHTML parses the WordPress core version out of a served readme.html document: the canonical <h1 id="version"> heading first, then the looser ">Version X.Y.Z<" element form used by older releases. The candidate is sanitized via sanitizeVersion (control characters stripped, capped at maxVersionLen runes). found is false when neither marker matches or the sanitized result is empty.

func ExtractOPMLVersion added in v0.3.0

func ExtractOPMLVersion(body string) (version string, found bool)

ExtractOPMLVersion parses the WordPress core version from the generator attribute of a wp-links-opml.php document (generator="WordPress/X.Y"). found is false when absent.

func ExtractPassiveSlugs added in v0.2.0

func ExtractPassiveSlugs(html string) (plugins, themes []string)

ExtractPassiveSlugs parses plugin and theme slugs from HTML the way WPScan does passive detection: any reference to wp-content/plugins/<slug>/ or wp-content/themes/<slug>/ counts as evidence the component is installed. Returns deduplicated, sorted plugin slugs and theme slugs.

func ExtractPassiveSlugsIn added in v0.2.0

func ExtractPassiveSlugsIn(html, contentDir string) (plugins, themes []string)

ExtractPassiveSlugsIn is ExtractPassiveSlugs with a custom wp-content directory name: references to <contentDir>/plugins/<slug>/ or <contentDir>/themes/<slug>/ count as evidence the component is installed. Returns deduplicated, sorted plugin slugs and theme slugs.

func ExtractPassiveVersions added in v0.3.0

func ExtractPassiveVersions(html string) map[string]string

ExtractPassiveVersions parses plugin and theme versions from asset URLs in HTML: any wp-content/plugins/<slug>/...?ver=1.2.3 or wp-content/themes/<slug>/...?ver=1.2.3 reference counts as evidence of the installed version. The result maps slug to version; when a slug is referenced with several versions the first one in document order wins. The map is capped at maxPassiveVersions entries.

func ExtractPassiveVersionsIn added in v0.3.0

func ExtractPassiveVersionsIn(html, contentDir string) map[string]string

ExtractPassiveVersionsIn is ExtractPassiveVersions with a custom wp-content directory name, mirroring ExtractPassiveSlugsIn.

func ExtractRESTRoutePlugins added in v0.8.0

func ExtractRESTRoutePlugins(body []byte) []string

ExtractRESTRoutePlugins parses the WordPress REST API root index (/wp-json/) for plugin namespaces: when the body parses as {"routes": {...}} the route keys are the candidate list, and a plain array of route strings is accepted too. Each route key is reduced to its first namespace segment (see restRouteSlug); segments matching the exact known core prefixes (wp, oembed, wp-site-health, wp/block-directory) are dropped and anything not matching ^[a-z0-9_-]+$ or longer than maxRESTRoutePlugins runes is rejected. The surviving slugs are deduplicated, sorted and capped at maxRESTRoutePlugins entries. A body that parses as neither shape (or carries no routes at all) returns nil.

func ExtractRSSVersion added in v0.3.0

func ExtractRSSVersion(body string) (version string, found bool)

ExtractRSSVersion parses the WordPress core version from an RSS feed generator element in its URL form (<generator>https://wordpress.org/?v=X.Y.Z</generator>). found is false when absent.

func ExtractRequiresAtLeast added in v0.5.0

func ExtractRequiresAtLeast(body string) string

ExtractRequiresAtLeast parses the "Requires at least:" header line of a WordPress readme.txt or theme style.css and returns the minimum WordPress version required, sanitized. It returns "" when the header is absent. The match is case-insensitive, tolerates trailing spaces (and the "Requires at least = Y" separator spelling), and never spans lines.

func ExtractTestedUpTo added in v0.5.0

func ExtractTestedUpTo(body string) string

ExtractTestedUpTo parses the "Tested up to:" header line of a WordPress readme.txt (or theme style.css header) and returns the WordPress version the component was tested against, sanitized. It returns "" when the header is absent. The match is case-insensitive, tolerates trailing spaces (and the historical "Tested up to = X" separator spelling), and never spans lines.

func ExtractTimthumbVersion added in v0.8.0

func ExtractTimthumbVersion(body string) (string, bool)

ExtractTimthumbVersion parses the version out of a TimThumb source or info body using the common release markers (see timthumbVersionRe). The first marker present wins and the result is sanitized via sanitizeVersion (control characters stripped, capped at maxVersionLen runes). found is false when the body carries no recognizable marker.

func ExtractVersionFromChangelog added in v0.4.0

func ExtractVersionFromChangelog(body string) (string, bool)

ExtractVersionFromChangelog parses the first version heading inside a readme.txt Changelog section ("== Changelog ==" or "## Changelog"): the classic "= X.Y.Z =" heading, the markdown "### X.Y.Z" heading, or a "X.Y.Z - ..." first line. The candidate is sanitized and must parse as a numeric version (internal/version.Parse). found is false when there is no Changelog section or no parseable heading — readmes whose only version is buried in the changelog (no "Stable tag:" line) still get detected.

func ExtractVersionFromReadme

func ExtractVersionFromReadme(body string) (version string, found bool)

ExtractVersionFromReadme parses the "Stable tag:" line of a WordPress plugin readme.txt and returns the version string. found is false when no parseable stable tag exists.

func ExtractVersionFromStyleCSS

func ExtractVersionFromStyleCSS(body string) (version string, found bool)

ExtractVersionFromStyleCSS parses the "Version:" header of a WordPress theme style.css and returns the version string. found is false when no parseable version exists.

func ExtractVersionFromVersionPHP added in v1.1.0

func ExtractVersionFromVersionPHP(body string) (string, bool)

ExtractVersionFromVersionPHP parses the WordPress core version from a wp-includes/version.php source body (the $wp_version assignment). found is false when the assignment is absent.

func ExtractWordPressVersion

func ExtractWordPressVersion(html string) (version string, found bool)

ExtractWordPressVersion parses the WordPress version from the generator meta tag in the homepage HTML. found is false when absent.

Types

type CoreEvidence added in v0.3.0

type CoreEvidence struct {
	Source     string `json:"source"`
	Version    string `json:"version"`
	Confidence int    `json:"confidence,omitempty"`
}

CoreEvidence records one WordPress core version observation together with the source that produced it: "meta" (generator meta tag), "rss" (feed generator element), "opml" (wp-links-opml.php generator attribute), "asset-ver" (core-released asset ?ver= cache-buster), "readme-html" (readme.html "Version X.Y.Z" heading) or "fingerprint" (core asset md5 table). Confidence carries the same 0..100 reliability score used by Detected.

type Detected

type Detected struct {
	Slug       string `json:"slug"`
	Name       string `json:"name"`
	Type       string `json:"type"`
	Version    string `json:"installed_version"`
	Source     string `json:"source,omitempty"`
	Confidence int    `json:"confidence,omitempty"`
	// TestedUpTo and RequiresAtLeast are readme.txt / style.css header
	// metadata ("Tested up to:" / "Requires at least:" lines) captured for
	// reporting; both are omitted when the artifact carries none.
	TestedUpTo      string `json:"tested_up_to,omitempty"`
	RequiresAtLeast string `json:"requires_at_least,omitempty"`
	// ActiveInstalls is the active-install estimate for this slug from the
	// --popular-file counts maps (counts_plugins / counts_themes, by Type),
	// capped at maxActiveInstalls. Omitted (0) when the file carried no
	// count for the slug; the built-in popular lists never carry counts.
	ActiveInstalls int `json:"active_installs,omitempty"`
}

Detected is a plugin/theme/core component whose presence and version were identified on the target. Source records how it was found: "passive" (slug referenced in page HTML), "passive-ver" (asset ?ver= query string), "readme" (plugin readme.txt "Stable tag:" probe), "readme-changelog" (plugin readme.txt Changelog section heading), "composer" (plugin composer.json "version" field), "style.css" (theme stylesheet probe), "rest" (unauthenticated wp-json listing), "rest-routes" (plugin namespace from the wp-json route index), "auth-rest" (authenticated wp-json inventory) or "fingerprint" (core asset md5 table). Confidence is the 0..100 reliability estimate assigned to the detection source (see sourceConfidence); it is omitted from JSON output when zero.

type Finding

type Finding struct {
	Slug             string          `json:"slug"`
	Name             string          `json:"name"`
	Type             string          `json:"type"`
	InstalledVersion string          `json:"installed_version"`
	Vulnerabilities  []Vulnerability `json:"vulnerabilities"`
}

Finding links an installed component to its matching vulnerabilities.

type LoginBrute added in v0.3.0

type LoginBrute struct {
	User     string `json:"user"`
	Password string `json:"password"`
	URL      string `json:"url"`
}

LoginBrute is a credential pair that successfully authenticated against the target, either through wp-login.php or the XML-RPC endpoint.

type Options

type Options struct {
	Threads             int               // concurrent HTTP requests (default 5)
	Timeout             time.Duration     // per-request timeout (default 10s, alias for RequestTimeout)
	Stealth             bool              // throttle to 1 request/second
	RateLimit           float64           // max requests per second (0 = unlimited)
	APIOnly             bool              // skip brute-force enumeration, only wp-json/plugins
	MaxRequests         int               // cap on brute-force enumeration requests (default 500)
	Enumerate           string            // what to enumerate: u/p/t, combinable (default "pt")
	UserAgent           string            // custom User-Agent for all requests
	RandomUA            bool              // pick a random browser User-Agent per request
	BasicAuthUser       string            // HTTP Basic auth username sent on every request (--basic-auth USER:PASS)
	BasicAuthPass       string            // HTTP Basic auth password sent on every request
	Cookie              string            // static Cookie header sent on every request (--cookie)
	Headers             map[string]string // extra request headers sent on every request (--headers k=v,..)
	VHost               string            // Host header override for every request (--vhost)
	Force               bool              // --force: scan on even without WordPress fingerprints
	ExcludeVulns        []string          // --exclude-vulns: vulnerability IDs to skip entirely (case-sensitive)
	DetectionMode       string            // passive (homepage only), aggressive (DB only), mixed (default)
	Proxy               string            // http://, https://, socks5:// or socks5h:// proxy URL
	ProxyAuth           string            // --proxy-auth USER:PASS for SOCKS5 proxies (RFC 1929)
	ProxyTargetOnly     bool              // --proxy-target-only: use the proxy only for target-host traffic
	TLSFingerprint      string            // --tls-fingerprint: chrome | firefox | random (TLSClientConfig variations)
	PerHostRateLimit    float64           // --per-host-rate-limit N: per-host requests per second (0 = off)
	NoXMLRPC            bool              // skip the XML-RPC (xmlrpc.php) ping check
	Checks              string            // extra checks: cb (config backups), dbe (db exports), comma-separated
	ConnectTimeout      time.Duration     // TCP dial timeout (default 10s)
	RequestTimeout      time.Duration     // per-request timeout (default 10s)
	ContentDir          string            // wp-content directory (default "wp-content")
	PluginsDir          string            // plugins directory (default "wp-content/plugins")
	ExcludeContentBased string            // regex; matching homepage HTML aborts the scan
	Scope               string            // regex; a non-matching target URL is out of scope
	PluginsList         string            // file with plugin slugs (one per line, # comments)
	ThemesList          string            // file with theme slugs (one per line, # comments)
	MaxScanDuration     time.Duration     // hard stop for the whole scan; 0 = unlimited
	CacheTTL            time.Duration     // HTTP response cache TTL; 0 = off
	CrawlPages          int               // --crawl-pages N: passively crawl up to N sitemap pages (0 = disabled)
	Findings            chan Finding      // when set, every finding is emitted live

	PasswordsFile string // --passwords FILE: wordlist for the wp-login brute force (one per line)
	UsernamesFile string // --usernames FILE: wordlist for brute-force attacks (one per line)
	User          string // --user USER: single username for the XML-RPC multicall attack
	XMLRPCBrute   string // --xmlrpc-brute FILE: wordlist for the XML-RPC multicall attack
	MCPerRequest  int    // --multicall-max-passwords N: passwords per multicall request (default 3)
	WPAuth        string // --wp-auth USER:PASS: Basic auth for the REST inventory
	NoBrute       bool   // --no-brute: disable credential brute force (login + XML-RPC)
	NoSummary     bool   // --no-summary: skip gathering scan summary statistics

	// FingerprintDB is an optional path to a JSON core-fingerprint table
	// ("{"files": {path: {md5hex: [versions]}}}") used as a final core
	// version fallback when meta/RSS/OPML sources all fail. A missing or
	// unparseable table is silently skipped. Disabled when empty.
	// Context, when set, overrides the scan-wide context used by
	// requestCtx() (signal-based cancellation etc.). MaxScanDuration
	// takes precedence when both are set.
	Context context.Context

	FingerprintDB string
	// CoreVersionOverride pins the reported WordPress core version to the
	// --wp-version value, skipping the whole version-detection chain
	// (generator meta, RSS/OPML generators, core asset ?ver= and the
	// fingerprint table) while still fetching the homepage normally for
	// passive evidence. Empty when unused.
	CoreVersionOverride string
	// InsecureTLS disables TLS certificate verification (--disable-tls-
	// checks; scanners behind MITM proxies).
	InsecureTLS bool
	// MediaIDs caps attachment-ID probing for the "m" enumerate token;
	// 0 keeps the legacy homepage-presence check only.
	MediaIDs int
	// PopularSlugs appends the static popular plugin/theme slug seed lists
	// (popular.go) to aggressive enumeration after the DB top-slug list.
	// The CLI flag defaults it to true; building Options directly leaves
	// the zero value (false), which keeps the seed lists off.
	PopularSlugs bool
	// PopularThemes appends the static popular theme slug seed list
	// (popular.go) to aggressive enumeration after the DB top-slug list,
	// independently of PopularSlugs so the CLI can map the WPScan-style
	// enumerate tokens onto them ("t" enables popular themes, "vt"/"at"
	// keep only the vuln-heavy DB themes). The zero value (false) keeps
	// the theme seeds off, mirroring PopularSlugs.
	PopularThemes bool
	// AllowForeignRedirect allows following HTTP redirects whose target
	// host:port differs from the scanned target's authority. The default
	// (false) blocks foreign redirects as SSRF hardening, surfacing them as
	// fetch errors.
	AllowForeignRedirect bool
	// MaxRetries is how many times a transient transport error (a non-HTTP
	// failure from the transport layer) is retried with exponential backoff
	// plus jitter before giving up. The CLI flag defaults it to 2; 0
	// disables retries entirely.
	MaxRetries int
	// Discover404 probes ONE deliberately nonexistent path
	// (/<contentDir>-404-check-<random>/) after the homepage fetch and
	// treats any wp-content references in its 200 body as passive
	// plugin/theme evidence, WPScan's urls_in_404_page parity. The zero
	// value (off) preserves the historical request budget; the CLI defaults
	// it to true.
	Discover404 bool
	// PopularFile is an optional path to a JSON popular-list file
	// ({"plugins":["slug",...],"themes":["slug",...], optionally
	// "counts_plugins":{"slug":N} and "counts_themes":{"slug":N}}) that
	// replaces the built-in popular.go seed lists for aggressive
	// enumeration. The counts maps decorate detected components with
	// active-install estimates (capped at maxActiveInstalls). A missing
	// or unparseable file falls back to the built-ins with a one-time
	// warning, never an error. Empty when unused.
	PopularFile string
	// PluginsThreshold aborts enumeration with a warning when at least
	// this many plugins are found (0 = disabled, matching WPScan's
	// --plugins-threshold default 100; WPScan errors, onyx warns so the
	// scan still reports partial results). ThemesThreshold is the same
	// for themes (WPScan default 20).
	PluginsThreshold int
	ThemesThreshold  int
}

Options tunes the scan behaviour. Zero values fall back to defaults.

type Result

type Result struct {
	Target           string                `json:"target"`
	IsWordPress      bool                  `json:"is_wordpress"`
	WordPressVersion string                `json:"wordpress_version,omitempty"`
	CoreEvidence     []CoreEvidence        `json:"core_evidence,omitempty"` // which source produced WordPressVersion
	Evidence         []string              `json:"evidence,omitempty"`
	Detected         []Detected            `json:"detected,omitempty"`
	Findings         []Finding             `json:"findings,omitempty"`
	Nuclei           []nuclei.NucleiResult `json:"nuclei,omitempty"`
	PoCs             []pocs.PoCLink        `json:"pocs,omitempty"`
	Users            []User                `json:"users,omitempty"`
	XMLRPC           bool                  `json:"xmlrpc,omitempty"`          // xmlrpc.php ping answered
	XMLRPCPingback   bool                  `json:"xmlrpc_pingback,omitempty"` // pingback.ping exposed (SSRF amplification)
	// XMLRPCMethods lists the methods advertised by system.listMethods,
	// capped at 20 entries; nil when XML-RPC is disabled or the list
	// could not be parsed.
	XMLRPCMethods    []string     `json:"xmlrpc_methods,omitempty"`
	Interesting      []string     `json:"interesting,omitempty"`
	ConfigBackups    []string     `json:"config_backups,omitempty"`
	DBExports        []string     `json:"db_exports,omitempty"`
	RateLimitHits    int          `json:"rate_limit_hits,omitempty"`    // 429s seen
	TimedOut         bool         `json:"timed_out,omitempty"`          // --max-scan-duration expired
	RateLimitedAbort bool         `json:"rate_limited_abort,omitempty"` // enumeration stopped: target kept answering 429
	Errors           []string     `json:"errors,omitempty"`
	LoginBrutes      []LoginBrute `json:"login_brutes,omitempty"` // valid credentials found by brute force
	AuthStatus       string       `json:"auth_status,omitempty"`  // --wp-auth: authenticated | failed | ""
	Summary          *Summary     `json:"summary,omitempty"`      // scan statistics; nil with --no-summary
	// ScannedAt records when the scan finished (UTC); added in schema 1.0
	// so saved results carry their own timestamp.
	ScannedAt time.Time `json:"scanned_at"`
	// SchemaVersion is the version of the Result JSON shape, so CI
	// consumers can detect breaking output changes. "1.0" is the first
	// versioned shape (onyx 0.5.0+); always present.
	SchemaVersion string `json:"schema_version"`
}

Result is the output of a scan.

type Scanner

type Scanner struct {
	// contains filtered or unexported fields
}

Scanner drives one scan against a single target.

func NewScanner

func NewScanner(database *db.DB, base string, opts Options) (*Scanner, error)

NewScanner builds a Scanner for base, using the given database and options.

func (*Scanner) Progress added in v0.2.0

func (s *Scanner) Progress() *progress.Bar

Progress returns the attached progress bar, or nil.

func (*Scanner) Scan

func (s *Scanner) Scan() (*Result, error)

Scan runs the full workflow: WordPress detection, enumeration and matching.

func (*Scanner) SetProgress added in v0.2.0

func (s *Scanner) SetProgress(b *progress.Bar)

SetProgress attaches a progress bar to the scanner. A nil bar disables progress reporting entirely.

type Summary added in v0.3.0

type Summary struct {
	DurationMS  int64 `json:"duration_ms"`
	Requests    int   `json:"requests"`
	RateLimited int   `json:"rate_limited"`
	Detected    int   `json:"detected"`
	Findings    int   `json:"findings"`
	Critical    int   `json:"critical"`
	High        int   `json:"high"`
	Medium      int   `json:"medium"`
	Low         int   `json:"low"`
	Users       int   `json:"users"`
}

Summary holds scan-wide statistics, computed at the end of Scan() (skipped with --no-summary). Severity counts come from the findings.

type User added in v0.2.0

type User struct {
	ID   int    `json:"id"`
	Slug string `json:"slug"`
	Name string `json:"name"`
}

User is a WordPress user account discovered during enumeration. ID is the numeric user id (when known), Slug is the author-archive slug, Name is the display name (only the REST API provides it).

type Vulnerability

type Vulnerability struct {
	ID             string   `json:"id"`
	Title          string   `json:"title"`
	CVE            string   `json:"cve"`
	CVSSScore      float64  `json:"cvss_score"`
	Rating         string   `json:"cvss_rating"`
	Description    string   `json:"description"`
	AffectedLabels []string `json:"affected_versions"`
	PublishedAt    string   `json:"published_at"`
	// Epss is the EXPloit Prediction Scoring System probability (0..1)
	// attached by the intel enrichment step; omitted when unknown.
	Epss float64 `json:"epss,omitempty"`
	// Kev reports whether the CVE is listed in the CISA Known Exploited
	// Vulnerabilities catalog; omitted when false.
	Kev bool `json:"kev,omitempty"`
	// CVSSVector is the raw CVSS vector string from the feed record
	// (e.g. "CVSS:3.1/AV:N/AC:L/..."); omitted when unknown.
	CVSSVector string `json:"cvss_vector,omitempty"`
	// Remediation is the human-readable fix guidance from the feed
	// (e.g. "Update the plugin to version X or newer"); omitted when
	// the record carries none.
	Remediation string `json:"remediation,omitempty"`
	// PatchedVersions lists the feed's known-fixed versions for this
	// software entry; omitted when unknown.
	PatchedVersions []string `json:"patched_versions,omitempty"`
}

Vulnerability is one matched database record.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL