Documentation
¶
Overview ¶
Package secscan is the security-scan orchestrator: it ties ClamAV, YARA, the threat feed, the hash cache, and the admin hash allowlist into the single pipeline the proxy hot path calls. Extracted from package main per ADR-0006 Slice 2; the collaborators arrive as constructor-injected interfaces (Deps), so the engines stay decoupled and the decision tree is unit-testable with fakes. package main keeps the free-function glue (safeScanBody, scanBlock*, bodyNeedsBuffering, the status map) and the production adapters (security_scan.go shim).
Pipeline for every proxied request/response:
URL / domain threat-feed check (CheckURL / CheckDomain) Instant, in-memory lookup against URLhaus + OpenPhish data. Applied in handleRequest before the request is forwarded upstream.
Response body scan (ScanBody) a. SHA-256 hash looked up in cache → return cached verdict immediately. b. ClamAV INSTREAM scan (binary + text content). c. YARA rule matching (all loaded *.yar / *.yara rules). d. Result stored in hash cache. Applied in handleHTTP and handleTunnelInspect after reading the body.
All components are optional:
- ClamAV is skipped when no address is configured or daemon is unreachable.
- YARA is skipped when no matcher is injected or it reports not loaded.
- Threat feeds are skipped when no checker is injected or it is disabled.
Index ¶
- Constants
- func AddRemoteScanFail()
- func AddScanSkipped()
- func DecompressForScan(data []byte, contentEncoding string) []byte
- type ClamScanner
- type CounterSnapshot
- type Deps
- type HashExcluder
- type RemoteScanner
- func (rs *RemoteScanner) Enabled() bool
- func (rs *RemoteScanner) Health() error
- func (rs *RemoteScanner) Init(baseURL string)
- func (rs *RemoteScanner) ScanBody(data []byte, contentType string) *Result
- func (rs *RemoteScanner) Status() (map[string]interface{}, error)
- func (rs *RemoteScanner) URL() string
- type Result
- type ScanResponse
- type Scanner
- func (ss *Scanner) BodyScanEnabled() bool
- func (ss *Scanner) CacheClear()
- func (ss *Scanner) CacheEvict(hash string) bool
- func (ss *Scanner) CacheGet(hash string) (hashcache.ScanCacheResult, bool)
- func (ss *Scanner) CacheReady() bool
- func (ss *Scanner) CacheSet(hash string, r hashcache.ScanCacheResult)
- func (ss *Scanner) CacheStats() (hits, misses int64, size int)
- func (ss *Scanner) CheckDomain(domain string) *Result
- func (ss *Scanner) CheckURL(rawURL string) *Result
- func (ss *Scanner) ClamAVStatus() string
- func (ss *Scanner) ClamAVVersion() (clamav.Version, bool)
- func (ss *Scanner) Enabled() bool
- func (ss *Scanner) Init(clamAddr string, maxBytes int64, cache *hashcache.HashCache)
- func (ss *Scanner) MaxBytes() int64
- func (ss *Scanner) ScanBody(data []byte) *Result
- type ThreatChecker
- type YARAMatcher
Constants ¶
const MaxDecompressBytes = 64 << 20
MaxDecompressBytes limits decompressed data to 64 MB to guard against gzip bombs.
const ScanBodyTimeout = 10 * time.Second
ScanBodyTimeout caps the total time for all body scanners (ClamAV + YARA). If the combined scan doesn't finish in time, the content is blocked (fail-closed).
Variables ¶
This section is empty.
Functions ¶
func AddRemoteScanFail ¶
func AddRemoteScanFail()
AddRemoteScanFail records a remote scan sidecar failure (incremented by the remote scanner client in package main).
func AddScanSkipped ¶
func AddScanSkipped()
AddScanSkipped records a response forwarded unscanned because it exceeded the scan buffer limit (incremented by package main's logScanLimitExceeded).
func DecompressForScan ¶
DecompressForScan transparently decompresses a response body based on its Content-Encoding header so that ClamAV/YARA signatures can match the actual content. Supports gzip, deflate, and identity (no-op). Brotli ("br") is attempted as gzip (some servers mislabel); on failure the raw bytes are returned so the scan still runs (defense in depth).
Returned data is limited to MaxDecompressBytes to guard against gzip bombs. If decompression fails, the original data is returned unchanged — the scan still runs on compressed bytes (fail-open for availability, but the signature gap is closed for the common case).
Types ¶
type ClamScanner ¶
ClamScanner is the ClamAV surface ScanBody/ClamAVStatus need.
type CounterSnapshot ¶
type CounterSnapshot struct {
ClamBlocked int64
YARABlocked int64
ThreatFeedBlocked int64
ScanTimeout int64
ScanSkipped int64
RemoteScanFail int64
}
CounterSnapshot is a point-in-time copy of the scan counters.
type Deps ¶
type Deps struct {
Clam ClamScanner
Yara YARAMatcher
Feed ThreatChecker
Excl HashExcluder
Cache *hashcache.HashCache
MaxBytes int64
}
Deps carries the injectable collaborators for New. Nil collaborators behave as absent (that engine/check is skipped), so partial injection is fine.
type HashExcluder ¶
HashExcluder is the admin hash-allowlist surface ScanBody needs.
type RemoteScanner ¶
type RemoteScanner struct {
// contains filtered or unexported fields
}
RemoteScanner sends scan requests to a scan microservice.
func (*RemoteScanner) Enabled ¶
func (rs *RemoteScanner) Enabled() bool
Enabled reports whether remote scanning is configured.
func (*RemoteScanner) Health ¶
func (rs *RemoteScanner) Health() error
Health checks the remote scan service liveness.
func (*RemoteScanner) Init ¶
func (rs *RemoteScanner) Init(baseURL string)
Init configures the remote scanner client.
func (*RemoteScanner) ScanBody ¶
func (rs *RemoteScanner) ScanBody(data []byte, contentType string) *Result
ScanBody sends data to the remote scan service and returns the result. Returns nil when the content is clean or the service is unreachable (fail-open on network errors to avoid blocking all traffic when the sidecar is down).
func (*RemoteScanner) Status ¶
func (rs *RemoteScanner) Status() (map[string]interface{}, error)
Status fetches the remote scanner status (mirrors /api/security-scan/status).
func (*RemoteScanner) URL ¶
func (rs *RemoteScanner) URL() string
URL returns the configured base URL.
type Result ¶
type Result struct {
Blocked bool
Reason string // virus name, YARA rule name, or feed source
Source string // "clamav", "yara", or "threatfeed"
Hash string // SHA-256 hex of scanned content (body scans only)
}
Result describes the outcome of a scan that triggered a block.
type ScanResponse ¶
type ScanResponse struct {
Clean bool `json:"clean"`
Blocked bool `json:"blocked"`
Reason string `json:"reason,omitempty"`
Source string `json:"source,omitempty"` // "clamav", "yara", "dpi"
Hash string `json:"hash,omitempty"` // SHA-256 of scanned content
DPIPattern string `json:"dpi_pattern,omitempty"` // matched DPI pattern
ElapsedMS int64 `json:"elapsed_ms"`
}
ScanResponse is the JSON wire type between the scan sidecar and this client. The sidecar server (package main, scan_svc.go) produces it; keep the field set in sync with the server handler.
type Scanner ¶
type Scanner struct {
// contains filtered or unexported fields
}
Scanner ties together ClamAV, YARA, the threat feed, and the hash cache into a single, easy-to-use interface for the proxy pipeline.
func New ¶
New builds a scanner from injected collaborators (ADR-0006). The scanner starts DISABLED — call Init to enable it — mirroring the pre-extraction package-main singleton (a struct literal with cache+maxBytes defaults that only Init flipped on). Collaborators are fixed at construction and never mutated afterwards (Init only touches clam/cache/maxBytes/enabled).
func (*Scanner) BodyScanEnabled ¶
BodyScanEnabled reports whether body scanning (ClamAV and/or YARA) is active.
func (*Scanner) CacheClear ¶
func (ss *Scanner) CacheClear()
CacheClear empties the scan hash cache.
func (*Scanner) CacheEvict ¶
CacheEvict removes a single hash from the scan cache, reporting whether it was present.
func (*Scanner) CacheGet ¶
func (ss *Scanner) CacheGet(hash string) (hashcache.ScanCacheResult, bool)
CacheGet looks up a scan verdict (test support).
func (*Scanner) CacheReady ¶
CacheReady reports whether the scan hash cache is initialised.
func (*Scanner) CacheSet ¶
func (ss *Scanner) CacheSet(hash string, r hashcache.ScanCacheResult)
CacheSet stores a scan verdict directly (test support and remote-scan result adoption).
func (*Scanner) CacheStats ¶
CacheStats returns hash-cache hit/miss counters and current size.
func (*Scanner) CheckDomain ¶
CheckDomain checks a bare hostname against the threat feed. Returns nil when no threat is found.
func (*Scanner) CheckURL ¶
CheckURL checks a full URL against the threat feed. Returns nil when no threat is found.
func (*Scanner) ClamAVStatus ¶
ClamAVStatus returns a human-readable daemon connectivity string. Tier 2.3: Result is cached for clamStatusTTL to avoid hammering the ClamAV daemon on every admin dashboard poll. Cache is invalidated on Init().
func (*Scanner) ClamAVVersion ¶ added in v1.0.22
ClamAVVersion returns the ClamAV engine + signature database version, so operators can see whether virus definitions are current. Returns ok=false when ClamAV is disabled, the client does not support VERSION, or the daemon is unreachable. Cached for clamVersionTTL (definitions change rarely) and invalidated on Init, mirroring ClamAVStatus.
func (*Scanner) Init ¶
Init configures the scanner.
clamAddr — ClamAV address string (see clamav.New); "" disables ClamAV.
maxBytes — maximum bytes to buffer per response (0 = use default 5 MiB).
cache — hash cache to adopt; nil keeps the current one (ADR-0006: the
cache is handed over here instead of being poked from outside).
type ThreatChecker ¶
type ThreatChecker interface {
Enabled() bool
CheckURL(rawURL string) (bool, string)
CheckDomain(domain string) (bool, string)
}
ThreatChecker is the threat-feed surface CheckURL/CheckDomain need.
type YARAMatcher ¶
YARAMatcher is the YARA surface the scan pipeline needs. Loaded and Enabled are deliberately distinct to preserve pre-ADR-0006 behavior verbatim: BodyScanEnabled keys on rules-present only (Loaded), while the body scan additionally honors the runtime enable toggle (Enabled).