mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 15:21:30 +02:00
The option list takes the height the menu has instead of a fixed window. Vulnerability grouping lists finding attributes first, then where they were found. Claude-Session: https://claude.ai/code/session_01VqBkGyjzd8GYYSSMaQHgMG
3351 lines
94 KiB
Python
3351 lines
94 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from dataclasses import field as dc_field
|
|
from enum import StrEnum
|
|
|
|
from shared.definitions.ai_services import AI_FIELD, AI_MODEL_FIELD, AI_QUERY_VALUES
|
|
from shared.definitions.domain_posture import (
|
|
QUERY_VALUES as POSTURE_VALUES,
|
|
)
|
|
from shared.definitions.endpoints import (
|
|
ENDPOINT_CLASSES,
|
|
INTEREST_KEYS,
|
|
SOURCE_LABELS,
|
|
)
|
|
from shared.definitions.evidence import EVIDENCE_ORDER, Evidence
|
|
from shared.definitions.hygiene import (
|
|
QUERY_VALUES as HYGIENE_VALUES,
|
|
)
|
|
from shared.definitions.interest import (
|
|
BAND_ORDER,
|
|
KIND_KEYS,
|
|
)
|
|
from shared.definitions.interest import (
|
|
SOURCE_LABELS as INTEREST_SOURCE_LABELS,
|
|
)
|
|
from shared.definitions.ports import (
|
|
PORT_SOURCE_LABELS,
|
|
SERVICE_CLASS_LABELS,
|
|
)
|
|
from shared.definitions.secrets import (
|
|
DETECTOR_KEYS as SECRET_DETECTOR_KEYS,
|
|
)
|
|
from shared.definitions.secrets import (
|
|
GROUP_ORDER as SECRET_GROUP_KEYS,
|
|
)
|
|
from shared.definitions.secrets import (
|
|
STATE_ORDER as SECRET_STATE_KEYS,
|
|
)
|
|
from shared.definitions.secrets import (
|
|
SecretSource as _SecretSource,
|
|
)
|
|
from shared.definitions.software import (
|
|
CAVEAT_ORDER as CAVEAT_KEYS,
|
|
)
|
|
from shared.definitions.software import (
|
|
CONFIDENCE_ORDER as CONFIDENCE_KEYS,
|
|
)
|
|
from shared.definitions.software import (
|
|
VERSION_SOURCE_LABELS,
|
|
)
|
|
from shared.definitions.surface import SURFACE_NOUN, SurfaceDimension
|
|
from shared.definitions.threat_intel import SIGNAL_ORDER
|
|
from shared.definitions.vulnerabilities import (
|
|
PROTOCOLS,
|
|
SCANNER_LABELS,
|
|
SEVERITY_ORDER,
|
|
VULN_STATES,
|
|
)
|
|
|
|
SECRET_SOURCE_KEYS: tuple[str, ...] = tuple(v.value for v in _SecretSource)
|
|
|
|
MAX_QUERY_LENGTH = 2000
|
|
MAX_QUERY_NODES = 40
|
|
MAX_FREE_TERMS = 8
|
|
MAX_NUMBER = 2_147_483_647
|
|
COUNT_CAP = 10_000
|
|
SNIPPET_RADIUS = 70
|
|
SNIPPET_LENGTH = 190
|
|
|
|
|
|
class FieldType(StrEnum):
|
|
STRING = "string"
|
|
ENUM = "enum"
|
|
NUMBER = "number"
|
|
BYTES = "bytes"
|
|
DURATION = "duration"
|
|
DATE = "date"
|
|
IP = "ip"
|
|
TEXT = "text"
|
|
FLAG = "flag"
|
|
|
|
|
|
class Op(StrEnum):
|
|
MATCH = ":"
|
|
EQ = "="
|
|
NE = "!="
|
|
GT = ">"
|
|
GTE = ">="
|
|
LT = "<"
|
|
LTE = "<="
|
|
RE = "~"
|
|
NRE = "!~"
|
|
|
|
|
|
_STRING_OPS = (Op.MATCH, Op.EQ, Op.NE, Op.RE, Op.NRE)
|
|
_ENUM_OPS = (Op.MATCH, Op.EQ, Op.NE)
|
|
_SCALAR_OPS = (Op.MATCH, Op.EQ, Op.NE, Op.GT, Op.GTE, Op.LT, Op.LTE)
|
|
_TEXT_OPS = (Op.MATCH, Op.EQ, Op.NE)
|
|
|
|
OPS_BY_TYPE: dict[FieldType, tuple[Op, ...]] = {
|
|
FieldType.STRING: _STRING_OPS,
|
|
FieldType.ENUM: _ENUM_OPS,
|
|
FieldType.NUMBER: _SCALAR_OPS,
|
|
FieldType.BYTES: _SCALAR_OPS,
|
|
FieldType.DURATION: _SCALAR_OPS,
|
|
FieldType.DATE: _SCALAR_OPS,
|
|
FieldType.IP: _ENUM_OPS,
|
|
FieldType.TEXT: _TEXT_OPS,
|
|
FieldType.FLAG: (Op.MATCH,),
|
|
}
|
|
|
|
OP_HELP: dict[str, str] = {
|
|
":": "Contains, or equals for numbers and flags",
|
|
"=": "Exact match",
|
|
"!=": "Not equal",
|
|
">": "Greater than",
|
|
">=": "Greater than or equal",
|
|
"<": "Less than",
|
|
"<=": "Less than or equal",
|
|
"~": "Matches regular expression",
|
|
"!~": "Does not match regular expression",
|
|
}
|
|
|
|
CONNECTORS: dict[str, str] = {
|
|
"and": "Both sides must match. Two terms side by side mean and.",
|
|
"or": "Either side may match.",
|
|
"not": "Excludes what follows. A leading - or ! does the same.",
|
|
"( )": "Groups part of a query, as in is:live and (status:403 or status:401).",
|
|
'" "': "Treats the value as one literal phrase.",
|
|
"[a,b]": "Any of the listed values, as in tech:[nginx,apache].",
|
|
"a..b": "Inclusive range, as in status:200..399.",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class QueryField:
|
|
name: str
|
|
type: FieldType
|
|
group: str
|
|
description: str
|
|
example: str
|
|
aliases: tuple[str, ...] = ()
|
|
values: tuple[str, ...] = ()
|
|
facet: str | None = None
|
|
free_text: bool = False
|
|
evidence: str | None = None
|
|
unit: str | None = None
|
|
dynamic_sub: str | None = None
|
|
|
|
|
|
GROUPS: tuple[str, ...] = (
|
|
"Scope",
|
|
"Host",
|
|
"HTTP",
|
|
"Response",
|
|
"Hygiene",
|
|
"AI",
|
|
"Network",
|
|
"Certificates",
|
|
"Findings",
|
|
"Flags",
|
|
)
|
|
|
|
CERT_STATES: tuple[str, ...] = ("expired", "expiring", "self-signed", "valid")
|
|
STATUS_CLASSES: tuple[str, ...] = ("2xx", "3xx", "4xx", "5xx", "none")
|
|
|
|
FLAGS: dict[str, str] = {
|
|
"live": "Responded with 2xx or 3xx",
|
|
"web": "Answered on HTTP at all",
|
|
"new": "Absent from the previous scan of this target",
|
|
"resolved": "Resolves to at least one IP",
|
|
"auth": "Login wall or 401/403",
|
|
"cdn": "Served through a CDN",
|
|
"cloud": "Hosted on a cloud provider's address",
|
|
"waf": "A WAF was fingerprinted",
|
|
"screenshot": "A screenshot was captured",
|
|
"important": "Flagged important",
|
|
"wildcard": "Matched a wildcard DNS record",
|
|
"issue": "Has a cert, exposure or availability problem",
|
|
"sensitive": "An admin or database port is open",
|
|
"http2": "Negotiated HTTP/2",
|
|
"redirect": "Final URL differs from the probed URL",
|
|
"vulnerable": "A vulnerability scan reported a finding on it",
|
|
"kev": "A known-exploited weakness was found on it",
|
|
"exposed": "Flagged as an exposure by a rule, a correlation or AI",
|
|
}
|
|
|
|
FIELDS: tuple[QueryField, ...] = (
|
|
QueryField(
|
|
name="target",
|
|
type=FieldType.STRING,
|
|
group="Scope",
|
|
description="Target this host belongs to.",
|
|
example="target:acme.com",
|
|
aliases=("scope",),
|
|
facet="target",
|
|
),
|
|
QueryField(
|
|
name="host",
|
|
type=FieldType.STRING,
|
|
group="Host",
|
|
description="Hostname of the asset.",
|
|
example="host:api",
|
|
aliases=("name", "subdomain"),
|
|
free_text=True,
|
|
evidence="host",
|
|
),
|
|
QueryField(
|
|
name="url",
|
|
type=FieldType.STRING,
|
|
group="Host",
|
|
description="Probed URL, or the final URL after redirects.",
|
|
example="url:/admin",
|
|
free_text=True,
|
|
evidence="url",
|
|
),
|
|
QueryField(
|
|
name="path",
|
|
type=FieldType.STRING,
|
|
group="Host",
|
|
description="Path component of the probed URL.",
|
|
example="path:/api/v1",
|
|
evidence="path",
|
|
),
|
|
QueryField(
|
|
name="cname",
|
|
type=FieldType.STRING,
|
|
group="Host",
|
|
description="CNAME target the host points at.",
|
|
example="cname:s3.amazonaws.com",
|
|
free_text=True,
|
|
evidence="cname",
|
|
),
|
|
QueryField(
|
|
name="source",
|
|
type=FieldType.ENUM,
|
|
group="Host",
|
|
description="Tool or feed that discovered the host.",
|
|
example="source:crtsh",
|
|
facet="source",
|
|
free_text=True,
|
|
evidence="source",
|
|
),
|
|
QueryField(
|
|
name="discovered",
|
|
type=FieldType.DATE,
|
|
group="Host",
|
|
description="When the host was first seen in this scan.",
|
|
example="discovered:<7d",
|
|
aliases=("found", "seen"),
|
|
),
|
|
QueryField(
|
|
name="status",
|
|
type=FieldType.NUMBER,
|
|
group="HTTP",
|
|
description="HTTP status code. Accepts a class such as 4xx.",
|
|
example="status:>=500",
|
|
aliases=("code", "http.status"),
|
|
values=STATUS_CLASSES,
|
|
),
|
|
QueryField(
|
|
name="title",
|
|
type=FieldType.STRING,
|
|
group="HTTP",
|
|
description="Page title of the response.",
|
|
example='title:"index of"',
|
|
free_text=True,
|
|
evidence="title",
|
|
),
|
|
QueryField(
|
|
name="server",
|
|
type=FieldType.STRING,
|
|
group="HTTP",
|
|
description="Server header banner.",
|
|
example="server:nginx",
|
|
aliases=("webserver",),
|
|
free_text=True,
|
|
evidence="server",
|
|
),
|
|
QueryField(
|
|
name="tech",
|
|
type=FieldType.ENUM,
|
|
group="HTTP",
|
|
description="Technology fingerprinted on the response.",
|
|
example="tech:[jenkins,tomcat]",
|
|
facet="tech",
|
|
free_text=True,
|
|
evidence="tech",
|
|
),
|
|
QueryField(
|
|
name="content_type",
|
|
type=FieldType.STRING,
|
|
group="HTTP",
|
|
description="Content-Type of the response.",
|
|
example="content_type:json",
|
|
aliases=("ctype", "mime"),
|
|
evidence="content_type",
|
|
),
|
|
QueryField(
|
|
name="size",
|
|
type=FieldType.BYTES,
|
|
group="HTTP",
|
|
description="Response body size. Understands kb and mb.",
|
|
example="size:>500kb",
|
|
aliases=("length", "content_length"),
|
|
unit="bytes",
|
|
),
|
|
QueryField(
|
|
name="words",
|
|
type=FieldType.NUMBER,
|
|
group="HTTP",
|
|
description="Word count of the response body.",
|
|
example="words:<20",
|
|
),
|
|
QueryField(
|
|
name="paths",
|
|
type=FieldType.NUMBER,
|
|
group="HTTP",
|
|
description="Number of endpoints discovered on the host.",
|
|
example="paths:>50",
|
|
aliases=("endpoints",),
|
|
),
|
|
QueryField(
|
|
name="lines",
|
|
type=FieldType.NUMBER,
|
|
group="HTTP",
|
|
description="Line count of the response body.",
|
|
example="lines:>2000",
|
|
),
|
|
QueryField(
|
|
name="time",
|
|
type=FieldType.DURATION,
|
|
group="HTTP",
|
|
description="Response time. Understands ms and s.",
|
|
example="time:>3s",
|
|
aliases=("rt", "response_time"),
|
|
unit="seconds",
|
|
),
|
|
QueryField(
|
|
name="redirect",
|
|
type=FieldType.STRING,
|
|
group="HTTP",
|
|
description="Location header the response redirects to.",
|
|
example="redirect:login",
|
|
aliases=("location",),
|
|
free_text=True,
|
|
evidence="redirect",
|
|
),
|
|
QueryField(
|
|
name="favicon",
|
|
type=FieldType.STRING,
|
|
group="HTTP",
|
|
description="mmh3 favicon hash.",
|
|
example="favicon:-1278323681",
|
|
free_text=True,
|
|
evidence="favicon",
|
|
),
|
|
QueryField(
|
|
name="screenshot",
|
|
type=FieldType.STRING,
|
|
group="HTTP",
|
|
description=(
|
|
"Perceptual hash of the rendered page. Matches every host whose "
|
|
"screenshot renders the same way."
|
|
),
|
|
example="screenshot:0030242430d41010",
|
|
aliases=("render",),
|
|
),
|
|
QueryField(
|
|
name="body",
|
|
type=FieldType.TEXT,
|
|
group="Response",
|
|
description=(
|
|
"Words in the captured response body. Matches whole words and "
|
|
"prefixes. Quote a value to match a phrase."
|
|
),
|
|
example='body:"internal server"',
|
|
aliases=("response",),
|
|
free_text=True,
|
|
evidence="body",
|
|
),
|
|
QueryField(
|
|
name="header",
|
|
type=FieldType.TEXT,
|
|
group="Response",
|
|
description=(
|
|
"Words in the response headers. Use header.<name>:<value> to pin "
|
|
"it to one header."
|
|
),
|
|
example="header.x-powered-by:php",
|
|
aliases=("headers",),
|
|
free_text=True,
|
|
evidence="header",
|
|
dynamic_sub="header name",
|
|
),
|
|
QueryField(
|
|
name="ip",
|
|
type=FieldType.IP,
|
|
group="Network",
|
|
description="Resolved IP address. Accepts a CIDR range.",
|
|
example="ip:10.0.0.0/8",
|
|
free_text=True,
|
|
evidence="ip",
|
|
),
|
|
QueryField(
|
|
name="asn",
|
|
type=FieldType.NUMBER,
|
|
group="Network",
|
|
description="Autonomous system number.",
|
|
example="asn:15169",
|
|
),
|
|
QueryField(
|
|
name="org",
|
|
type=FieldType.STRING,
|
|
group="Network",
|
|
description="Autonomous system owner.",
|
|
example='org:"digital ocean"',
|
|
aliases=("asn_org",),
|
|
free_text=True,
|
|
evidence="org",
|
|
),
|
|
QueryField(
|
|
name="cdn",
|
|
type=FieldType.STRING,
|
|
group="Network",
|
|
description="CDN in front of the host. yes or no filters on presence.",
|
|
example="cdn:cloudflare",
|
|
free_text=True,
|
|
evidence="cdn",
|
|
),
|
|
QueryField(
|
|
name="waf",
|
|
type=FieldType.STRING,
|
|
group="Network",
|
|
description="WAF fingerprinted on the host. yes or no filters on presence.",
|
|
example="waf:no",
|
|
free_text=True,
|
|
evidence="waf",
|
|
),
|
|
QueryField(
|
|
name="port",
|
|
type=FieldType.NUMBER,
|
|
group="Network",
|
|
description="Open port on a resolved IP.",
|
|
example="port:[22,3389]",
|
|
),
|
|
QueryField(
|
|
name="service",
|
|
type=FieldType.ENUM,
|
|
group="Network",
|
|
description="Service name on an open port.",
|
|
example="service:ssh",
|
|
facet="service",
|
|
),
|
|
QueryField(
|
|
name="hygiene",
|
|
type=FieldType.ENUM,
|
|
group="Hygiene",
|
|
description=(
|
|
"A hardening check the host fails on any of its responses. "
|
|
"Also takes warning, info, any or none."
|
|
),
|
|
example="hygiene:cookie_no_secure",
|
|
aliases=("hardening",),
|
|
values=HYGIENE_VALUES,
|
|
facet="hygiene",
|
|
),
|
|
QueryField(
|
|
name="posture",
|
|
type=FieldType.ENUM,
|
|
group="Hygiene",
|
|
description=(
|
|
"A domain posture check the host's zone fails: SPF, DMARC, DKIM, "
|
|
"MTA-STS, DNSSEC or CAA. Also takes warning, info, any or none."
|
|
),
|
|
example="posture:dmarc_missing",
|
|
aliases=("dns_posture", "mail"),
|
|
values=POSTURE_VALUES,
|
|
facet="posture",
|
|
),
|
|
QueryField(
|
|
name=AI_FIELD,
|
|
type=FieldType.ENUM,
|
|
group="AI",
|
|
description=(
|
|
"AI service identified on any of the host's web assets. "
|
|
"Takes a service, a category, yes or no."
|
|
),
|
|
example="ai:ollama",
|
|
aliases=("ai_service",),
|
|
values=AI_QUERY_VALUES,
|
|
),
|
|
QueryField(
|
|
name=AI_MODEL_FIELD,
|
|
type=FieldType.STRING,
|
|
group="AI",
|
|
description="Model an AI service on the host listed. yes or no filters on presence.",
|
|
example="ai.model~llama",
|
|
),
|
|
QueryField(
|
|
name="cert",
|
|
type=FieldType.ENUM,
|
|
group="Certificates",
|
|
description="Certificate state.",
|
|
example="cert:expired",
|
|
values=CERT_STATES,
|
|
),
|
|
QueryField(
|
|
name="cert.cn",
|
|
type=FieldType.STRING,
|
|
group="Certificates",
|
|
description="Certificate subject common name.",
|
|
example="cert.cn:*.internal",
|
|
free_text=True,
|
|
evidence="cert",
|
|
),
|
|
QueryField(
|
|
name="cert.san",
|
|
type=FieldType.STRING,
|
|
group="Certificates",
|
|
description="Subject alternative name on the certificate.",
|
|
example="cert.san:staging",
|
|
free_text=True,
|
|
evidence="cert",
|
|
),
|
|
QueryField(
|
|
name="cert.issuer",
|
|
type=FieldType.STRING,
|
|
group="Certificates",
|
|
description="Certificate issuer.",
|
|
example='cert.issuer:"let\'s encrypt"',
|
|
free_text=True,
|
|
evidence="cert",
|
|
),
|
|
QueryField(
|
|
name="cert.expires",
|
|
type=FieldType.DATE,
|
|
group="Certificates",
|
|
description="Certificate expiry date.",
|
|
example="cert.expires:<30d",
|
|
),
|
|
QueryField(
|
|
name="tls.version",
|
|
type=FieldType.STRING,
|
|
group="Certificates",
|
|
description="Negotiated TLS version.",
|
|
example="tls.version:tls10",
|
|
),
|
|
QueryField(
|
|
name="content_hash",
|
|
type=FieldType.STRING,
|
|
group="Response",
|
|
description="Hash of the response body.",
|
|
example="content_hash:8f4e2a",
|
|
aliases=("body_hash",),
|
|
),
|
|
QueryField(
|
|
name="tracking",
|
|
type=FieldType.STRING,
|
|
group="Response",
|
|
description="Analytics, tag manager or advertising account ID.",
|
|
example="tracking=UA-58851320",
|
|
),
|
|
QueryField(
|
|
name="jarm",
|
|
type=FieldType.STRING,
|
|
group="Certificates",
|
|
description="JARM TLS fingerprint.",
|
|
example="jarm:29d3fd00029d29d0",
|
|
),
|
|
QueryField(
|
|
name="cert.fingerprint",
|
|
type=FieldType.STRING,
|
|
group="Certificates",
|
|
description="Hash of the certificate.",
|
|
example="cert.fingerprint:9f2b1c",
|
|
aliases=("cert.hash",),
|
|
),
|
|
QueryField(
|
|
name="header_hash",
|
|
type=FieldType.STRING,
|
|
group="Response",
|
|
description="Hash of the response header set.",
|
|
example="header_hash:3ab91c",
|
|
),
|
|
QueryField(
|
|
name="is",
|
|
type=FieldType.FLAG,
|
|
group="Flags",
|
|
description="Property of the host.",
|
|
example="is:live",
|
|
aliases=("has",),
|
|
values=tuple(FLAGS),
|
|
),
|
|
QueryField(
|
|
name="vuln",
|
|
type=FieldType.ENUM,
|
|
group="Findings",
|
|
description="Severity of a finding recorded on this host.",
|
|
example="vuln:critical",
|
|
aliases=("finding", "severity"),
|
|
values=SEVERITY_ORDER,
|
|
facet="vuln",
|
|
),
|
|
QueryField(
|
|
name="cve",
|
|
type=FieldType.STRING,
|
|
group="Findings",
|
|
description="Published vulnerability identifier reported on this host.",
|
|
example="cve:CVE-2021-44228",
|
|
),
|
|
QueryField(
|
|
name="exposure",
|
|
type=FieldType.ENUM,
|
|
group="Findings",
|
|
description="The reason this host was flagged as an exposure.",
|
|
example="exposure:admin_interface",
|
|
aliases=("interest", "worth"),
|
|
values=KIND_KEYS,
|
|
facet="interest",
|
|
),
|
|
QueryField(
|
|
name="flagged",
|
|
type=FieldType.ENUM,
|
|
group="Findings",
|
|
description="The source of the flag: a rule, a correlation, or AI.",
|
|
example="flagged:ai",
|
|
values=tuple(INTEREST_SOURCE_LABELS),
|
|
facet="flagged",
|
|
),
|
|
QueryField(
|
|
name="exposure_band",
|
|
type=FieldType.ENUM,
|
|
group="Findings",
|
|
description="Strength of the exposure on this host.",
|
|
example="exposure_band:critical",
|
|
aliases=("interest_band", "band"),
|
|
values=BAND_ORDER,
|
|
),
|
|
)
|
|
|
|
EVIDENCE_LABELS: dict[str, str] = {
|
|
"host": "Hostname",
|
|
"url": "URL",
|
|
"path": "Path",
|
|
"cname": "CNAME",
|
|
"source": "Source",
|
|
"title": "Title",
|
|
"server": "Server",
|
|
"tech": "Tech",
|
|
"content_type": "Content type",
|
|
"redirect": "Redirect",
|
|
"favicon": "Favicon",
|
|
"screenshot": "Rendered page",
|
|
"body": "Response body",
|
|
"header": "Response headers",
|
|
"ip": "IP",
|
|
"org": "Network owner",
|
|
"cdn": "CDN",
|
|
"waf": "WAF",
|
|
"cert": "Certificate",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GroupDimension:
|
|
key: str
|
|
label: str
|
|
description: str
|
|
|
|
|
|
GROUP_DIMENSIONS: tuple[GroupDimension, ...] = (
|
|
GroupDimension(
|
|
key="target",
|
|
label="Target",
|
|
description="Rows belonging to the same target",
|
|
),
|
|
GroupDimension(
|
|
key="ip",
|
|
label="IP address",
|
|
description="Names sharing a resolved address",
|
|
),
|
|
GroupDimension(
|
|
key="favicon",
|
|
label="Favicon",
|
|
description="Names serving the same favicon",
|
|
),
|
|
GroupDimension(
|
|
key="screenshot",
|
|
label="Rendered page",
|
|
description="Names whose screenshots render the same page",
|
|
),
|
|
GroupDimension(
|
|
key="title",
|
|
label="Page title",
|
|
description="Names serving the same page title",
|
|
),
|
|
GroupDimension(
|
|
key="cname",
|
|
label="CNAME target",
|
|
description="Names pointing at the same alias",
|
|
),
|
|
GroupDimension(
|
|
key="tech",
|
|
label="Technology",
|
|
description="Names running the same technology",
|
|
),
|
|
GroupDimension(
|
|
key="content_hash",
|
|
label="Identical content",
|
|
description="Names serving a byte-identical response",
|
|
),
|
|
GroupDimension(
|
|
key="jarm",
|
|
label="TLS fingerprint",
|
|
description="Names sharing a JARM TLS fingerprint",
|
|
),
|
|
GroupDimension(
|
|
key="cert.fingerprint",
|
|
label="Certificate",
|
|
description="Names presenting the same certificate",
|
|
),
|
|
GroupDimension(
|
|
key="cert.issuer",
|
|
label="Certificate issuer",
|
|
description="Names whose certificate has the same issuer",
|
|
),
|
|
GroupDimension(
|
|
key="header_hash",
|
|
label="Header set",
|
|
description="Names returning an identical set of response headers",
|
|
),
|
|
GroupDimension(
|
|
key="server",
|
|
label="Server banner",
|
|
description="Names reporting the same server header",
|
|
),
|
|
GroupDimension(
|
|
key="cdn",
|
|
label="CDN",
|
|
description="Names fronted by the same CDN",
|
|
),
|
|
GroupDimension(
|
|
key="status",
|
|
label="Status class",
|
|
description="Names by HTTP response class",
|
|
),
|
|
GroupDimension(
|
|
key="hygiene",
|
|
label="Hygiene check",
|
|
description="Names failing the same hardening check",
|
|
),
|
|
GroupDimension(
|
|
key="posture",
|
|
label="Posture check",
|
|
description="Names whose zone fails the same posture check",
|
|
),
|
|
)
|
|
|
|
RENDER_SAMPLE_HOSTS = 12
|
|
MAX_GROUPS = 50
|
|
|
|
|
|
EXAMPLE_GROUPS: tuple[str, ...] = (
|
|
"Takeover risk",
|
|
"Exposed services",
|
|
"Non-production",
|
|
"Access control",
|
|
"Certificates",
|
|
"Origin exposure",
|
|
"Hygiene",
|
|
"Change",
|
|
"Health",
|
|
"Exposures",
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class QueryExample:
|
|
query: str
|
|
description: str
|
|
group: str
|
|
generic: bool = False
|
|
|
|
|
|
EXAMPLES: tuple[QueryExample, ...] = (
|
|
QueryExample(
|
|
query="is:resolved and not is:web",
|
|
description="Resolving hosts with no HTTP service",
|
|
group="Takeover risk",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="cname:. and not is:web",
|
|
description="CNAME records with no HTTP service",
|
|
group="Takeover risk",
|
|
),
|
|
QueryExample(
|
|
query="cname:elb.amazonaws.com and status:404",
|
|
description="Load balancer aliases returning 404",
|
|
group="Takeover risk",
|
|
),
|
|
QueryExample(
|
|
query=(
|
|
"cname:[myshopify.com,github.io,herokuapp.com,azurewebsites.net,"
|
|
"netlify.app,pantheonsite.io,wpengine.com,ghost.io]"
|
|
),
|
|
description="Aliases pointing to third-party hosting",
|
|
group="Takeover risk",
|
|
),
|
|
QueryExample(
|
|
query="cname:. and not is:resolved",
|
|
description="CNAME records that fail to resolve",
|
|
group="Takeover risk",
|
|
),
|
|
QueryExample(
|
|
query="is:wildcard",
|
|
description="Hosts matched only by a wildcard record",
|
|
group="Takeover risk",
|
|
),
|
|
QueryExample(
|
|
query="is:sensitive",
|
|
description="Administrative or database ports exposed",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="port:[3306,5432,27017,6379,9200,11211,5984]",
|
|
description="Database ports reachable from the internet",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="port:[3306,5432,1433,27017,6379,9200,11211,5984] and hosts:>10",
|
|
description="Database ports on addresses shared by more than ten hostnames",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="port:[22,23,3389,5900]",
|
|
description="Remote administration ports reachable",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query='body:"index of" and status:200',
|
|
description="Directory listings enabled",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query=(
|
|
'body:"begin rsa private key" or body:"begin private key" '
|
|
"or body:aws_secret_access_key"
|
|
),
|
|
description="Private key material in a response body",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query='body:traceback or body:"stack trace" or title:exception',
|
|
description="Application stack traces in responses",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query='body:"sql syntax" or body:"odbc driver"',
|
|
description="Database error messages in responses",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="title:phpinfo or body:phpinfo",
|
|
description="phpinfo diagnostic pages exposed",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="body:swagger or url:swagger or title:swagger",
|
|
description="API documentation publicly accessible",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query=(
|
|
"tech:[jenkins,gitlab,grafana,kibana,prometheus,sonarqube,jira,"
|
|
"confluence] and status:200"
|
|
),
|
|
description="Developer tooling exposed to the internet",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="host:[jenkins,gitlab,grafana,kibana,jira,confluence,vault,consul]",
|
|
description="Hostnames indicating internal tooling",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="content_type:json and status:200",
|
|
description="JSON endpoints served directly",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="host:[staging,dev,test,uat,qa,sandbox,preprod,demo]",
|
|
description="Non-production hostnames publicly resolvable",
|
|
group="Non-production",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query=(
|
|
"host:[staging,dev,test,uat,qa,sandbox,preprod] and is:live and not is:auth"
|
|
),
|
|
description="Non-production hosts served without authentication",
|
|
group="Non-production",
|
|
),
|
|
QueryExample(
|
|
query="(host:admin or host:portal or host:console) and is:live and not is:auth",
|
|
description="Administrative interfaces without authentication",
|
|
group="Non-production",
|
|
),
|
|
QueryExample(
|
|
query="is:auth and is:live",
|
|
description="Authenticated interfaces currently serving",
|
|
group="Access control",
|
|
),
|
|
QueryExample(
|
|
query="is:auth",
|
|
description="Hosts requiring authentication",
|
|
group="Access control",
|
|
),
|
|
QueryExample(
|
|
query="header.access-control-allow-origin:*",
|
|
description="Wildcard cross-origin policy on responses",
|
|
group="Access control",
|
|
),
|
|
QueryExample(
|
|
query="cert:expired and is:live",
|
|
description="Reachable hosts serving an expired certificate",
|
|
group="Certificates",
|
|
),
|
|
QueryExample(
|
|
query="cert:expiring",
|
|
description="Certificates expiring within 30 days",
|
|
group="Certificates",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="cert:self-signed",
|
|
description="Self-signed certificates in use",
|
|
group="Certificates",
|
|
),
|
|
QueryExample(
|
|
query="cert.cn:internal or cert.san:internal or cert.san:local",
|
|
description="Internal hostnames disclosed in certificates",
|
|
group="Certificates",
|
|
),
|
|
QueryExample(
|
|
query="cert.san:*. and is:live",
|
|
description="Wildcard certificates in use",
|
|
group="Certificates",
|
|
),
|
|
QueryExample(
|
|
query="tls.version:[tls10,tls11]",
|
|
description="Obsolete TLS versions negotiated",
|
|
group="Certificates",
|
|
),
|
|
QueryExample(
|
|
query="is:live and not cdn:yes and waf:no",
|
|
description="Hosts served without CDN or WAF",
|
|
group="Origin exposure",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="server:[apache,nginx,iis] and not cdn:yes",
|
|
description="Origin server banners disclosed",
|
|
group="Origin exposure",
|
|
),
|
|
QueryExample(
|
|
query="tech:[php,tomcat,jboss,weblogic,coldfusion] and status:200",
|
|
description="Runtimes with significant CVE history",
|
|
group="Origin exposure",
|
|
),
|
|
QueryExample(
|
|
query="hygiene:warning",
|
|
description="Hosts failing a warning-level hardening check",
|
|
group="Hygiene",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="hygiene:cors_credentials",
|
|
description="CORS credentials allowed with a wildcard or null origin",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="hygiene:cookie_no_secure",
|
|
description="Cookies set over https without Secure",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="hygiene:cookie_no_httponly",
|
|
description="Session cookies readable by scripts",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="hygiene:no_frame_protection and is:live",
|
|
description="Pages that can be framed by another site",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="hygiene:[csp_unsafe_inline,csp_wildcard_script]",
|
|
description="Content-Security-Policy that does not restrict scripts",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="hygiene:no_https_redirect",
|
|
description="Plaintext http with no redirect to https",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="hygiene:[server_version,runtime_disclosed] and not cdn:yes",
|
|
description="Origin software versions disclosed in headers",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="hygiene:none and is:live",
|
|
description="Reachable hosts passing every hardening check",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="is:new and (status:2xx or status:3xx)",
|
|
description="Newly reachable since the previous scan",
|
|
group="Change",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:new and is:auth",
|
|
description="New authenticated interfaces since the previous scan",
|
|
group="Change",
|
|
),
|
|
QueryExample(
|
|
query="is:new and cert:expired",
|
|
description="New hosts serving an expired certificate",
|
|
group="Change",
|
|
),
|
|
QueryExample(
|
|
query="is:new",
|
|
description="Hosts absent from the previous scan",
|
|
group="Change",
|
|
),
|
|
QueryExample(
|
|
query="discovered:<7d and is:live",
|
|
description="Reachable hosts discovered in the last 7 days",
|
|
group="Change",
|
|
),
|
|
QueryExample(
|
|
query="status:>=500 or title:exception",
|
|
description="Server-side errors returned",
|
|
group="Health",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="not is:resolved",
|
|
description="Hostnames that no longer resolve",
|
|
group="Health",
|
|
),
|
|
QueryExample(
|
|
query="size:<500 and status:200",
|
|
description="Minimal-content responses returning 200",
|
|
group="Health",
|
|
),
|
|
QueryExample(
|
|
query="time:>5s",
|
|
description="Responses slower than 5 seconds",
|
|
group="Health",
|
|
),
|
|
QueryExample(
|
|
query="is:redirect and not is:live",
|
|
description="Redirects terminating in an error",
|
|
group="Health",
|
|
),
|
|
QueryExample(
|
|
query="is:exposed",
|
|
description="Everything flagged as an exposure",
|
|
group="Exposures",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:exposed and is:new",
|
|
description="Flagged for the first time by this scan",
|
|
group="Exposures",
|
|
),
|
|
QueryExample(
|
|
query="flagged:ai",
|
|
description="Flagged by AI judgement",
|
|
group="Exposures",
|
|
),
|
|
QueryExample(
|
|
query="exposure_band:critical",
|
|
description="Exposures in the critical band",
|
|
group="Exposures",
|
|
),
|
|
QueryExample(
|
|
query="exposure:admin_interface and not is:auth",
|
|
description="Administrative surfaces answering without a login",
|
|
group="Exposures",
|
|
),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class QueryRegistry:
|
|
key: str
|
|
noun: str
|
|
noun_plural: str
|
|
fields: tuple[QueryField, ...]
|
|
flags: dict[str, str]
|
|
groups: tuple[str, ...]
|
|
dimensions: tuple[GroupDimension, ...]
|
|
examples: tuple[QueryExample, ...]
|
|
example_groups: tuple[str, ...]
|
|
by_name: dict[str, QueryField] = dc_field(init=False, repr=False)
|
|
canonical: dict[str, str] = dc_field(init=False, repr=False)
|
|
dynamic_parent: str | None = dc_field(init=False, repr=False)
|
|
|
|
def __post_init__(self) -> None:
|
|
set_ = object.__setattr__
|
|
set_(self, "by_name", {f.name: f for f in self.fields})
|
|
set_(
|
|
self,
|
|
"canonical",
|
|
{
|
|
**{f.name: f.name for f in self.fields},
|
|
**{a: f.name for f in self.fields for a in f.aliases},
|
|
},
|
|
)
|
|
dynamic = next((f.name for f in self.fields if f.dynamic_sub), None)
|
|
set_(self, "dynamic_parent", dynamic)
|
|
|
|
|
|
HOST_QUERY = QueryRegistry(
|
|
key="host",
|
|
noun=SURFACE_NOUN[SurfaceDimension.WEB_ASSETS.value][0],
|
|
noun_plural=SURFACE_NOUN[SurfaceDimension.WEB_ASSETS.value][1],
|
|
fields=FIELDS,
|
|
flags=FLAGS,
|
|
groups=GROUPS,
|
|
dimensions=GROUP_DIMENSIONS,
|
|
examples=EXAMPLES,
|
|
example_groups=EXAMPLE_GROUPS,
|
|
)
|
|
|
|
IP_GROUPS: tuple[str, ...] = (
|
|
"Scope",
|
|
"Address",
|
|
"Network",
|
|
"Services",
|
|
"Hosts",
|
|
"Findings",
|
|
"Flags",
|
|
)
|
|
|
|
IP_FLAGS: dict[str, str] = {
|
|
"new": "Not recorded by an earlier scan of this target",
|
|
"alive": "Responded to a probe",
|
|
"open": "One or more ports are open",
|
|
"sensitive": "An admin or database port is open",
|
|
"hosted": "One or more hostnames resolve to it",
|
|
"web": "An HTTP service was probed on it",
|
|
"cdn": "Fronted by a CDN",
|
|
"ptr": "Has a PTR record",
|
|
"private": "In a private address range",
|
|
"v4": "IPv4 address",
|
|
"v6": "IPv6 address",
|
|
"vulnerable": "A vulnerability scan reported a finding on it",
|
|
"kev": "A known-exploited weakness was found on it",
|
|
}
|
|
|
|
IP_EXPOSURE: dict[str, str] = {
|
|
"open": "Open ports",
|
|
"responding": "Responding",
|
|
"quiet": "No response",
|
|
}
|
|
|
|
IP_FIELDS: tuple[QueryField, ...] = (
|
|
QueryField(
|
|
name="target",
|
|
type=FieldType.STRING,
|
|
group="Scope",
|
|
description="Target this address belongs to.",
|
|
example="target:acme.com",
|
|
aliases=("scope",),
|
|
facet="target",
|
|
),
|
|
QueryField(
|
|
name="ip",
|
|
type=FieldType.IP,
|
|
group="Address",
|
|
description="IP address. Accepts a CIDR range.",
|
|
example="ip:104.16.0.0/12",
|
|
aliases=("address", "addr"),
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="ptr",
|
|
type=FieldType.STRING,
|
|
group="Address",
|
|
description="PTR record for the address.",
|
|
example="ptr:.internal",
|
|
aliases=("reverse", "rdns"),
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="asn",
|
|
type=FieldType.NUMBER,
|
|
group="Network",
|
|
description="Autonomous system number announcing the address.",
|
|
example="asn:13335",
|
|
),
|
|
QueryField(
|
|
name="org",
|
|
type=FieldType.STRING,
|
|
group="Network",
|
|
description="Operator of the autonomous system.",
|
|
example='org:"digital ocean"',
|
|
aliases=("asn_org", "operator"),
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="country",
|
|
type=FieldType.ENUM,
|
|
group="Network",
|
|
description="Two-letter registered country of the network block.",
|
|
example="country:DE",
|
|
facet="country",
|
|
),
|
|
QueryField(
|
|
name="prefix",
|
|
type=FieldType.STRING,
|
|
group="Network",
|
|
description="Announced prefix containing the address.",
|
|
example="prefix:104.16",
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="cdn",
|
|
type=FieldType.STRING,
|
|
group="Network",
|
|
description="CDN in front of the address. yes or no filters on presence.",
|
|
example="cdn:cloudflare",
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="port",
|
|
type=FieldType.NUMBER,
|
|
group="Services",
|
|
description="Open port on the address.",
|
|
example="port:[22,3389]",
|
|
facet="port",
|
|
),
|
|
QueryField(
|
|
name="service",
|
|
type=FieldType.ENUM,
|
|
group="Services",
|
|
description="Service name on an open port.",
|
|
example="service:ssh",
|
|
facet="service",
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="ports",
|
|
type=FieldType.NUMBER,
|
|
group="Services",
|
|
description="Number of open ports on the address.",
|
|
example="ports:>10",
|
|
aliases=("port_count",),
|
|
),
|
|
QueryField(
|
|
name="host",
|
|
type=FieldType.STRING,
|
|
group="Hosts",
|
|
description="Hostname that resolves to the address.",
|
|
example="host:api",
|
|
aliases=("name", "subdomain"),
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="hosts",
|
|
type=FieldType.NUMBER,
|
|
group="Hosts",
|
|
description="Number of hostnames resolving to the address.",
|
|
example="hosts:>5",
|
|
aliases=("host_count",),
|
|
),
|
|
QueryField(
|
|
name="assets",
|
|
type=FieldType.NUMBER,
|
|
group="Hosts",
|
|
description="Number of HTTP services probed on the address.",
|
|
example="assets:>0",
|
|
aliases=("asset_count",),
|
|
),
|
|
QueryField(
|
|
name="is",
|
|
type=FieldType.FLAG,
|
|
group="Flags",
|
|
description="Property of the address.",
|
|
example="is:sensitive",
|
|
aliases=("has",),
|
|
values=tuple(IP_FLAGS),
|
|
),
|
|
QueryField(
|
|
name="vuln",
|
|
type=FieldType.ENUM,
|
|
group="Findings",
|
|
description="Severity of a finding recorded on this address.",
|
|
example="vuln:critical",
|
|
aliases=("finding",),
|
|
values=SEVERITY_ORDER,
|
|
),
|
|
QueryField(
|
|
name="cve",
|
|
type=FieldType.STRING,
|
|
group="Findings",
|
|
description="Published vulnerability identifier reported on this address.",
|
|
example="cve:CVE-2021-44228",
|
|
),
|
|
)
|
|
|
|
IP_GROUP_DIMENSIONS: tuple[GroupDimension, ...] = (
|
|
GroupDimension(
|
|
key="target",
|
|
label="Target",
|
|
description="Rows belonging to the same target",
|
|
),
|
|
GroupDimension(
|
|
key="asn",
|
|
label="Autonomous system",
|
|
description="Addresses announced by the same AS",
|
|
),
|
|
GroupDimension(
|
|
key="org",
|
|
label="Network operator",
|
|
description="Addresses run by the same operator",
|
|
),
|
|
GroupDimension(
|
|
key="prefix",
|
|
label="Announced prefix",
|
|
description="Addresses inside the same announced prefix",
|
|
),
|
|
GroupDimension(
|
|
key="country",
|
|
label="Country",
|
|
description="Addresses registered in the same country",
|
|
),
|
|
GroupDimension(
|
|
key="cdn",
|
|
label="CDN",
|
|
description="Addresses fronted by the same CDN",
|
|
),
|
|
GroupDimension(
|
|
key="port",
|
|
label="Open port",
|
|
description="Addresses exposing the same port",
|
|
),
|
|
GroupDimension(
|
|
key="service",
|
|
label="Service",
|
|
description="Addresses running the same service",
|
|
),
|
|
)
|
|
|
|
IP_EXAMPLE_GROUPS: tuple[str, ...] = (
|
|
"Exposed services",
|
|
"Origin exposure",
|
|
"Hosting",
|
|
"Hygiene",
|
|
"Change",
|
|
)
|
|
|
|
IP_EXAMPLES: tuple[QueryExample, ...] = (
|
|
QueryExample(
|
|
query="is:sensitive",
|
|
description="Administrative or database ports exposed",
|
|
group="Exposed services",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:new",
|
|
description="Addresses not recorded by the previous scan",
|
|
group="Change",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:new and is:sensitive",
|
|
description="Administrative ports on addresses that are new this scan",
|
|
group="Change",
|
|
),
|
|
QueryExample(
|
|
query="port:[3306,5432,27017,6379,9200,11211,5984]",
|
|
description="Database ports reachable from the internet",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="port:[22,23,3389,5900]",
|
|
description="Remote administration ports reachable",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="service:[ftp,telnet,rlogin,vnc]",
|
|
description="Cleartext remote-access services running",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="ports:>10",
|
|
description="Addresses exposing more than ten ports",
|
|
group="Exposed services",
|
|
),
|
|
QueryExample(
|
|
query="is:open and not is:cdn",
|
|
description="Open ports on addresses without a CDN",
|
|
group="Origin exposure",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:web and not is:cdn",
|
|
description="HTTP served directly from the origin address",
|
|
group="Origin exposure",
|
|
),
|
|
QueryExample(
|
|
query="is:cdn and is:open",
|
|
description="Open ports on CDN-fronted addresses",
|
|
group="Origin exposure",
|
|
),
|
|
QueryExample(
|
|
query="hosts:>10",
|
|
description="Addresses shared by more than ten hostnames",
|
|
group="Hosting",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:open and not is:hosted",
|
|
description="Open ports on addresses with no hostname",
|
|
group="Hosting",
|
|
),
|
|
QueryExample(
|
|
query="is:v6",
|
|
description="IPv6 addresses in the attack surface",
|
|
group="Hosting",
|
|
),
|
|
QueryExample(
|
|
query="is:alive and not is:ptr",
|
|
description="Responding addresses with no PTR record",
|
|
group="Hosting",
|
|
),
|
|
QueryExample(
|
|
query="is:private",
|
|
description="Private addresses published in public DNS",
|
|
group="Hygiene",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:hosted and not is:alive",
|
|
description="Hostnames resolving to an unresponsive address",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="ptr:. and not is:hosted",
|
|
description="Addresses with a PTR record but no known hostname",
|
|
group="Hygiene",
|
|
),
|
|
)
|
|
|
|
IP_QUERY = QueryRegistry(
|
|
key="ip",
|
|
noun=SURFACE_NOUN[SurfaceDimension.IPS.value][0],
|
|
noun_plural=SURFACE_NOUN[SurfaceDimension.IPS.value][1],
|
|
fields=IP_FIELDS,
|
|
flags=IP_FLAGS,
|
|
groups=IP_GROUPS,
|
|
dimensions=IP_GROUP_DIMENSIONS,
|
|
examples=IP_EXAMPLES,
|
|
example_groups=IP_EXAMPLE_GROUPS,
|
|
)
|
|
|
|
SERVICE_GROUPS: tuple[str, ...] = (
|
|
"Scope",
|
|
"Service",
|
|
"Software",
|
|
"Address",
|
|
"Hosts",
|
|
"Findings",
|
|
"Flags",
|
|
)
|
|
|
|
SERVICE_FLAGS: dict[str, str] = {
|
|
"new": "Not open in an earlier scan of this target",
|
|
"http": "Answered an HTTP request",
|
|
"tls": "Negotiates TLS",
|
|
"sensitive": "An administrative or datastore port",
|
|
"named": "Software identified",
|
|
"passive": "Reported by an internet-wide scanner, not confirmed by this scan",
|
|
"confirmed": "Observed directly by this scan",
|
|
"cdn": "On a CDN-fronted address",
|
|
"hosted": "A hostname resolves to the address",
|
|
"private": "In a private address range",
|
|
"v4": "IPv4 address",
|
|
"v6": "IPv6 address",
|
|
"vulnerable": "A vulnerability scan reported a finding on it",
|
|
"kev": "A known-exploited weakness was found on it",
|
|
}
|
|
|
|
SERVICE_EXPOSURE: dict[str, str] = dict(SERVICE_CLASS_LABELS)
|
|
|
|
SERVICE_FIELDS: tuple[QueryField, ...] = (
|
|
QueryField(
|
|
name="target",
|
|
type=FieldType.STRING,
|
|
group="Scope",
|
|
description="Target this service belongs to.",
|
|
example="target:acme.com",
|
|
aliases=("scope",),
|
|
facet="target",
|
|
),
|
|
QueryField(
|
|
name="port",
|
|
type=FieldType.NUMBER,
|
|
group="Service",
|
|
description="Port number. Accepts a list or a range.",
|
|
example="port:[22,3389]",
|
|
facet="port",
|
|
),
|
|
QueryField(
|
|
name="service",
|
|
type=FieldType.STRING,
|
|
group="Service",
|
|
description="Service running on the port.",
|
|
example="service:ssh",
|
|
facet="service",
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="class",
|
|
type=FieldType.ENUM,
|
|
group="Service",
|
|
description="Service class.",
|
|
example="class:database",
|
|
aliases=("kind",),
|
|
values=tuple(SERVICE_EXPOSURE),
|
|
facet="class",
|
|
),
|
|
QueryField(
|
|
name="protocol",
|
|
type=FieldType.ENUM,
|
|
group="Service",
|
|
description="Transport protocol.",
|
|
example="protocol:tcp",
|
|
values=("tcp", "udp"),
|
|
),
|
|
QueryField(
|
|
name="source",
|
|
type=FieldType.ENUM,
|
|
group="Service",
|
|
description="The source that observed the service.",
|
|
example="source:naabu",
|
|
values=tuple(PORT_SOURCE_LABELS),
|
|
facet="source",
|
|
),
|
|
QueryField(
|
|
name="product",
|
|
type=FieldType.STRING,
|
|
group="Software",
|
|
description="Software named by the service banner.",
|
|
example="product:OpenSSH",
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="version",
|
|
type=FieldType.STRING,
|
|
group="Software",
|
|
description="Version reported by the service.",
|
|
example="version:8.9",
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="banner",
|
|
type=FieldType.TEXT,
|
|
group="Software",
|
|
description="Text the service returned on connect.",
|
|
example="banner:ubuntu",
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name=AI_FIELD,
|
|
type=FieldType.ENUM,
|
|
group="Software",
|
|
description=(
|
|
"AI service identified on the port. Takes a service, a category, yes or no."
|
|
),
|
|
example="ai:ollama",
|
|
aliases=("ai_service",),
|
|
values=AI_QUERY_VALUES,
|
|
),
|
|
QueryField(
|
|
name=AI_MODEL_FIELD,
|
|
type=FieldType.STRING,
|
|
group="Software",
|
|
description="Model an AI service on the port listed. yes or no filters on presence.",
|
|
example="ai.model~llama",
|
|
),
|
|
QueryField(
|
|
name="ip",
|
|
type=FieldType.IP,
|
|
group="Address",
|
|
description="Address the service listens on. Accepts a CIDR range.",
|
|
example="ip:104.16.0.0/12",
|
|
aliases=("address", "addr"),
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="asn",
|
|
type=FieldType.NUMBER,
|
|
group="Address",
|
|
description="Autonomous system announcing the address.",
|
|
example="asn:13335",
|
|
facet="asn",
|
|
),
|
|
QueryField(
|
|
name="org",
|
|
type=FieldType.STRING,
|
|
group="Address",
|
|
description="Operator of the announcing network.",
|
|
example="org:cloudflare",
|
|
aliases=("as_name", "asn_org"),
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="country",
|
|
type=FieldType.ENUM,
|
|
group="Address",
|
|
description="Two-letter country of the address.",
|
|
example="country:DE",
|
|
facet="country",
|
|
),
|
|
QueryField(
|
|
name="cdn",
|
|
type=FieldType.STRING,
|
|
group="Address",
|
|
description="CDN in front of the address. yes or no filters on presence.",
|
|
example="cdn:cloudflare",
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="host",
|
|
type=FieldType.STRING,
|
|
group="Hosts",
|
|
description="Hostname resolving to the address.",
|
|
example="host:api",
|
|
aliases=("name", "subdomain"),
|
|
free_text=True,
|
|
),
|
|
QueryField(
|
|
name="status",
|
|
type=FieldType.NUMBER,
|
|
group="Hosts",
|
|
description="HTTP status returned on this port.",
|
|
example="status:200",
|
|
),
|
|
QueryField(
|
|
name="is",
|
|
type=FieldType.FLAG,
|
|
group="Flags",
|
|
description="Property of the service.",
|
|
example="is:sensitive",
|
|
aliases=("has",),
|
|
values=tuple(SERVICE_FLAGS),
|
|
),
|
|
QueryField(
|
|
name="vuln",
|
|
type=FieldType.ENUM,
|
|
group="Findings",
|
|
description="Severity of a finding recorded on this service.",
|
|
example="vuln:critical",
|
|
aliases=("finding",),
|
|
values=SEVERITY_ORDER,
|
|
),
|
|
QueryField(
|
|
name="cve",
|
|
type=FieldType.STRING,
|
|
group="Findings",
|
|
description="Published vulnerability identifier reported on this service.",
|
|
example="cve:CVE-2021-44228",
|
|
),
|
|
)
|
|
|
|
SERVICE_GROUP_DIMENSIONS: tuple[GroupDimension, ...] = (
|
|
GroupDimension(
|
|
key="target",
|
|
label="Target",
|
|
description="Rows belonging to the same target",
|
|
),
|
|
GroupDimension(
|
|
key="service",
|
|
label="Service",
|
|
description="Ports running the same service",
|
|
),
|
|
GroupDimension(
|
|
key="class",
|
|
label="Service class",
|
|
description="Services of the same class",
|
|
),
|
|
GroupDimension(
|
|
key="port",
|
|
label="Port",
|
|
description="Services on the same port number",
|
|
),
|
|
GroupDimension(
|
|
key="product",
|
|
label="Software",
|
|
description="Services running the same software",
|
|
),
|
|
GroupDimension(
|
|
key="ip",
|
|
label="Address",
|
|
description="Services on the same address",
|
|
),
|
|
GroupDimension(
|
|
key="asn",
|
|
label="Autonomous system",
|
|
description="Services in the same announced network",
|
|
),
|
|
GroupDimension(
|
|
key="org",
|
|
label="Network operator",
|
|
description="Services on the same operator's network",
|
|
),
|
|
GroupDimension(
|
|
key="country",
|
|
label="Country",
|
|
description="Services in the same country",
|
|
),
|
|
GroupDimension(
|
|
key="source",
|
|
label="Evidence",
|
|
description="Services with the same evidence",
|
|
),
|
|
)
|
|
|
|
SERVICE_EXAMPLE_GROUPS: tuple[str, ...] = (
|
|
"Critical exposure",
|
|
"Change",
|
|
"Remote access",
|
|
"Web surface",
|
|
"Evidence",
|
|
"Hygiene",
|
|
)
|
|
|
|
SERVICE_EXAMPLES: tuple[QueryExample, ...] = (
|
|
QueryExample(
|
|
query="class:database",
|
|
description="Data stores reachable from the internet",
|
|
group="Critical exposure",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="service:[redis,memcached,mongodb,elasticsearch]",
|
|
description="Data stores with no authentication by default",
|
|
group="Critical exposure",
|
|
),
|
|
QueryExample(
|
|
query="service:docker",
|
|
description="Container runtime API exposed",
|
|
group="Critical exposure",
|
|
),
|
|
QueryExample(
|
|
query="is:sensitive and not is:cdn",
|
|
description="Administrative ports on an origin address",
|
|
group="Critical exposure",
|
|
),
|
|
QueryExample(
|
|
query="is:new",
|
|
description="Ports that were closed at the previous scan",
|
|
group="Change",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:new and is:sensitive",
|
|
description="Administrative ports opened since the previous scan",
|
|
group="Change",
|
|
),
|
|
QueryExample(
|
|
query="class:remote",
|
|
description="Interactive administration reachable",
|
|
group="Remote access",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="service:ssh and not port:22",
|
|
description="SSH on a non-standard port",
|
|
group="Remote access",
|
|
),
|
|
QueryExample(
|
|
query="service:[telnet,ftp,rlogin,rexec]",
|
|
description="Protocols that carry credentials in the clear",
|
|
group="Remote access",
|
|
),
|
|
QueryExample(
|
|
query="service:rdp or service:vnc",
|
|
description="Remote desktop reachable from the internet",
|
|
group="Remote access",
|
|
),
|
|
QueryExample(
|
|
query="is:http and not port:[80,443]",
|
|
description="Web services on a non-standard port",
|
|
group="Web surface",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:http and not is:tls",
|
|
description="Web services without TLS",
|
|
group="Web surface",
|
|
),
|
|
QueryExample(
|
|
query="class:web and not is:http",
|
|
description="Web ports with no HTTP response",
|
|
group="Web surface",
|
|
),
|
|
QueryExample(
|
|
query="status:401 or status:403",
|
|
description="Services requiring authentication",
|
|
group="Web surface",
|
|
),
|
|
QueryExample(
|
|
query="is:passive",
|
|
description="Ports reported by external scanners, not confirmed",
|
|
group="Evidence",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:named",
|
|
description="Services with identified software",
|
|
group="Evidence",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:confirmed and not is:named",
|
|
description="Open ports with no service identified",
|
|
group="Evidence",
|
|
),
|
|
QueryExample(
|
|
query="class:mail and not is:tls",
|
|
description="Mail services without transport encryption",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="not is:hosted",
|
|
description="Services on addresses with no known hostname",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="class:infra",
|
|
description="Directory, file and management services",
|
|
group="Hygiene",
|
|
generic=True,
|
|
),
|
|
)
|
|
|
|
SERVICE_QUERY = QueryRegistry(
|
|
key="service",
|
|
noun=SURFACE_NOUN[SurfaceDimension.SERVICES.value][0],
|
|
noun_plural=SURFACE_NOUN[SurfaceDimension.SERVICES.value][1],
|
|
fields=SERVICE_FIELDS,
|
|
flags=SERVICE_FLAGS,
|
|
groups=SERVICE_GROUPS,
|
|
dimensions=SERVICE_GROUP_DIMENSIONS,
|
|
examples=SERVICE_EXAMPLES,
|
|
example_groups=SERVICE_EXAMPLE_GROUPS,
|
|
)
|
|
|
|
|
|
VULN_GROUPS: tuple[str, ...] = (
|
|
"Scope",
|
|
"Finding",
|
|
"Classification",
|
|
"Asset",
|
|
"Review",
|
|
"Flags",
|
|
)
|
|
|
|
VULN_FLAGS: dict[str, str] = {
|
|
"new": "Not reported by an earlier scan of this target",
|
|
"kev": "Listed in CISA KEV",
|
|
"ransomware": "Listed in CISA ransomware campaigns",
|
|
"overdue": "Past the CISA remediation deadline",
|
|
"weaponised": "A public exploit is published",
|
|
"untestable": "No scanner template covers this CVE",
|
|
"cve": "Carries a published vulnerability identifier",
|
|
"exploitable": "Known exploited, or above the EPSS threshold",
|
|
"corroborated": "A second signal at the same location names the same CVE or weakness class",
|
|
"proven": "The asset produced an out-of-band interaction",
|
|
"recorded": "The request and response that produced it were stored",
|
|
"extracted": "The check pulled a value out of the response",
|
|
"web": "Found on an HTTP asset",
|
|
"cdn": "On an asset served through a CDN",
|
|
"triaged": "Reviewed",
|
|
"open": "Not reviewed",
|
|
"suppressed": "Reviewed and set aside as a false positive or accepted risk",
|
|
}
|
|
|
|
VULN_FIELDS: tuple[QueryField, ...] = (
|
|
QueryField(
|
|
name="target",
|
|
type=FieldType.STRING,
|
|
group="Scope",
|
|
description="Target this finding belongs to.",
|
|
example="target:acme.com",
|
|
aliases=("scope",),
|
|
facet="target",
|
|
),
|
|
QueryField(
|
|
name="name",
|
|
type=FieldType.STRING,
|
|
group="Finding",
|
|
description="Title of the check that fired.",
|
|
example="name:log4j",
|
|
aliases=("title",),
|
|
free_text=True,
|
|
evidence="name",
|
|
),
|
|
QueryField(
|
|
name="template",
|
|
type=FieldType.STRING,
|
|
group="Finding",
|
|
description="Identifier of the check that fired.",
|
|
example="template:CVE-2021-44228",
|
|
aliases=("check", "rule", "id"),
|
|
facet="template",
|
|
free_text=True,
|
|
evidence="template",
|
|
),
|
|
QueryField(
|
|
name="severity",
|
|
type=FieldType.ENUM,
|
|
group="Finding",
|
|
description="Severity the check declares.",
|
|
example="severity:[critical,high]",
|
|
aliases=("sev",),
|
|
values=SEVERITY_ORDER,
|
|
facet="severity",
|
|
),
|
|
QueryField(
|
|
name="type",
|
|
type=FieldType.ENUM,
|
|
group="Finding",
|
|
description="Protocol the check speaks.",
|
|
example="type:ssl",
|
|
aliases=("protocol",),
|
|
values=PROTOCOLS,
|
|
facet="protocol",
|
|
),
|
|
QueryField(
|
|
name="scanner",
|
|
type=FieldType.ENUM,
|
|
group="Finding",
|
|
description="Tool that produced the finding.",
|
|
example="scanner:nuclei",
|
|
values=tuple(SCANNER_LABELS),
|
|
facet="scanner",
|
|
),
|
|
QueryField(
|
|
name="tag",
|
|
type=FieldType.STRING,
|
|
group="Finding",
|
|
description="Tag carried by the check.",
|
|
example="tag:takeover",
|
|
aliases=("tags",),
|
|
facet="tag",
|
|
),
|
|
QueryField(
|
|
name="matcher",
|
|
type=FieldType.STRING,
|
|
group="Finding",
|
|
description="Named matcher inside the check that fired.",
|
|
example="matcher:unauthenticated",
|
|
),
|
|
QueryField(
|
|
name="extracted",
|
|
type=FieldType.STRING,
|
|
group="Finding",
|
|
description="Value the check pulled out of the response.",
|
|
example="extracted:admin",
|
|
free_text=True,
|
|
evidence="extracted",
|
|
),
|
|
QueryField(
|
|
name="author",
|
|
type=FieldType.STRING,
|
|
group="Finding",
|
|
description="Author of the check.",
|
|
example="author:pdteam",
|
|
),
|
|
QueryField(
|
|
name="exploit",
|
|
type=FieldType.NUMBER,
|
|
group="Classification",
|
|
description="Exploitation rank, 0-100, from the signals behind the finding.",
|
|
example="exploit:>=80",
|
|
aliases=("rank",),
|
|
),
|
|
QueryField(
|
|
name="poc",
|
|
type=FieldType.NUMBER,
|
|
group="Classification",
|
|
description="Number of published proof-of-concept exploits.",
|
|
example="poc:>0",
|
|
aliases=("exploits",),
|
|
),
|
|
QueryField(
|
|
name="signal",
|
|
type=FieldType.ENUM,
|
|
group="Classification",
|
|
description="Exploitation signal carried by the finding.",
|
|
example="signal:ransom_path",
|
|
values=SIGNAL_ORDER,
|
|
facet="signal",
|
|
),
|
|
QueryField(
|
|
name="cve",
|
|
type=FieldType.STRING,
|
|
group="Classification",
|
|
description="Published vulnerability identifier.",
|
|
example="cve:CVE-2021-44228",
|
|
facet="cve",
|
|
free_text=True,
|
|
evidence="cve",
|
|
),
|
|
QueryField(
|
|
name="cwe",
|
|
type=FieldType.STRING,
|
|
group="Classification",
|
|
description="Weakness class.",
|
|
example="cwe:CWE-89",
|
|
),
|
|
QueryField(
|
|
name="cvss",
|
|
type=FieldType.NUMBER,
|
|
group="Classification",
|
|
description="CVSS base score.",
|
|
example="cvss>=9",
|
|
),
|
|
QueryField(
|
|
name="epss",
|
|
type=FieldType.NUMBER,
|
|
group="Classification",
|
|
description="Probability of exploitation in the next 30 days, 0 to 1.",
|
|
example="epss>=0.5",
|
|
),
|
|
QueryField(
|
|
name="host",
|
|
type=FieldType.STRING,
|
|
group="Asset",
|
|
description="Hostname the finding sits on.",
|
|
example="host:api",
|
|
aliases=("name.host", "subdomain"),
|
|
facet="host",
|
|
free_text=True,
|
|
evidence="host",
|
|
),
|
|
QueryField(
|
|
name="location",
|
|
type=FieldType.STRING,
|
|
group="Asset",
|
|
description="Exact place the check matched.",
|
|
example="location:/actuator",
|
|
aliases=("matched", "url", "path"),
|
|
free_text=True,
|
|
evidence="location",
|
|
),
|
|
QueryField(
|
|
name="ip",
|
|
type=FieldType.IP,
|
|
group="Asset",
|
|
description="Address behind the finding. Accepts a CIDR range.",
|
|
example="ip:104.16.0.0/12",
|
|
aliases=("address", "addr"),
|
|
),
|
|
QueryField(
|
|
name="port",
|
|
type=FieldType.NUMBER,
|
|
group="Asset",
|
|
description="Port the finding is on.",
|
|
example="port:8080",
|
|
),
|
|
QueryField(
|
|
name="status",
|
|
type=FieldType.NUMBER,
|
|
group="Asset",
|
|
description="HTTP status the asset returned to this scan.",
|
|
example="status:200",
|
|
),
|
|
QueryField(
|
|
name="tech",
|
|
type=FieldType.STRING,
|
|
group="Asset",
|
|
description="Technology fingerprinted on the asset.",
|
|
example="tech:nginx",
|
|
),
|
|
QueryField(
|
|
name="asn",
|
|
type=FieldType.NUMBER,
|
|
group="Asset",
|
|
description="Autonomous system announcing the address.",
|
|
example="asn:13335",
|
|
),
|
|
QueryField(
|
|
name="country",
|
|
type=FieldType.ENUM,
|
|
group="Asset",
|
|
description="Two-letter country of the address.",
|
|
example="country:DE",
|
|
),
|
|
QueryField(
|
|
name="state",
|
|
type=FieldType.ENUM,
|
|
group="Review",
|
|
description="Review decision recorded for this finding.",
|
|
example="state:false_positive",
|
|
values=VULN_STATES,
|
|
facet="state",
|
|
),
|
|
QueryField(
|
|
name="evidence",
|
|
type=FieldType.ENUM,
|
|
group="Finding",
|
|
description="How the finding was established: observed, corroborated or proven.",
|
|
example="evidence:corroborated",
|
|
values=tuple(e for e in EVIDENCE_ORDER if e != Evidence.INFERRED.value),
|
|
facet="evidence",
|
|
),
|
|
QueryField(
|
|
name="seen",
|
|
type=FieldType.DATE,
|
|
group="Review",
|
|
description="When this scan recorded the finding.",
|
|
example="seen:<24h",
|
|
aliases=("discovered", "age"),
|
|
),
|
|
QueryField(
|
|
name="is",
|
|
type=FieldType.FLAG,
|
|
group="Flags",
|
|
description="Property of the finding.",
|
|
example="is:kev",
|
|
aliases=("has",),
|
|
values=tuple(VULN_FLAGS),
|
|
),
|
|
)
|
|
|
|
VULN_GROUP_DIMENSIONS: tuple[GroupDimension, ...] = (
|
|
GroupDimension(
|
|
key="severity",
|
|
label="Severity",
|
|
description="Findings of the same severity",
|
|
),
|
|
GroupDimension(
|
|
key="template",
|
|
label="Check",
|
|
description="The same weakness across every asset it was found on",
|
|
),
|
|
GroupDimension(
|
|
key="tag",
|
|
label="Category",
|
|
description="Findings the checks categorise the same way",
|
|
),
|
|
GroupDimension(
|
|
key="cve",
|
|
label="CVE",
|
|
description="Findings sharing a published identifier",
|
|
),
|
|
GroupDimension(
|
|
key="evidence",
|
|
label="Evidence",
|
|
description="Findings with the same evidence",
|
|
),
|
|
GroupDimension(
|
|
key="state",
|
|
label="Review state",
|
|
description="Findings at the same point in review",
|
|
),
|
|
GroupDimension(
|
|
key="scanner",
|
|
label="Scanner",
|
|
description="Findings from the same tool",
|
|
),
|
|
GroupDimension(
|
|
key="type",
|
|
label="Protocol",
|
|
description="Findings on the same protocol",
|
|
),
|
|
GroupDimension(
|
|
key="host",
|
|
label="Web asset",
|
|
description="Everything found on one hostname",
|
|
),
|
|
GroupDimension(
|
|
key="location",
|
|
label="Location",
|
|
description="Every check that fired at the same place",
|
|
),
|
|
GroupDimension(
|
|
key="port",
|
|
label="Port",
|
|
description="Findings on the same port",
|
|
),
|
|
GroupDimension(
|
|
key="target",
|
|
label="Target",
|
|
description="Rows belonging to the same target",
|
|
),
|
|
)
|
|
|
|
VULN_EXAMPLE_GROUPS: tuple[str, ...] = (
|
|
"Priority",
|
|
"Change",
|
|
"Access control",
|
|
"Data exposure",
|
|
"Infrastructure",
|
|
"Review",
|
|
)
|
|
|
|
VULN_EXAMPLES: tuple[QueryExample, ...] = (
|
|
QueryExample(
|
|
query="is:kev",
|
|
description="Weaknesses with confirmed exploitation in the wild",
|
|
group="Priority",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="severity:critical",
|
|
description="Findings rated critical",
|
|
group="Priority",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:corroborated",
|
|
description="Findings a second check confirms at the same location",
|
|
group="Priority",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:corroborated and severity:[critical,high]",
|
|
description="Severe findings that more than one check agrees on",
|
|
group="Priority",
|
|
),
|
|
QueryExample(
|
|
query="evidence:proven",
|
|
description="Findings the asset answered with an out-of-band callback",
|
|
group="Priority",
|
|
),
|
|
QueryExample(
|
|
query="is:exploitable and not is:suppressed",
|
|
description="Exploited or likely to be, and not suppressed",
|
|
group="Priority",
|
|
),
|
|
QueryExample(
|
|
query="severity:[critical,high] and not is:cdn",
|
|
description="Severe findings on origin infrastructure",
|
|
group="Priority",
|
|
),
|
|
QueryExample(
|
|
query="cvss>=9",
|
|
description="Findings scored 9.0 or above",
|
|
group="Priority",
|
|
),
|
|
QueryExample(
|
|
query="is:new",
|
|
description="Findings absent from the previous scan of this target",
|
|
group="Change",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:new and severity:[critical,high]",
|
|
description="Severe findings that appeared since the previous scan",
|
|
group="Change",
|
|
),
|
|
QueryExample(
|
|
query="seen:<24h and is:cve",
|
|
description="Published vulnerabilities recorded in the last day",
|
|
group="Change",
|
|
),
|
|
QueryExample(
|
|
query="tag:default-login",
|
|
description="Default credentials accepted",
|
|
group="Access control",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="tag:panel and severity:[critical,high,medium]",
|
|
description="Administrative interfaces reachable from the internet",
|
|
group="Access control",
|
|
),
|
|
QueryExample(
|
|
query="tag:[unauth,auth-bypass]",
|
|
description="Protected functionality reachable without credentials",
|
|
group="Access control",
|
|
),
|
|
QueryExample(
|
|
query="tag:[exposure,disclosure]",
|
|
description="Files and data disclosed over HTTP",
|
|
group="Data exposure",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="tag:[backup,config,files]",
|
|
description="Source, configuration and backups reachable over HTTP",
|
|
group="Data exposure",
|
|
),
|
|
QueryExample(
|
|
query="is:extracted",
|
|
description="Findings that pulled a concrete value out of the response",
|
|
group="Data exposure",
|
|
),
|
|
QueryExample(
|
|
query="tag:takeover",
|
|
description="Hostnames pointing at claimable third-party infrastructure",
|
|
group="Infrastructure",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="type:ssl",
|
|
description="Transport security defects",
|
|
group="Infrastructure",
|
|
),
|
|
QueryExample(
|
|
query="type:[network,dns]",
|
|
description="Findings outside the web surface",
|
|
group="Infrastructure",
|
|
),
|
|
QueryExample(
|
|
query="tag:misconfig",
|
|
description="Misconfigured services",
|
|
group="Infrastructure",
|
|
),
|
|
QueryExample(
|
|
query="is:open and severity:[critical,high]",
|
|
description="Severe findings not reviewed",
|
|
group="Review",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="state:false_positive",
|
|
description="Findings a reviewer rejected",
|
|
group="Review",
|
|
),
|
|
QueryExample(
|
|
query="is:triaged",
|
|
description="Findings with a review decision",
|
|
group="Review",
|
|
),
|
|
)
|
|
|
|
VULN_QUERY = QueryRegistry(
|
|
key="vulnerability",
|
|
noun=SURFACE_NOUN[SurfaceDimension.VULNERABILITIES.value][0],
|
|
noun_plural=SURFACE_NOUN[SurfaceDimension.VULNERABILITIES.value][1],
|
|
fields=VULN_FIELDS,
|
|
flags=VULN_FLAGS,
|
|
groups=VULN_GROUPS,
|
|
dimensions=VULN_GROUP_DIMENSIONS,
|
|
examples=VULN_EXAMPLES,
|
|
example_groups=VULN_EXAMPLE_GROUPS,
|
|
)
|
|
|
|
|
|
ENDPOINT_GROUPS: tuple[str, ...] = (
|
|
"Scope",
|
|
"Location",
|
|
"Parameters",
|
|
"Response",
|
|
"Evidence",
|
|
"Findings",
|
|
"Flags",
|
|
)
|
|
|
|
ENDPOINT_FLAGS: dict[str, str] = {
|
|
"new": "Absent from the previous scan of this target",
|
|
"param": "Accepts at least one query parameter",
|
|
"probed": "Requested by this scan",
|
|
"live": "Answered with 2xx or 3xx",
|
|
"redirect": "Answered with a redirect",
|
|
"auth": "Answered 401 or 403",
|
|
"missing": "Answered 404 or 410",
|
|
"api": "An API route, by path shape or content type",
|
|
"js": "JavaScript, including bundles and source maps",
|
|
"static": "Static content",
|
|
"interesting": "Matched an interest pattern in the path or a parameter",
|
|
"sensitive": "A credential, backup or version control path",
|
|
"orphan": "Not linked from any crawled page",
|
|
"archive-only": "Recorded by an archive and unreachable in this scan",
|
|
"linked": "Reached from a page on the live site",
|
|
"crawled": "Reached by the crawler",
|
|
"root": "The site root",
|
|
"titled": "Has a page title",
|
|
"vulnerable": "A vulnerability scan reported a finding on it",
|
|
"kev": "A known-exploited weakness was found on it",
|
|
}
|
|
|
|
ENDPOINT_FIELDS: tuple[QueryField, ...] = (
|
|
QueryField(
|
|
name="target",
|
|
type=FieldType.STRING,
|
|
group="Scope",
|
|
description="Target this endpoint belongs to.",
|
|
example="target:acme.com",
|
|
aliases=("scope",),
|
|
facet="target",
|
|
),
|
|
QueryField(
|
|
name="url",
|
|
type=FieldType.STRING,
|
|
group="Location",
|
|
description="Full URL.",
|
|
example="url:/wp-admin",
|
|
free_text=True,
|
|
evidence="URL",
|
|
),
|
|
QueryField(
|
|
name="path",
|
|
type=FieldType.STRING,
|
|
group="Location",
|
|
description="Path, without the host or the query string.",
|
|
example="path:/api/v1/users",
|
|
free_text=True,
|
|
evidence="Path",
|
|
),
|
|
QueryField(
|
|
name="dir",
|
|
type=FieldType.STRING,
|
|
group="Location",
|
|
description="Directory the endpoint sits in. dir: matches the whole branch, dir= only that folder.",
|
|
example='dir:"/admin/"',
|
|
aliases=("directory", "folder"),
|
|
facet="dir",
|
|
),
|
|
QueryField(
|
|
name="file",
|
|
type=FieldType.STRING,
|
|
group="Location",
|
|
description="File name, without the directory.",
|
|
example="file:config.json",
|
|
aliases=("filename",),
|
|
),
|
|
QueryField(
|
|
name="ext",
|
|
type=FieldType.STRING,
|
|
group="Location",
|
|
description="File extension.",
|
|
example="ext:[bak,old,sql]",
|
|
aliases=("extension",),
|
|
facet="ext",
|
|
),
|
|
QueryField(
|
|
name="host",
|
|
type=FieldType.STRING,
|
|
group="Location",
|
|
description="Hostname the endpoint is on.",
|
|
example="host:api.example.com",
|
|
facet="host",
|
|
free_text=True,
|
|
evidence="Host",
|
|
),
|
|
QueryField(
|
|
name="port",
|
|
type=FieldType.NUMBER,
|
|
group="Location",
|
|
description="Port the endpoint is served on.",
|
|
example="port:8443",
|
|
),
|
|
QueryField(
|
|
name="scheme",
|
|
type=FieldType.ENUM,
|
|
group="Location",
|
|
description="URL scheme.",
|
|
example="scheme:http",
|
|
values=("http", "https"),
|
|
),
|
|
QueryField(
|
|
name="depth",
|
|
type=FieldType.NUMBER,
|
|
group="Location",
|
|
description="Directory depth of the endpoint.",
|
|
example="depth:>3",
|
|
),
|
|
QueryField(
|
|
name="class",
|
|
type=FieldType.ENUM,
|
|
group="Location",
|
|
description="The kind of content the endpoint serves.",
|
|
example="class:api",
|
|
aliases=("kind",),
|
|
values=ENDPOINT_CLASSES,
|
|
facet="class",
|
|
),
|
|
QueryField(
|
|
name="param",
|
|
type=FieldType.STRING,
|
|
group="Parameters",
|
|
description="Name of a query parameter the endpoint accepts.",
|
|
example="param:redirect",
|
|
aliases=("parameter",),
|
|
facet="param",
|
|
),
|
|
QueryField(
|
|
name="params",
|
|
type=FieldType.NUMBER,
|
|
group="Parameters",
|
|
description="Number of parameters the endpoint accepts.",
|
|
example="params:>2",
|
|
),
|
|
QueryField(
|
|
name="interest",
|
|
type=FieldType.ENUM,
|
|
group="Parameters",
|
|
description="Interest pattern matched by path or parameter shape.",
|
|
example="interest:open_redirect",
|
|
values=INTEREST_KEYS,
|
|
facet="interest",
|
|
),
|
|
QueryField(
|
|
name="status",
|
|
type=FieldType.NUMBER,
|
|
group="Response",
|
|
description="HTTP status the endpoint answered with. Empty until probed.",
|
|
example="status:200..299",
|
|
facet="status",
|
|
),
|
|
QueryField(
|
|
name="type",
|
|
type=FieldType.STRING,
|
|
group="Response",
|
|
description="Content type of the response.",
|
|
example="type:json",
|
|
aliases=("content_type", "mime"),
|
|
),
|
|
QueryField(
|
|
name="title",
|
|
type=FieldType.STRING,
|
|
group="Response",
|
|
description="Page title.",
|
|
example='title:"index of"',
|
|
free_text=True,
|
|
evidence="Title",
|
|
),
|
|
QueryField(
|
|
name="length",
|
|
type=FieldType.BYTES,
|
|
group="Response",
|
|
description="Response body size.",
|
|
example="length:>100000",
|
|
aliases=("size",),
|
|
unit="bytes",
|
|
),
|
|
QueryField(
|
|
name="words",
|
|
type=FieldType.NUMBER,
|
|
group="Response",
|
|
description="Word count of the response body.",
|
|
example="words:<10",
|
|
),
|
|
QueryField(
|
|
name="tech",
|
|
type=FieldType.STRING,
|
|
group="Response",
|
|
description="Technology fingerprinted on the response.",
|
|
example="tech:django",
|
|
facet="tech",
|
|
),
|
|
QueryField(
|
|
name="method",
|
|
type=FieldType.STRING,
|
|
group="Response",
|
|
description="HTTP method the endpoint was seen with.",
|
|
example="method:POST",
|
|
),
|
|
QueryField(
|
|
name="source",
|
|
type=FieldType.ENUM,
|
|
group="Evidence",
|
|
description="The source that discovered the endpoint. An endpoint may have several.",
|
|
example="source:crawl",
|
|
values=tuple(SOURCE_LABELS),
|
|
facet="source",
|
|
),
|
|
QueryField(
|
|
name="from",
|
|
type=FieldType.STRING,
|
|
group="Evidence",
|
|
description="The URL that linked to this one.",
|
|
example="from:/sitemap.xml",
|
|
aliases=("found_on", "referrer"),
|
|
),
|
|
QueryField(
|
|
name="seen",
|
|
type=FieldType.DATE,
|
|
group="Evidence",
|
|
description="When this scan recorded the endpoint.",
|
|
example="seen:<7d",
|
|
),
|
|
QueryField(
|
|
name="vuln",
|
|
type=FieldType.ENUM,
|
|
group="Findings",
|
|
description="Severity of a vulnerability reported at this location.",
|
|
example="vuln:critical",
|
|
values=SEVERITY_ORDER,
|
|
),
|
|
QueryField(
|
|
name="cve",
|
|
type=FieldType.STRING,
|
|
group="Findings",
|
|
description="CVE reported at this location.",
|
|
example="cve:CVE-2024",
|
|
),
|
|
QueryField(
|
|
name="is",
|
|
type=FieldType.FLAG,
|
|
group="Flags",
|
|
description="Property of the endpoint.",
|
|
example="is:orphan",
|
|
values=tuple(ENDPOINT_FLAGS),
|
|
),
|
|
)
|
|
|
|
ENDPOINT_GROUP_DIMENSIONS: tuple[GroupDimension, ...] = (
|
|
GroupDimension(
|
|
key="target",
|
|
label="Target",
|
|
description="Rows belonging to the same target",
|
|
),
|
|
GroupDimension(
|
|
key="dir",
|
|
label="Directory",
|
|
description="Endpoints in the same directory",
|
|
),
|
|
GroupDimension(
|
|
key="host",
|
|
label="Host",
|
|
description="Endpoints on the same host",
|
|
),
|
|
GroupDimension(
|
|
key="class",
|
|
label="Kind",
|
|
description="Endpoints of the same kind",
|
|
),
|
|
GroupDimension(
|
|
key="ext",
|
|
label="Extension",
|
|
description="Endpoints with the same file extension",
|
|
),
|
|
GroupDimension(
|
|
key="status",
|
|
label="Status",
|
|
description="Endpoints answering with the same status",
|
|
),
|
|
GroupDimension(
|
|
key="source",
|
|
label="Source",
|
|
description="Endpoints found the same way",
|
|
),
|
|
GroupDimension(
|
|
key="param",
|
|
label="Parameter",
|
|
description="Endpoints accepting the same parameter",
|
|
),
|
|
GroupDimension(
|
|
key="interest",
|
|
label="Interest",
|
|
description="Endpoints flagged for the same reason",
|
|
),
|
|
GroupDimension(
|
|
key="tech",
|
|
label="Technology",
|
|
description="Endpoints running the same technology",
|
|
),
|
|
)
|
|
|
|
ENDPOINT_EXAMPLE_GROUPS: tuple[str, ...] = (
|
|
"Attack surface",
|
|
"Hidden surface",
|
|
"Change",
|
|
"API",
|
|
"Evidence",
|
|
"Hygiene",
|
|
)
|
|
|
|
ENDPOINT_EXAMPLES: tuple[QueryExample, ...] = (
|
|
QueryExample(
|
|
query="is:param and is:live",
|
|
description="Live endpoints that accept input",
|
|
group="Attack surface",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="interest:open_redirect or interest:ssrf",
|
|
description="Parameters carrying a destination URL",
|
|
group="Attack surface",
|
|
),
|
|
QueryExample(
|
|
query="interest:traversal and is:live",
|
|
description="Live endpoints taking a file or path parameter",
|
|
group="Attack surface",
|
|
),
|
|
QueryExample(
|
|
query="is:sensitive",
|
|
description="Credential, backup and version control paths",
|
|
group="Attack surface",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="ext:[bak,old,sql,zip,tar,gz]",
|
|
description="Archive and backup files in the web root",
|
|
group="Attack surface",
|
|
),
|
|
QueryExample(
|
|
query="interest:admin and status:200",
|
|
description="Administrative paths answering without authentication",
|
|
group="Attack surface",
|
|
),
|
|
QueryExample(
|
|
query="is:orphan",
|
|
description="Endpoints not linked from any crawled page",
|
|
group="Hidden surface",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:archive-only",
|
|
description="Recorded by an archive and unreachable in this scan",
|
|
group="Hidden surface",
|
|
),
|
|
QueryExample(
|
|
query="source:archive and is:live",
|
|
description="Archived endpoints that respond",
|
|
group="Hidden surface",
|
|
),
|
|
QueryExample(
|
|
query="depth:>4 and is:live",
|
|
description="Deeply nested live endpoints",
|
|
group="Hidden surface",
|
|
),
|
|
QueryExample(
|
|
query="is:new",
|
|
description="Endpoints absent from the previous scan",
|
|
group="Change",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:new and is:param",
|
|
description="New input surface since the last scan",
|
|
group="Change",
|
|
),
|
|
QueryExample(
|
|
query="class:api",
|
|
description="API routes",
|
|
group="API",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="interest:api_doc",
|
|
description="Swagger, OpenAPI and GraphQL descriptions of the API",
|
|
group="API",
|
|
),
|
|
QueryExample(
|
|
query="class:api and is:param and not is:auth",
|
|
description="API routes taking input with no auth wall",
|
|
group="API",
|
|
),
|
|
QueryExample(
|
|
query="not is:probed",
|
|
description="Endpoints not requested by this scan",
|
|
group="Evidence",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="source:response_mining",
|
|
description="Found in stored responses",
|
|
group="Evidence",
|
|
),
|
|
QueryExample(
|
|
query="source:robots or source:sitemap",
|
|
description="Paths declared in robots.txt or a sitemap",
|
|
group="Evidence",
|
|
),
|
|
QueryExample(
|
|
query="is:vulnerable",
|
|
description="Endpoints a vulnerability scan reported a finding on",
|
|
group="Evidence",
|
|
),
|
|
QueryExample(
|
|
query="interest:debug_endpoint",
|
|
description="Actuator, metrics and diagnostic routes",
|
|
group="Hygiene",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="status:500..599",
|
|
description="Endpoints answering with a server error",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="is:redirect and not is:live",
|
|
description="Redirects ending in an error",
|
|
group="Hygiene",
|
|
),
|
|
QueryExample(
|
|
query="type:json and not class:api",
|
|
description="JSON served outside the API surface",
|
|
group="Hygiene",
|
|
),
|
|
)
|
|
|
|
ENDPOINT_QUERY = QueryRegistry(
|
|
key="endpoint",
|
|
noun=SURFACE_NOUN[SurfaceDimension.ENDPOINTS.value][0],
|
|
noun_plural=SURFACE_NOUN[SurfaceDimension.ENDPOINTS.value][1],
|
|
fields=ENDPOINT_FIELDS,
|
|
flags=ENDPOINT_FLAGS,
|
|
groups=ENDPOINT_GROUPS,
|
|
dimensions=ENDPOINT_GROUP_DIMENSIONS,
|
|
examples=ENDPOINT_EXAMPLES,
|
|
example_groups=ENDPOINT_EXAMPLE_GROUPS,
|
|
)
|
|
|
|
|
|
# ---------- software ----------
|
|
|
|
VERSION_SOURCE_KEYS: tuple[str, ...] = tuple(VERSION_SOURCE_LABELS)
|
|
|
|
SOFTWARE_GROUPS: tuple[str, ...] = (
|
|
"Software",
|
|
"Vulnerability",
|
|
"Confidence",
|
|
"Asset",
|
|
"Flags",
|
|
)
|
|
|
|
SOFTWARE_FLAGS: dict[str, str] = {
|
|
"new": "Not inferred by an earlier scan of this target",
|
|
"kev": "Listed in CISA KEV",
|
|
"ransomware": "Listed in CISA ransomware campaigns",
|
|
"overdue": "Past the CISA remediation deadline",
|
|
"likely": "EPSS score of 0.088 or above",
|
|
"firm": "Every part of the match is verified",
|
|
"conditional": "NVD names a further component this scan did not identify",
|
|
"backport": "A distribution build. Fixes may land without a version change",
|
|
"fingerprinted": "Version read from the page body",
|
|
"stated": "Version stated in a server header",
|
|
"web": "Inferred from a web asset",
|
|
"service": "Inferred from a service banner",
|
|
"corroborated": "A check at the same host names the same CVE",
|
|
"fixable": "A newer release in this scan is not affected",
|
|
}
|
|
|
|
SOFTWARE_FIELDS: tuple[QueryField, ...] = (
|
|
QueryField(
|
|
name="cve",
|
|
type=FieldType.STRING,
|
|
group="Vulnerability",
|
|
description="Published identifier of the weakness.",
|
|
example="cve:CVE-2021-23017",
|
|
free_text=True,
|
|
evidence="cve",
|
|
),
|
|
QueryField(
|
|
name="software",
|
|
type=FieldType.STRING,
|
|
group="Software",
|
|
description="Name of the software as the scan read it.",
|
|
example="software:nginx",
|
|
aliases=("name",),
|
|
free_text=True,
|
|
evidence="software",
|
|
),
|
|
QueryField(
|
|
name="product",
|
|
type=FieldType.STRING,
|
|
group="Software",
|
|
description="Product as NVD files it.",
|
|
example="product:http_server",
|
|
),
|
|
QueryField(
|
|
name="vendor",
|
|
type=FieldType.STRING,
|
|
group="Software",
|
|
description="Vendor as NVD files it.",
|
|
example="vendor:apache",
|
|
),
|
|
QueryField(
|
|
name="version",
|
|
type=FieldType.STRING,
|
|
group="Software",
|
|
description="Version the asset reported.",
|
|
example="version:2.4.52",
|
|
),
|
|
QueryField(
|
|
name="source",
|
|
type=FieldType.ENUM,
|
|
group="Software",
|
|
description="Source of the reported version.",
|
|
example="source:banner",
|
|
values=tuple(VERSION_SOURCE_KEYS),
|
|
facet="source",
|
|
),
|
|
QueryField(
|
|
name="severity",
|
|
type=FieldType.ENUM,
|
|
group="Vulnerability",
|
|
description="Severity NVD scored the CVE at.",
|
|
example="severity:critical",
|
|
values=SEVERITY_ORDER,
|
|
facet="severity",
|
|
),
|
|
QueryField(
|
|
name="cvss",
|
|
type=FieldType.NUMBER,
|
|
group="Vulnerability",
|
|
description="CVSS base score.",
|
|
example="cvss>=9",
|
|
),
|
|
QueryField(
|
|
name="epss",
|
|
type=FieldType.NUMBER,
|
|
group="Vulnerability",
|
|
description="Probability of exploitation in the next 30 days.",
|
|
example="epss>=0.5",
|
|
),
|
|
QueryField(
|
|
name="rank",
|
|
type=FieldType.NUMBER,
|
|
group="Vulnerability",
|
|
description="Exploitation rank built from the intelligence signals.",
|
|
example="rank>=40",
|
|
),
|
|
QueryField(
|
|
name="confidence",
|
|
type=FieldType.ENUM,
|
|
group="Confidence",
|
|
description="Verification level of the match.",
|
|
example="confidence:high",
|
|
values=tuple(CONFIDENCE_KEYS),
|
|
facet="confidence",
|
|
),
|
|
QueryField(
|
|
name="caveat",
|
|
type=FieldType.ENUM,
|
|
group="Confidence",
|
|
description="A part of the match this scan did not verify.",
|
|
example="caveat:backport",
|
|
values=tuple(CAVEAT_KEYS),
|
|
facet="caveat",
|
|
),
|
|
QueryField(
|
|
name="evidence",
|
|
type=FieldType.ENUM,
|
|
group="Confidence",
|
|
description="Inferred from the version alone, or corroborated by a check at the same host.",
|
|
example="evidence:corroborated",
|
|
values=(Evidence.INFERRED.value, Evidence.CORROBORATED.value),
|
|
facet="evidence",
|
|
),
|
|
QueryField(
|
|
name="target",
|
|
type=FieldType.STRING,
|
|
group="Asset",
|
|
description="Target the asset belongs to.",
|
|
example="target:example.com",
|
|
),
|
|
QueryField(
|
|
name="host",
|
|
type=FieldType.STRING,
|
|
group="Asset",
|
|
description="Hostname running the software.",
|
|
example="host:www.example.com",
|
|
free_text=True,
|
|
evidence="host",
|
|
),
|
|
QueryField(
|
|
name="ip",
|
|
type=FieldType.IP,
|
|
group="Asset",
|
|
description="Address running the software.",
|
|
example="ip:203.0.113.10",
|
|
),
|
|
QueryField(
|
|
name="port",
|
|
type=FieldType.NUMBER,
|
|
group="Asset",
|
|
description="Port the software answers on.",
|
|
example="port:443",
|
|
),
|
|
QueryField(
|
|
name="seen",
|
|
type=FieldType.DATE,
|
|
group="Asset",
|
|
description="When this scan inferred it.",
|
|
example="seen:<24h",
|
|
aliases=("discovered", "age"),
|
|
),
|
|
QueryField(
|
|
name="is",
|
|
type=FieldType.FLAG,
|
|
group="Flags",
|
|
description="Property of the match.",
|
|
example="is:kev",
|
|
aliases=("has",),
|
|
values=tuple(SOFTWARE_FLAGS),
|
|
),
|
|
)
|
|
|
|
SOFTWARE_GROUP_DIMENSIONS: tuple[GroupDimension, ...] = (
|
|
GroupDimension(
|
|
key="target", label="Target", description="Rows belonging to the same target"
|
|
),
|
|
GroupDimension(
|
|
key="cve", label="CVE", description="Every asset affected by one published CVE"
|
|
),
|
|
GroupDimension(
|
|
key="product",
|
|
label="Product",
|
|
description="Everything running the same software",
|
|
),
|
|
GroupDimension(
|
|
key="version",
|
|
label="Version",
|
|
description="Everything running the same release",
|
|
),
|
|
GroupDimension(
|
|
key="host", label="Host", description="Everything inferred on one hostname"
|
|
),
|
|
GroupDimension(
|
|
key="severity", label="Severity", description="Matches of the same severity"
|
|
),
|
|
GroupDimension(
|
|
key="confidence",
|
|
label="Confidence",
|
|
description="Matches with the same amount verified",
|
|
),
|
|
GroupDimension(
|
|
key="evidence",
|
|
label="Evidence",
|
|
description="Matches with the same evidence",
|
|
),
|
|
)
|
|
|
|
SOFTWARE_EXAMPLE_GROUPS: tuple[str, ...] = (
|
|
"Priority",
|
|
"Change",
|
|
"Software",
|
|
"Confidence",
|
|
)
|
|
|
|
SOFTWARE_EXAMPLES: tuple[QueryExample, ...] = (
|
|
QueryExample(
|
|
query="is:kev",
|
|
description="Software carrying a CVE with confirmed exploitation",
|
|
group="Priority",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:kev and is:firm",
|
|
description="Confirmed exploited and fully verified",
|
|
group="Priority",
|
|
),
|
|
QueryExample(
|
|
query="severity:critical and is:stated",
|
|
description="Critical CVEs where the server stated its own version",
|
|
group="Priority",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="epss>=0.5",
|
|
description="EPSS probability of 0.5 or above",
|
|
group="Priority",
|
|
),
|
|
QueryExample(
|
|
query="is:kev and is:fixable",
|
|
description="Known exploited CVEs with an unaffected release in this scan",
|
|
group="Priority",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:new",
|
|
description="Matches absent from the previous scan of this target",
|
|
group="Change",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="is:new and severity:[critical,high]",
|
|
description="Severe matches that appeared since the previous scan",
|
|
group="Change",
|
|
),
|
|
QueryExample(
|
|
query="software:nginx",
|
|
description="Everything running nginx",
|
|
group="Software",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="product:http_server and cvss>=9",
|
|
description="Apache builds carrying a critically scored CVE",
|
|
group="Software",
|
|
),
|
|
QueryExample(
|
|
query="software:wordpress and is:new",
|
|
description="WordPress matches absent from the previous scan",
|
|
group="Software",
|
|
),
|
|
QueryExample(
|
|
query="confidence:high",
|
|
description="Matches with no unverified part",
|
|
group="Confidence",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="caveat:backport",
|
|
description="Distribution builds that may patch without a version change",
|
|
group="Confidence",
|
|
),
|
|
QueryExample(
|
|
query="not caveat:conditional",
|
|
description="Matches NVD does not tie to a further component",
|
|
group="Confidence",
|
|
),
|
|
QueryExample(
|
|
query="evidence:corroborated",
|
|
description="Matches that a check at the same host also names",
|
|
group="Confidence",
|
|
),
|
|
)
|
|
|
|
SOFTWARE_QUERY = QueryRegistry(
|
|
key="software",
|
|
noun=SURFACE_NOUN[SurfaceDimension.SOFTWARE.value][0],
|
|
noun_plural=SURFACE_NOUN[SurfaceDimension.SOFTWARE.value][1],
|
|
fields=SOFTWARE_FIELDS,
|
|
flags=SOFTWARE_FLAGS,
|
|
groups=SOFTWARE_GROUPS,
|
|
dimensions=SOFTWARE_GROUP_DIMENSIONS,
|
|
examples=SOFTWARE_EXAMPLES,
|
|
example_groups=SOFTWARE_EXAMPLE_GROUPS,
|
|
)
|
|
|
|
|
|
# ---------- secrets ----------
|
|
|
|
SECRET_GROUPS: tuple[str, ...] = (
|
|
"Secret",
|
|
"State",
|
|
"Asset",
|
|
"Flags",
|
|
)
|
|
|
|
SECRET_FLAGS: dict[str, str] = {
|
|
"new": "Not read by an earlier scan of this target",
|
|
"exposed": "A private value readable in a stored response",
|
|
"public": "A value meant to be in the page",
|
|
"expired": "A token whose expiry is in the past",
|
|
"secret": "A private value, not a public identifier",
|
|
"shared": "Read on more than one host",
|
|
}
|
|
|
|
SECRET_FIELDS: tuple[QueryField, ...] = (
|
|
QueryField(
|
|
name="secret",
|
|
type=FieldType.ENUM,
|
|
group="Secret",
|
|
description="Kind of value the detector matched.",
|
|
example="secret:aws_access_key",
|
|
aliases=("kind",),
|
|
values=SECRET_DETECTOR_KEYS,
|
|
facet="kind",
|
|
),
|
|
QueryField(
|
|
name="group",
|
|
type=FieldType.ENUM,
|
|
group="Secret",
|
|
description="Family the detector belongs to.",
|
|
example="group:cloud",
|
|
values=SECRET_GROUP_KEYS,
|
|
facet="group",
|
|
),
|
|
QueryField(
|
|
name="vendor",
|
|
type=FieldType.STRING,
|
|
group="Secret",
|
|
description="Provider the value belongs to.",
|
|
example="vendor:stripe",
|
|
facet="vendor",
|
|
),
|
|
QueryField(
|
|
name="subject",
|
|
type=FieldType.STRING,
|
|
group="Secret",
|
|
description="Who or what the value names, such as an email domain or DSN host.",
|
|
example="subject:acme.com",
|
|
free_text=True,
|
|
evidence="subject",
|
|
facet="subject",
|
|
),
|
|
QueryField(
|
|
name="fingerprint",
|
|
type=FieldType.STRING,
|
|
group="Secret",
|
|
description="Identity of one value across scans and targets.",
|
|
example="fingerprint=3a9f…",
|
|
),
|
|
QueryField(
|
|
name="state",
|
|
type=FieldType.ENUM,
|
|
group="State",
|
|
description="Whether the value is exposed, public by design or expired.",
|
|
example="state:exposed",
|
|
values=SECRET_STATE_KEYS,
|
|
facet="state",
|
|
),
|
|
QueryField(
|
|
name="source",
|
|
type=FieldType.ENUM,
|
|
group="State",
|
|
description="Where in the response it was read.",
|
|
example="source:body",
|
|
values=SECRET_SOURCE_KEYS,
|
|
facet="source",
|
|
),
|
|
QueryField(
|
|
name="target",
|
|
type=FieldType.STRING,
|
|
group="Asset",
|
|
description="Target the value belongs to.",
|
|
example="target:example.com",
|
|
),
|
|
QueryField(
|
|
name="host",
|
|
type=FieldType.STRING,
|
|
group="Asset",
|
|
description="Hostname the value was read on.",
|
|
example="host:www.example.com",
|
|
free_text=True,
|
|
evidence="host",
|
|
),
|
|
QueryField(
|
|
name="url",
|
|
type=FieldType.STRING,
|
|
group="Asset",
|
|
description="URL the value was read from.",
|
|
example="url:/config.json",
|
|
free_text=True,
|
|
evidence="url",
|
|
),
|
|
QueryField(
|
|
name="seen",
|
|
type=FieldType.DATE,
|
|
group="Asset",
|
|
description="When this scan read it.",
|
|
example="seen:<24h",
|
|
aliases=("discovered", "age"),
|
|
),
|
|
QueryField(
|
|
name="is",
|
|
type=FieldType.FLAG,
|
|
group="Flags",
|
|
description="Property of the value.",
|
|
example="is:exposed",
|
|
aliases=("has",),
|
|
values=tuple(SECRET_FLAGS),
|
|
),
|
|
)
|
|
|
|
SECRET_GROUP_DIMENSIONS: tuple[GroupDimension, ...] = (
|
|
GroupDimension(
|
|
key="value",
|
|
label="Value",
|
|
description="One row per distinct value, however many places carry it",
|
|
),
|
|
GroupDimension(
|
|
key="target", label="Target", description="Values belonging to the same target"
|
|
),
|
|
GroupDimension(key="secret", label="Kind", description="Values of the same kind"),
|
|
GroupDimension(
|
|
key="group", label="Family", description="Values in the same family"
|
|
),
|
|
GroupDimension(
|
|
key="vendor", label="Provider", description="Values from the same provider"
|
|
),
|
|
GroupDimension(key="state", label="State", description="Values in the same state"),
|
|
GroupDimension(key="host", label="Host", description="Values read on one host"),
|
|
GroupDimension(
|
|
key="subject",
|
|
label="Subject",
|
|
description="Values naming the same subject",
|
|
),
|
|
)
|
|
|
|
SECRET_EXAMPLE_GROUPS: tuple[str, ...] = (
|
|
"Priority",
|
|
"Change",
|
|
"Kind",
|
|
"Contacts",
|
|
)
|
|
|
|
SECRET_EXAMPLES: tuple[QueryExample, ...] = (
|
|
QueryExample(
|
|
query="is:exposed",
|
|
description="Private values readable in a response",
|
|
group="Priority",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="group:cloud and is:exposed",
|
|
description="Exposed cloud provider credentials",
|
|
group="Priority",
|
|
),
|
|
QueryExample(
|
|
query="is:exposed and is:new",
|
|
description="Exposed values absent from the previous scan",
|
|
group="Change",
|
|
),
|
|
QueryExample(
|
|
query="secret:jwt and is:expired",
|
|
description="Expired JSON web tokens",
|
|
group="Kind",
|
|
),
|
|
QueryExample(
|
|
query="secret:aws_access_key",
|
|
description="AWS access keys",
|
|
group="Kind",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="secret:email",
|
|
description="Email addresses printed in responses",
|
|
group="Contacts",
|
|
generic=True,
|
|
),
|
|
QueryExample(
|
|
query="secret:email and subject:example.com",
|
|
description="Addresses on one email domain",
|
|
group="Contacts",
|
|
),
|
|
)
|
|
|
|
SECRET_QUERY = QueryRegistry(
|
|
key="secrets",
|
|
noun=SURFACE_NOUN[SurfaceDimension.SECRETS.value][0],
|
|
noun_plural=SURFACE_NOUN[SurfaceDimension.SECRETS.value][1],
|
|
fields=SECRET_FIELDS,
|
|
flags=SECRET_FLAGS,
|
|
groups=SECRET_GROUPS,
|
|
dimensions=SECRET_GROUP_DIMENSIONS,
|
|
examples=SECRET_EXAMPLES,
|
|
example_groups=SECRET_EXAMPLE_GROUPS,
|
|
)
|