checks

package
v0.0.0-...-2a8aaff Latest Latest
Warning

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

Go to latest
Published: Sep 21, 2026 License: AGPL-3.0 Imports: 102 Imported by: 0

Documentation

Overview

HTTP abuse detection.

This file holds the access-log line parser, the per-scan aggregator struct (domlogStats), the UA classifier, the bot-classifier interface, and the static allowlist classifier that consults embedded bot IP ranges. The rDNS verifying classifier arrives in Task 5.

Package checks: http_asn_crawl detector — single-ASN distributed crawl of uncacheable URLs saturating one account's PHP pool. See docs/superpowers/specs/2026-06-24-http-asn-crawl-detector-design.md.

Index

Constants

View Source
const (
	BlockSourceScan      = "scan"
	BlockSourceChallenge = "challenge"
	BlockSourceIncident  = "incident"
	BlockSourceCentral   = "central_intel"
)

Block sources label who asked for an auto-response block. They feed the outcome metric and let evidence rows say which pipeline produced them.

View Source
const (
	BlockSourceCLI   = "cli"
	BlockSourceWebUI = "web_ui"
)

Operator block sources, alongside the auto-response sources in applyblock.go. Operator paths bypass the chokepoint (they force-block and audit separately) but report into the same outcome metric.

View Source
const (
	QuarantineRestoreReplaceIfUnchanged = "replace_if_unchanged"
	QuarantineRestoreRemoveIfUnchanged  = "remove_if_unchanged"
)
View Source
const (
	CategoryAuth        = "Authentication & Login"
	CategoryBruteForce  = "Brute Force"
	CategoryMalware     = "Malware & Webshells"
	CategoryWeb         = "Web & Application"
	CategoryDatabase    = "Database Content"
	CategoryEmail       = "Email & Phishing"
	CategoryPerformance = "Performance"
	CategoryNetwork     = "Network & Firewall"
	CategorySystem      = "System Integrity"
	CategoryWAF         = "WAF & ModSecurity"
	CategoryCorrelation = "Correlation & Health"
	CategoryInternal    = "Internal"
)

Category labels are the groupings shown in the multi-select UI. Keep the order below in sync with checkCategoryOrder so categories render in a sane order rather than alphabetically (Auth first, Internal last).

View Source
const ContentLogicVersion = 1

ContentLogicVersion identifies the current shape of the PHP content-analysis heuristic set (analyzePHPContent and its helpers). BUMP IT in the same commit as any change to those heuristics, so findings produced by the previous logic are re-verified and cleared by the daemon sweep. See docs/superpowers/specs/2026-06-20-stale-content-finding-reverification-design.md.

View Source
const ContentScannerVersion = 4

ContentScannerVersion identifies scanner behavior that is not represented by the loaded YAML signature version or YARA rule count. Bump it when shared content classification changes so the daemon re-checks stale findings.

View Source
const JSTaintLogicVersion = 2

JSTaintLogicVersion identifies the current semantics of the JavaScript keystroke taint analyzer (internal/jstaint). BUMP IT in the same commit as any change to its sources, propagation, sinks, resource limits, parser version, or content pre-filter so findings produced by the previous logic are re-verified under the new one.

View Source
const MaxInertPHPScanBytes = benignPHPStubMaxScan

MaxInertPHPScanBytes is the largest file the inert-content recognizers read in full. Translation caches and comment-only stubs need every byte to prove that no code follows; a proven PHP terminator can still be accepted from an incomplete prefix because its tail is unreachable.

View Source
const (

	// PHPConfigMaxBytes is the shared scheduled and realtime read ceiling.
	PHPConfigMaxBytes = 1 << 20
)
View Source
const PHPTaintLogicVersion = 4

PHPTaintLogicVersion identifies the current semantics of the PHP remote- source taint analyzer (internal/phptaint). BUMP IT in the same commit as any change to its parser, pre-filter, propagation, sinks, resource limits, or evidence semantics so existing findings are re-verified by the isolated worker under the new logic.

View Source
const SeccompDropInBaseName = "csm-copy-fail-seccomp.conf"

SeccompDropInBaseName is the file name CSM writes inside each unit's /etc/systemd/system/<unit>.d/ override directory. Stable so the hardening audit can scan for it and the remove path can clean up without guessing.

Variables

View Source
var AncestryProbe func(pid uint32) bool

AncestryProbe reports whether the process tree rooted at pid contains a package-manager process. Nil on hosts without BPF process context, in which case ancestry checks no-op and the pkg-window + cron-content layers still apply. The BPF daemon wires this from processctx at startup.

View Source
var ErrKernelConfigUnreadable = os.ErrNotExist

EnsureFile is a sentinel return value: callers can use os.IsNotExist to test for the "kernel-config file absent" case explicitly.

View Source
var ErrModSecReloadNotConfigured = errors.New("modsec.reload_command is not set, so CSM cannot confirm its updated ModSecurity rules are active; they apply at the next web server reload")

ErrModSecReloadNotConfigured reports a CSM rule section that differs from the last one the web server reloaded, on a host with no reload command.

View Source
var ErrNoIPBlocker = errors.New("firewall engine not available")

ErrNoIPBlocker is returned when no firewall engine is wired. Callers must treat it as "the block did not happen", never as success.

View Source
var ErrVirtualPatchRestoreConflict = errors.New("virtual-patch restore conflicts with current .htaccess")
View Source
var ForceAll bool

ForceAll forces all checks to run regardless of throttle (used by baseline).

Functions

func AFAlgMarkerPath

func AFAlgMarkerPath() string

AFAlgMarkerPath returns the canonical marker file location. Exposed for the cmd/csm CLI which prints it to operators; production code in this package should reference the unexported constant directly.

func AFAlgOwner

func AFAlgOwner(ev AFAlgEvent) string

AFAlgOwner resolves the hosting account whose process opened the socket. The audit uid is resolved through the shared passwd cache; root, service users and unknown uids yield "" so the finding stays unattributed.

func AccountFromContext

func AccountFromContext(ctx context.Context) string

AccountFromContext returns the account scope previously attached by ContextWithAccountScope, or "" when no scope is set.

func AccountHomePatterns

func AccountHomePatterns() []string

AccountHomePatterns returns the glob for every account home ("<root>/*") on this platform. The realtime scanner needs it to recognise an account tree without hardcoding /home.

func AccountOwnerForDomain

func AccountOwnerForDomain(domain string) (string, bool)

AccountOwnerForDomain resolves the hosting account that owns a mail domain, for producers that know a verified local mailbox or domain. Correlation never calls it; owners are resolved where the mailbox is known.

func AllCheckNames

func AllCheckNames() []string

AllCheckNames returns every registered Check name, sorted alphabetically. Includes internal names; callers that render user-facing UI should use PublicCheckInfos instead.

func ApplyAFAlgSeccompDropIns

func ApplyAFAlgSeccompDropIns() ([]string, error)

ApplyAFAlgSeccompDropIns writes the canonical drop-in file for every candidate unit that exists on this host AND does not already have the file. After all writes, runs systemctl daemon-reload and a reload-or-restart per touched unit so the seccomp filter takes effect immediately.

Returns the list of units that received a new drop-in this call. An empty list with a nil error means everything was already covered (idempotent re-run).

func AttributeSocketOwner

func AttributeSocketOwner(f *alert.Finding, uid uint32)

AttributeSocketOwner attaches the hosting account identified by a kernel socket or connection-event UID. A missing process snapshot does not remove this evidence. Root, service and unresolved UIDs remain unattributed. The message carries the account too, so dispatch and audit identities keep different accounts contacting the same destination separate.

func AuditHtaccessContent

func AuditHtaccessContent(path string, content []byte) ([]alert.Finding, []htaccessByteRange)

func AuditHtaccessFile

func AuditHtaccessFile(path string) ([]alert.Finding, []htaccessByteRange)

AuditHtaccessFile runs every registered detector against the file at path. Returns the alert findings (one per detector hit) and the merged byte ranges that the cleaner would remove. The two outputs travel together so cleaning never disagrees with what the operator was alerted about.

func AutoBlockIPs

func AutoBlockIPs(cfg *config.Config, findings []alert.Finding) []alert.Finding

AutoBlockIPs processes all findings, including repeats, for IP blocking.

func AutoBlockQueueStatuses

func AutoBlockQueueStatuses(now time.Time) map[string]queuehealth.Status

AutoBlockQueueStatuses reads queue memory without waiting for the state mutex, filesystem, firewall or database. A batch is timed by operation progress.

func AutoCleanHtaccess

func AutoCleanHtaccess(cfg *config.Config, findings []alert.Finding) []alert.Finding

AutoCleanHtaccess runs the hardened .htaccess cleaner against every finding emitted by the new detector registry, gated by AutoResponse.CleanHtaccess. Skipped when the daemon's auto-response pipeline is disabled overall.

Unlike AutoQuarantineFiles, this routes around the quarantine/clean fork (.htaccess files are infrastructure -- moving them to /opt/csm/quarantine breaks the site). Each invocation backs up the original to /opt/csm/quarantine/pre_clean/<ts>_* inside CleanHtaccessFile before atomic-replacing. Marks evaluated input findings so alert delivery cannot repeat a response.

func AutoFixPermissions

func AutoFixPermissions(cfg *config.Config, findings []alert.Finding) (actions []alert.Finding, fixedKeys []string)

AutoFixPermissions sets world/group-writable PHP files to 0644. Returns the auto-response action findings and the keys of original findings that were successfully fixed (so the caller can dismiss them from the UI).

func AutoFixWPCron

func AutoFixWPCron(cfg *config.Config, findings []alert.Finding) (actions []alert.Finding, fixedKeys []string)

AutoFixWPCron disables WP-Cron and installs a per-user system cron for every perf_wp_cron finding. Returns the auto-response action findings and the keys of the originals so the caller can dismiss them. Gated behind an explicit opt-in because it edits customer wp-config.php and crontabs.

func AutoKillProcesses

func AutoKillProcesses(ctx context.Context, cfg *config.Config, findings []alert.Finding) []alert.Finding

AutoKillProcesses kills processes that match critical findings. Only targets: fake kernel threads, reverse shells, GSocket processes. Never kills root system services or cPanel processes.

func AutoQuarantineFiles

func AutoQuarantineFiles(cfg *config.Config, findings []alert.Finding) []alert.Finding

AutoQuarantineFiles moves malicious files to quarantine directory. Preserves original path and metadata in a sidecar .meta file. Marks evaluated input findings so alert delivery cannot repeat a response.

func AutoRespondDBMalware

func AutoRespondDBMalware(cfg *config.Config, findings []alert.Finding) []alert.Finding

AutoRespondDBMalware processes database injection findings and takes automated action: blocks attacker IPs extracted from WordPress session tokens, revokes compromised user sessions, and cleans confirmed malicious content from wp_options or stored database objects.

Only acts on high-confidence findings:

  • db_options_injection with confirmed malicious external script URLs
  • db_siteurl_hijack (siteurl/home pointing to malicious content)
  • db_malicious_trigger/event/procedure/function with structured metadata

Does NOT act on:

  • db_spam_injection (spam posts — needs manual review)
  • db_post_injection (script in posts — too many FPs from page builders)
  • db_options_injection without confirmed malicious URLs

func AutoRespondDBMalwareWithPolicy

func AutoRespondDBMalwareWithPolicy(cfg *config.Config, findings []alert.Finding, canRemediate func(alert.Finding) bool) []alert.Finding

AutoRespondDBMalwareWithPolicy keeps session IP enforcement independent of permission to edit a database or revoke sessions. A nil policy permits both; callers with suppressions supply a per-finding remediation decision.

func AutoVirtualPatchExposedFiles

func AutoVirtualPatchExposedFiles(cfg *config.Config, findings []alert.Finding) []alert.Finding

AutoVirtualPatchExposedFiles is the scan-time entry point. It acts only when auto_response is enabled and the mode is "auto"; the write is gated by the shared auto_response dry_run flag (dry_run reports the intended denials).

func BumpDirectSMTPEgressFindings

func BumpDirectSMTPEgressFindings()

BumpDirectSMTPEgressFindings increments the per-finding counter. Called by the connection consumer when EvaluateDirectSMTPEgress returns a finding.

func CMSCacheEmpty

func CMSCacheEmpty() bool

CMSCacheEmpty reports whether any verified core files are cached, so a caller can skip hashing entirely when the answer cannot be yes.

func CMSCacheMayContainSize

func CMSCacheMayContainSize(size int64) bool

CMSCacheMayContainSize reports whether hashing a file of size bytes can possibly produce a cached CMS hash.

func CanVerify

func CanVerify(checkType string) bool

CanVerify reports whether VerifyFinding has an automated re-check for the given check type. The Web UI gates the per-finding "Re-check" action on this so it never shows a button that could only report "not auto-verifiable".

func ChallengeRouteIPs

func ChallengeRouteIPs(cfg *config.Config, findings []alert.Finding) []alert.Finding

ChallengeRouteIPs processes findings and routes eligible IPs to the challenge list instead of hard-blocking them. Must be called BEFORE AutoBlockIPs so that challenged IPs are on the list when AutoBlockIPs checks Contains().

func ChallengeThenBlock

func ChallengeThenBlock(cfg *config.Config, findings []alert.Finding) (challengeActions, blockActions []alert.Finding)

ChallengeThenBlock runs the two IP-disposition stages in their required order -- challenge routing first so an eligible IP is on the challenge list before AutoBlockIPs checks membership, then hard-blocking -- and returns both action sets. Auto-response call sites use this single helper instead of hand-ordering the two calls, so the "challenge before block" invariant cannot be silently broken by reordering in one path. Both stages run on the same finding set (the full/repeat-offender set); callers append the returned actions wherever their pipeline expects them.

func CheckAFAlgEnforcement

func CheckAFAlgEnforcement(_ context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckAFAlgEnforcement is the periodic critical-tier check that enforces the AF_ALG mitigation policy. When the operator has opted in (via `csm harden --copy-fail`, which writes the marker file), this check reverts any drift on each tick. It is a no-op in advisory mode.

Emits a Warning finding (one per tick that took action) so the operator has an alert-pipeline record that system state was modified. Steady-state ticks emit no findings. Warning is the lowest severity available in the alert.Severity enum (Warning < High < Critical, no Info level).

func CheckAFAlgSocketUsage

func CheckAFAlgSocketUsage(_ context.Context, _ *config.Config, st *state.Store) []alert.Finding

CheckAFAlgSocketUsage scans the audit log for csm_af_alg_socket events and emits one Critical finding per strictly-newer event. The first run alerts on every event found — AF_ALG-from-userland is an exploit signature for CVE-2026-31431 ("Copy Fail"), not a baseline metric, so silent seeding would hide pre-existing compromise. The cursor in state.Store prevents duplicates on subsequent sweeps and survives daemon restarts.

Filtering is delegated to grep so we don't load the whole multi-hundred-MB audit log into memory each tick (same precedent as getAuditShadowInfo in auth.go). RunAllowNonZero is required because grep returns exit 1 on "no match" — the healthy default — and that must not surface as an error.

func CheckAPIAuthFailures

func CheckAPIAuthFailures(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckAPIAuthFailures parses cPanel access log for failed API authentication.

func CheckAPITokens

func CheckAPITokens(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

func CheckAdminEmailOverlap

func CheckAdminEmailOverlap(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckAdminEmailOverlap records every WordPress administrator email encountered during an account scan into a server-wide bbolt bucket, then emits a Warning finding for each email whose owner list now spans the configured minimum number of distinct accounts. The detection surface is shared-hosting credential leakage: a single compromised contractor account is one credential disclosure away from administrator access on every site they touch.

The check is silent when the bbolt store is unavailable (early daemon startup, test harness without state injection) -- it can't observe overlap without persistence between scans, and falling silent is better than a misleading partial result.

func CheckCpanelFileManager

func CheckCpanelFileManager(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckCpanelFileManager parses the cPanel access log for file management operations from non-infra IPs (file uploads, edits via cPanel File Manager).

func CheckCpanelLogins

func CheckCpanelLogins(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

CheckCpanelLogins parses the cPanel session log for suspicious login activity: - cPanel (cpaneld) logins from non-infra IPs - Same account logged in from multiple distinct IPs (credential compromise indicator) - Password change purge events (attacker or auto-response password resets)

func CheckCredentialReuse

func CheckCredentialReuse(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

CheckCredentialReuse flags WordPress administrator accounts that share an identical password hash across two or more distinct hosting accounts. Password hashes are salted on modern WordPress installs, so this is an exact at-rest hash reuse signal, not a weak-password detector.

Privacy: the raw password hash is never stored, logged, or emitted. Only a truncated one-way fingerprint is used to group identical hashes, and findings report the affected accounts and a count -- not the hash.

func CheckCrontabs

func CheckCrontabs(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

func CheckDNSConnections

func CheckDNSConnections(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckDNSConnections looks for established connections to port 53 on DNS servers that are NOT in /etc/resolv.conf. This catches DNS tunneling, GSocket relay discovery, and malware using hardcoded resolvers. Connections owned by known DNS server processes (e.g. named) are skipped.

func CheckDNSZoneChanges

func CheckDNSZoneChanges(_ context.Context, _ *config.Config, store *state.Store) []alert.Finding

CheckDNSZoneChanges monitors named zone files for tampering.

A raw file-hash watch over /var/named is too coarse: every cPanel serial bump, AutoSSL DCV TXT record, DKIM rotation, and customer Zone Editor edit rewrites the file, so hashing the whole zone alerts on routine activity and buries real hijacks. This check instead compares only the security-relevant records and weighs the change against cPanel provenance:

  • The "security fingerprint" covers delegation (NS), mail (MX), and apex/ wildcard address records -- the records an attacker rewrites to take over a domain. Serial, TXT/DKIM/SPF/DCV, and ordinary subdomain A records are ignored, so legitimate churn stays quiet.
  • cPanel stamps each zone it writes with an "(update_time):" header. A security change with no advance of that stamp means the file was edited out of band (direct file write, or a non-cPanel path) -- the signature of a hijack -- and is reported High. A security change that did go through cPanel is trusted more: an NS/MX move still surfaces as a Warning (could be a compromised account), while an apex/wildcard address repoint by the authenticated owner is routine and stays quiet.

There is deliberately no bulk-suppression gate: a mass out-of-band NS rewrite across every hosted domain is exactly the incident operators must see, and the per-record/provenance model already keeps benign mass operations quiet.

func CheckDatabaseContent

func CheckDatabaseContent(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

CheckDatabaseContent scans WordPress databases for injected malware, spam content, siteurl hijacking, and rogue admin accounts.

func CheckDatabaseDumps

func CheckDatabaseDumps(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

CheckDatabaseDumps detects mysqldump/pg_dump processes running under non-root users - potential data exfiltration.

func CheckDatabaseObjects

func CheckDatabaseObjects(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckDatabaseObjects scans every WordPress installation's database for triggers, events, procedures, and functions. Critical findings fire when the body matches a known-malware pattern; Warning findings fire when an object exists at all (vanilla CMSes ship none). The Detection.DBObjectScanning kill-switch silences both emit paths without disabling the manual drop-object CLI.

func CheckDispatchQueueStatus

func CheckDispatchQueueStatus(now time.Time) queuehealth.Status

CheckDispatchQueueStatus measures pending checks and their runner wrappers.

func CheckDrupalContent

func CheckDrupalContent(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

func CheckEmailPasswords

func CheckEmailPasswords(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckEmailPasswords audits Dovecot email account passwords for weak/predictable patterns. Uses internal throttle: skips if last refresh was less than password_check_interval_min ago.

func CheckErrorLogBloat

func CheckErrorLogBloat(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckErrorLogBloat walks configured web roots (default /home/*/public_html on cPanel) looking for error_log files that exceed the configured size threshold. The runner enforces a 60-minute throttle via checkThrottleMin.

func CheckExecutionQueueStatus

func CheckExecutionQueueStatus(now time.Time) queuehealth.Status

CheckExecutionQueueStatus reads memory only, including after a runner exits.

func CheckExposedFiles

func CheckExposedFiles(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckExposedFiles scans every cPanel docroot for sensitive files (database dumps, backup archives, config/source backups, phpinfo) that the web server actually serves, and reports each confirmed exposure. It reads only response headers except for a bounded phpinfo body used to reject empty stubs.

func CheckFTPLogins

func CheckFTPLogins(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

CheckFTPLogins detects pure-ftpd brute force. With a state store it reads /var/log/messages forward-only and accumulates per-IP failures over a sliding window; without a store it falls back to the legacy per-cycle tail.

func CheckFakeKernelThreads

func CheckFakeKernelThreads(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

func CheckFileIndex

func CheckFileIndex(ctx context.Context, cfg *config.Config, st *state.Store) []alert.Finding

CheckFileIndex builds an index of suspicious files using pure Go directory reads, diffs against the previous index, and alerts on new files. Uses directory mtime caching: unchanged dirs carry forward previous entries without calling ReadDir, while changed dirs are re-scanned.

When ctx carries AccountScanOptions with ForceFileIndex=true the function runs in audit mode: it enumerates only the in-scope account, bypasses the directory mtime cache entirely, and writes none of the live state files (fileindex.current, fileindex.previous, dircache.json). The normal incremental baseline is left byte-for-byte intact. All indexed files are treated as new so the caller receives findings for the full current state of the account.

func CheckFilesystem

func CheckFilesystem(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckFilesystem uses globs and targeted ReadDir to check for backdoors, hidden files, and SUID binaries. No `find` command needed.

func CheckFirewall

func CheckFirewall(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

func CheckForwarders

func CheckForwarders(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckForwarders audits all valiases and vfilters files for dangerous forwarder patterns. Uses internal throttle: skips if last refresh was less than password_check_interval_min ago (reuses the same interval).

func CheckGroupWritablePHP

func CheckGroupWritablePHP(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

CheckGroupWritablePHP scans for PHP files that are group-writable where the group is the web server (nobody/www-data). This allows webshells to persist by the web server modifying PHP files.

func CheckHealth

func CheckHealth(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckHealth verifies that CSM's dependencies are working. Reports on missing external commands, broken auditd, etc.

func CheckHtaccess

func CheckHtaccess(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckHtaccess scans for malicious .htaccess directives using pure Go ReadDir.

func CheckIPReputation

func CheckIPReputation(ctx context.Context, cfg *config.Config, scanState *state.Store) []alert.Finding

CheckIPReputation looks up non-infra IPs against threat intelligence. Four-tier approach:

  1. Skip if already blocked
  2. Check local threat DB (permanent blocklist + free feeds)
  3. Check AbuseIPDB cache
  4. Query AbuseIPDB for truly unknown IPs (max 5/cycle, ~720/day)

func CheckJoomlaContent

func CheckJoomlaContent(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

CheckJoomlaContent scans every Joomla installation under /home/*/public_html for malware-pattern matches in the three canonical attacker-touched tables. Mirrors the structure of CheckDatabaseContent without sharing code -- the credentials and table layout differ enough that a generic dispatcher is more abstraction than this point in the codebase needs.

func CheckKernelModules

func CheckKernelModules(ctx context.Context, _ *config.Config, store *state.Store) []alert.Finding

CheckKernelModules compares loaded kernel modules against baseline. All modules present at baseline time are considered known. Only modules loaded AFTER baseline trigger alerts.

func CheckLoadAverage

func CheckLoadAverage(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckLoadAverage compares load averages against per-core thresholds from config. The 1-minute load drives the Critical / High findings; when 1-minute is below the High threshold we additionally check the 5- and 15-minute averages for sustained pressure (>= 0.7 * High threshold on both) and emit a Warning. The sustained variant catches "constant 22%-of-cores busy for 15 minutes" which is invisible to a 1-minute spike check but is what operators actually want to see.

func CheckLocalThreatScore

func CheckLocalThreatScore(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckLocalThreatScore generates findings for IPs that have accumulated a high local threat score but have not yet been blocked. Runs every 10 minutes as part of TierCritical.

func CheckMagentoContent

func CheckMagentoContent(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

func CheckMailFilters

func CheckMailFilters(ctx context.Context, cfg *config.Config, st *state.Store) []alert.Finding

CheckMailFilters scans per-mailbox and domain-wide Exim filters for BEC-style exfiltration rules. Throttled to PasswordCheckIntervalMin, like the forwarder audit it complements.

func CheckMailPerAccount

func CheckMailPerAccount(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckMailPerAccount counts recent Exim arrivals per envelope-sender domain. Ownership requires the same verified submitter across the entire count.

func CheckMailQueue

func CheckMailQueue(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

func CheckModSecAuditLog

func CheckModSecAuditLog(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

CheckModSecAuditLog parses the ModSecurity audit log for blocked attacks. High-volume attackers are reported for potential auto-blocking.

func CheckMySQLConfig

func CheckMySQLConfig(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckMySQLConfig inspects MySQL global variables and runtime status for performance-impacting misconfigurations. Each issue emits its own finding with a stable message so deduplication works correctly.

func CheckMySQLUsers

func CheckMySQLUsers(ctx context.Context, _ *config.Config, store *state.Store) []alert.Finding

CheckMySQLUsers queries for MySQL users with elevated privileges that aren't standard cPanel-managed users.

func CheckNulledPlugins

func CheckNulledPlugins(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

CheckNulledPlugins scans WordPress plugin directories for signs of nulled/pirated plugins: missing licenses, known crack patterns, GPL bypass code, and plugins not found on wordpress.org.

func CheckOpenBasedir

func CheckOpenBasedir(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

CheckOpenBasedir verifies that each cPanel account has proper PHP isolation via CageFS and/or open_basedir. Flags accounts where CageFS is disabled AND open_basedir is not set.

func CheckOpenCartContent

func CheckOpenCartContent(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

func CheckOutboundConnections

func CheckOutboundConnections(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

func CheckOutboundEmailContent

func CheckOutboundEmailContent(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckOutboundEmailContent samples outbound email content from Exim spool for phishing URLs, credential harvesting language, suspicious mailers, and Reply-To mismatches.

func CheckOutboundPasteSites

func CheckOutboundPasteSites(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckOutboundPasteSites detects connections to known paste/exfiltration sites.

func CheckOutboundUserConnections

func CheckOutboundUserConnections(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckOutboundUserConnections looks for non-root user processes making outbound connections to IPs that aren't infra or well-known services. Catches compromised accounts phoning home.

func CheckOutdatedPlugins

func CheckOutdatedPlugins(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckOutdatedPlugins scans all WordPress installations for plugins with available updates and emits findings based on severity of the version gap. Results are cached in bbolt with a configurable refresh interval (default 24h).

func CheckPHPConfigChanges

func CheckPHPConfigChanges(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

CheckPHPConfigChanges monitors .user.ini and php.ini files anywhere under an account's document roots for settings that weaken PHP security (disable_functions cleared or neutralized, allow_url_include enabled, open_basedir removed). It runs as a deep check; the fanotify watcher also catches these writes in real-time.

func CheckPHPContent

func CheckPHPContent(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

func CheckPHPHandler

func CheckPHPHandler(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckPHPHandler detects PHP CGI handler usage on LiteSpeed servers. On LiteSpeed, CGI is significantly slower than LSAPI; this check fires a Critical finding for each PHP version using the CGI handler.

func CheckPHPProcessLoad

func CheckPHPProcessLoad(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckPHPProcessLoad scans /proc for PHP web workers, groups them by user, and fires Critical if total exceeds cores*multiplier, High per user if individual count exceeds threshold.

func CheckPHPProcesses

func CheckPHPProcesses(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

CheckPHPProcesses inspects running lsphp processes to detect active webshell execution. Only reads /proc cmdline - zero disk I/O.

func CheckPhishing

func CheckPhishing(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckPhishing scans HTML files in user document roots for phishing pages. Uses three detection layers:

  1. Content analysis - brand impersonation + credential harvesting patterns
  2. Structural analysis - self-contained HTML with embedded assets
  3. Directory anomaly - lone HTML files in otherwise empty directories

func CheckRPMIntegrity

func CheckRPMIntegrity(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

CheckRPMIntegrity verifies critical system binaries haven't been modified. Only checks a small set of security-critical packages. Dispatches to rpm -V on RHEL-family systems and debsums/dpkg --verify on Debian family.

func CheckRedisConfig

func CheckRedisConfig(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckRedisConfig inspects a local Redis instance for performance-impacting misconfigurations: unset maxmemory, noeviction policy, non-expiring keys, and an overly aggressive bgsave schedule for the dataset size.

func CheckSSHDConfig

func CheckSSHDConfig(ctx context.Context, _ *config.Config, store *state.Store) []alert.Finding

CheckSSHDConfig monitors sshd_config for dangerous changes.

func CheckSSHKeys

func CheckSSHKeys(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

func CheckSSHLogins

func CheckSSHLogins(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

CheckSSHLogins parses the platform authentication log for SSH logins from non-infra IPs. With a state store it reads the log forward-only from where the previous cycle stopped; without one it falls back to a per-cycle tail.

func CheckSSLCertIssuance

func CheckSSLCertIssuance(ctx context.Context, _ *config.Config, store *state.Store) []alert.Finding

CheckSSLCertIssuance monitors AutoSSL logs for new certificate issuance. Attackers may issue certificates for phishing domains using compromised accounts.

func CheckSensitiveFiles

func CheckSensitiveFiles(_ context.Context, _ *config.Config, store *state.Store) []alert.Finding

CheckSensitiveFiles is the periodic safety-net that runs when the BPF live monitor is unavailable or disabled. It content-hashes every watchset path and emits a finding when a hash differs from the previous run. The first run records baselines without emitting findings.

CheckShadowChanges in auth.go does richer per-user diff and infra-IP suppression for /etc/shadow specifically; this catch-all complements that for sshd_config, sudoers, cron drop-ins, etc. Both run in parallel; audit-log dedup handles the (rare) overlap.

func CheckShadowChanges

func CheckShadowChanges(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

func CheckSupplyChain

func CheckSupplyChain(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckSupplyChain scans customer dependency lockfiles for versions with known advisories. Dormant unless an advisory database is present at <state>/advisories/supply-chain.json.

func CheckSuspiciousProcesses

func CheckSuspiciousProcesses(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

func CheckSwapAndOOM

func CheckSwapAndOOM(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckSwapAndOOM checks for OOM killer events in dmesg and elevated swap usage from /proc/meminfo. Host OOM is Critical, cgroup OOM Warning, and swap usage above 50% High.

func CheckSymlinkAttacks

func CheckSymlinkAttacks(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

CheckSymlinkAttacks detects symbolic links inside user public_html directories that point outside the account's own directory. This is a classic shared hosting attack to read other users' files.

func CheckUID0Accounts

func CheckUID0Accounts(ctx context.Context, _ *config.Config, _ *state.Store) []alert.Finding

func CheckVulnerablePlugins

func CheckVulnerablePlugins(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckVulnerablePlugins matches the shared WordPress plugin inventory against the curated known-vulnerable feed. It participates in the same serialized refresh as CheckOutdatedPlugins and only reports -- it never disables a plugin.

func CheckVulnerableTimThumb

func CheckVulnerableTimThumb(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckVulnerableTimThumb scans web document roots for bundled TimThumb scripts and reports each confirmed instance. Detection-only: TimThumb is legitimate (if abandoned) code, so it is never auto-quarantined -- removing it would break the theme.

func CheckWAFStatus

func CheckWAFStatus(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckWAFStatus verifies that ModSecurity is loaded, the engine is in enforcement mode (not DetectionOnly), OWASP/Comodo rules are active, and rules are up to date.

func CheckWHMAccess

func CheckWHMAccess(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckWHMAccess parses the cPanel access log for WHM (port 2087) logins and password change API calls from non-infra IPs. Only reads the tail of the log - lightweight.

func CheckWPBruteForce

func CheckWPBruteForce(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckWPBruteForce detects brute force attacks against wp-login.php and xmlrpc.php by scanning access logs. Always scans per-domain domlogs because on LiteSpeed+cPanel, virtual host traffic only appears there. The central access log is scanned as a supplement.

Aggregates per-IP counts across ALL domains -- catches attackers who distribute requests across many sites to stay under per-site thresholds.

func CheckWPConfig

func CheckWPConfig(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckWPConfig scans /home/*/public_html (max depth 2) for wp-config.php files and reports excessive WP_MEMORY_LIMIT values, unlimited max_execution_time, and display_errors enabled in production. The runner enforces a 60-minute throttle via checkThrottleMin.

func CheckWPCore

func CheckWPCore(ctx context.Context, cfg *config.Config, _ *state.Store) (findings []alert.Finding)

CheckWPCore runs wp core verify-checksums for each WordPress installation using a bounded worker pool for concurrency. Installations that pass verification have their core files cached in GlobalCMSCache so the real-time scanner can skip signature matches on known-clean CMS files.

func CheckWPCron

func CheckWPCron(ctx context.Context, cfg *config.Config, scanState *state.Store) []alert.Finding

CheckWPCron scans configured web roots plus validated cPanel document roots for WordPress installs that have not disabled the built-in WP-Cron mechanism. Running WP-Cron via HTTP is a common cause of high load on busy sites. The runner enforces a 60-minute throttle via checkThrottleMin.

func CheckWPPluginVerification

func CheckWPPluginVerification(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckWPPluginVerification reports the shared inventory independently of its outdated/known-vulnerable consumers. The existing plugin refresh interval and disabled_checks controls remain authoritative.

func CheckWPTransientBloat

func CheckWPTransientBloat(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckWPTransientBloat scans configured web roots (default /home/*/public_html on cPanel) for WordPress installs and queries each database for oversized transients. DB credentials are read from wp-config.php; the password is passed via MYSQL_PWD environment variable (never on the command line). The runner enforces a 60-minute throttle via checkThrottleMin.

func CheckWebmailLogins

func CheckWebmailLogins(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckWebmailLogins parses cPanel access log for webmail logins from non-infra IPs.

func CheckWebshells

func CheckWebshells(ctx context.Context, cfg *config.Config, _ *state.Store) []alert.Finding

CheckWebshells uses pure Go ReadDir to scan for known webshell files and directories. No `find` command needed.

func CheckYARADeep

func CheckYARADeep(ctx context.Context, cfg *config.Config, st *state.Store) []alert.Finding

CheckYARADeep is the shared rolling deep-content walk. It reads each file once and dispatches the same in-memory snapshot to three consumers with independent cursors and completion records: the YARA backend and the JS and PHP taint analyzers. A missing, disabled, or failed consumer neither prevents nor controls another consumer's progress.

func CleanDatabaseSpam

func CleanDatabaseSpam(account string) []alert.Finding

CleanDatabaseSpam removes known spam/malware patterns from WordPress database content. Targets wp_posts and wp_options tables. Returns findings for each cleaned row.

func ContentDetectionVersion

func ContentDetectionVersion() string

ContentDetectionVersion returns a token identifying the full content-detection logic in effect: the heuristic and scanner versions, the loaded signature-set version, the loaded YARA rule count, and both taint analyzer versions. The re-verifier always re-runs the real classifier, so this token only gates the daemon sweep and enriches audit detail; its precision is not security-critical.

func ContextWithAccountScope

func ContextWithAccountScope(ctx context.Context, account string) context.Context

ContextWithAccountScope returns a derived context that restricts filesystem-based checks to a single cPanel/Linux account. Callers pass the resulting context into every check; helpers like GetScanHomeDirs read the scope back out. Empty account is a no-op (returns ctx unchanged, equivalent to a full host scan).

func ContextWithScanOptions

func ContextWithScanOptions(ctx context.Context, opts AccountScanOptions) context.Context

ContextWithScanOptions attaches opts to ctx so that helpers called during a scan can read the active options without threading a parameter through every call site. Use ScanOptionsFromContext to retrieve them.

func CorrelationInputOf

func CorrelationInputOf(f alert.Finding) (account string, eligible bool)

CorrelationInputOf reports how one finding enters cross-account correlation: the hosting account it resolves to, empty when none could be determined, and whether its check is an eligible input at all. It applies the same registry classification and identity rules CorrelateFindings uses, so a caller can explain or calibrate a correlation result without re-implementing them. Eligibility here is the check's class only; whether a given finding then counts also depends on its severity.

func CurrentASNLookup

func CurrentASNLookup() func(ip string) (asn uint, org string)

CurrentASNLookup returns the wired ASN resolver, or nil when none is set. Both the polling connection scan and the live BPF connection evaluator use it so bad-ASN classification behaves identically on either path.

func DerivedCorrelationChecks

func DerivedCorrelationChecks() []string

DerivedCorrelationChecks returns the names correlation itself emits, sorted. The caller owns the slice.

func DiffSensitiveWatchset

func DiffSensitiveWatchset(prev, cur map[string]SensitiveFileState, contents map[string][]byte, liveReported map[string]SensitiveFileState) []alert.Finding

DiffSensitiveWatchset compares two refresh snapshots of the watchset and returns the findings the newer one warrants. Both maps are keyed by absolute path. contents holds the bytes used for cur's content digests.

Identity is the path, never the inode. Keying on dev+inode reported every atomic rewrite (write temp, rename over) as a brand-new file, which is how /etc/passwd came to "appear" 16 times in a month on a live host. A path the previous snapshot knew about can only have changed, not appeared.

Paths that vanished produce nothing: the caller unwatches the inode, and a deletion is not evidence of the tampering this watchset exists to catch.

liveReported holds the exact states whose live-hook findings were delivered since the last refresh. A matching state is adopted without a duplicate. An appearance is still reported: the hook describes a write, not the fact that the path did not exist before.

func DirectSMTPEgressBackendEnabled

func DirectSMTPEgressBackendEnabled(cfg *config.Config, backend string) bool

func DisableFunctionsNeutralized

func DisableFunctionsNeutralized(val string) bool

DisableFunctionsNeutralized reports whether a disable_functions value fails to actually disable any dangerous function -- empty, "none", or set to junk (the `disable_functions=ByPassed By 0xNix` camouflage). A genuine hardening list names at least one exact dangerous function; substrings such as "systemd" do not disable system().

func DisabledCheckConfigNames

func DisabledCheckConfigNames() []string

DisabledCheckConfigNames returns every value top-level disabled_checks accepts: this is exactly the set splitDisabledChecks honors -- every emitted finding name (including internal ones) plus every compatibility runner ID. It is broader than DisabledCheckNames (the UI vocabulary) so POST-side validation never rejects a value an existing operator config relies on.

func DisabledCheckNames

func DisabledCheckNames() []string

DisabledCheckNames returns the sorted public finding-name vocabulary accepted by top-level disabled_checks for scheduled check execution. Runner IDs are also accepted by splitDisabledChecks for existing configs, but are not exposed in the UI.

func EmailPasswordQueueStatuses

func EmailPasswordQueueStatuses(now time.Time) map[string]queuehealth.Status

EmailPasswordQueueStatuses reads admission and KDF ownership from memory.

func EnumerateScanAccounts

func EnumerateScanAccounts(_ *config.Config) ([]string, error)

EnumerateScanAccounts returns the sorted list of cPanel account usernames eligible for a server-wide scan. Source of truth: the cPanel user registry (/var/cpanel/users) intersected with present home directories (/home/<user>). All filesystem access goes through the package osFS hook so it is fakeable.

Fallback: when /var/cpanel/users is absent (non-cPanel platform), the function falls back to enumerating /home subdirectories whose names pass name validation. This makes the function usable on generic Linux hosts.

A hard FS error reading the registry (not os.ErrNotExist) is propagated as an error. An empty registry returns ([]string{}, nil).

func EvaluateBadASNOutbound

func EvaluateBadASNOutbound(cfg *config.Config, dstIP net.IP, asn uint, asOrg string) (alert.Finding, bool)

EvaluateBadASNOutbound classifies one outbound connection's destination by autonomous system and returns a finding when the ASN is bad. It is a pure function: the caller supplies the destination IP and the ASN/org already resolved from the GeoLite2-ASN database, so the classifier has no IO and is the third leg of the host-takeover chain correlator.

Classification:

  • blocked_asns always flags (e.g. known bulletproof hosters);
  • when allowed_asns is non-empty, any ASN outside it flags (allowlist mode for hosts whose legitimate egress is confined to a few providers).

An ASN of 0 (no AS found for the IP) is skipped: classifying it would flag every destination missing from the ASN database. Private, loopback, link-local, and unspecified destinations are skipped because ASN lookup is meaningless for them.

func EvaluateConnection

func EvaluateConnection(
	cfg *config.Config,
	uid uint32,
	dstIP net.IP,
	dstPort uint16,
	localPort uint16,
	proto string,
	user string,
) (alert.Finding, bool)

EvaluateConnection returns a populated alert.Finding and true when the connection should be reported, or a zero finding and false when it should be ignored. Host-interface lookups are cached. Used by the BPF live backend (per-event) and the polling backend (per row of /proc/net/tcp[6]).

func EvaluateDirectSMTPEgress

func EvaluateDirectSMTPEgress(cfg *config.Config, in DirectSMTPEgressInput) (alert.Finding, bool)

EvaluateDirectSMTPEgress returns a populated finding when the input represents a non-MTA local process opening an outbound SMTP connection. Owner resolution uses the shared passwd cache. Detector-disabled config returns (zero, false) without inspecting the input.

func EvaluateExec

func EvaluateExec(uid uint32, pid uint32, comm, exe, parentComm string) []alert.Finding

EvaluateExec returns findings for a single execve event observed by the BPF live backend. Inputs are the (UID, PID, comm, exe, parentComm) tuple the kernel hook collects. It stamps the detection time because these findings enter the realtime bus directly. The legacy periodic checks (CheckSuspiciousProcesses, CheckFakeKernelThreads) keep using cmdline-aware detection that this function cannot replicate.

func EvaluateSensitiveFileAppearance

func EvaluateSensitiveFileAppearance(path string) (alert.Finding, bool)

EvaluateSensitiveFileAppearance returns a finding when a path no previous watchset refresh had seen shows up -- a genuinely new cron drop-in, sudoers fragment, or user crontab.

func EvaluateSensitiveFileWrite

func EvaluateSensitiveFileWrite(path string, uid, pid uint32, comm string) (alert.Finding, bool)

EvaluateSensitiveFileWrite returns a populated alert.Finding and true when the BPF live backend observed a write to a watchset path. It reads current content for content-bound self-write suppression. Returns false for paths classifySensitive does not recognise -- the BPF program already filters via its dev+inode map, but this guards against stale map entries pointing at unrelated files.

func EvaluateSensitiveFileWriteSnapshot

func EvaluateSensitiveFileWriteSnapshot(path string, uid, pid uint32, comm string, content []byte, contentKnown bool) (alert.Finding, bool)

EvaluateSensitiveFileWriteSnapshot evaluates a live write against bytes captured with the file state that will be recorded for refresh deduplication. Keeping those two observations together prevents a concurrent rename from making the finding describe or suppress different content.

func ExpandWatchset

func ExpandWatchset(root string) []string

ExpandWatchset returns the absolute paths in the watchset, with globs expanded against the given filesystem root. Non-existent paths drop silently; the next refresh picks them up once they are created. root is "/" in production and a t.TempDir in tests.

func ExtractIPFromFinding

func ExtractIPFromFinding(f alert.Finding) string

ExtractIPFromFinding extracts an IP address from a finding.

func FTPLoginFinding

func FTPLoginFinding(line string, cfg *config.Config) (alert.Finding, bool)

FTPLoginFinding builds the finding for a successful pure-ftpd login line, reporting false when the line is not a login or the client address is loopback, infrastructure, or unusable. The daemon's realtime log watcher calls it so a login seen live and the same line re-read by CheckFTPLogins carry one identity; without that the state store sees two findings and the operator gets one login reported twice. It has no brute-force history, so the escalation to ftp_login_after_bruteforce stays with the scheduled check.

func FileContentSHA256

func FileContentSHA256(path string) string

FileContentSHA256 returns the hex SHA-256 of the whole file, or "" if the file cannot be read, is not a regular file, or exceeds the size cap.

func FileIndexQueueStatuses

func FileIndexQueueStatuses(now time.Time) map[string]queuehealth.Status

FileIndexQueueStatuses reads memory without waiting for a scan or filesystem.

func FindingReverifyVersion

func FindingReverifyVersion() string

FindingReverifyVersion identifies every verifier family the startup sweep runs unattended, plus the sweep's own semantics. Including the exposure verifier forces one sweep when that family is first deployed or its fail-closed semantics change.

func FixDescription

func FixDescription(checkType, message string, filePath ...string) string

FixDescription returns a human-readable description of what the fix will do for a given check type and file path. Returns empty string if no fix is available.

func ForgetNetblockHistory

func ForgetNetblockHistory(statePath, ip string, clear func()) error

ForgetNetblockHistory serializes the operator's firewall/database mutation and history removal with auto-block cycles. clear may be nil for history-only cleanup; it must not call another auto-block state operation.

func FormatCleanResult

func FormatCleanResult(r CleanResult) string

FormatCleanResult returns a human-readable summary of a clean operation.

func FormatDBCleanResult

func FormatDBCleanResult(r DBCleanResult) string

FormatDBCleanResult formats a DBCleanResult for terminal output.

func FullScanMaxFileBytes

func FullScanMaxFileBytes(cfg *config.Config) int64

FullScanMaxFileBytes converts cfg.Thresholds.FullScanMaxFileMB to a byte limit for per-file content reads during a full-scan job. A configured value of 0, negative, or above the validated maximum falls back to 16 MiB so the caller never gets an unconstrained or overflowed limit from a bad field.

func GetScanHomeDirs

func GetScanHomeDirs(ctx context.Context) ([]os.DirEntry, error)

GetScanHomeDirs returns the list of home directories to scan. When ctx carries an account scope (via ContextWithAccountScope), only that account is returned. Otherwise every entry under every account root is read. Nil ctx is tolerated for legacy callers and treated as host-wide. Callers that need the directory path use scanHomeDirPath on each entry.

func HasFix

func HasFix(checkType string) bool

HasFix returns true if the check type has a known automated fix.

func HashFile

func HashFile(path string) string

HashFile computes the SHA256 hash of a file. Returns empty string on error.

func HostingAccountForUser

func HostingAccountForUser(name string) string

HostingAccountForUser returns name when it is a hosting account: its passwd home directory sits directly under a configured account root. Root, system and service users, unknown names and the lookup sentinel resolve to "". The passwd cache is the same one process findings use for uid resolution, so tests point both at one fixture file.

func IPResponseAnswersFinding

func IPResponseAnswersFinding(cfg *config.Config, f alert.Finding, blocked bool) bool

IPResponseAnswersFinding is the alert.IPResponsePolicy for suppress_blocked_alerts. A block or challenge on the source address answers only attacker-side findings: attacker activity or attempted access that is not evidence of compromise. Once the source is stopped the operator has nothing left to do. Compromise evidence, successful logins and audit events stay visible even when their source address is already blocked.

A challenge answers only the findings challenge routing would send to the gate. The address being on the challenge list is itself proof the gate is in use, so the policy does not consult challenge.enabled.

func InitAutoBlockQueueHealth

func InitAutoBlockQueueHealth(statePath string) error

InitAutoBlockQueueHealth observes existing retries before daemon consumers start, including when automatic blocking is disabled. It never changes state.

func InlineQuarantine

func InlineQuarantine(f alert.Finding, path string, data []byte) (string, bool)

InlineQuarantine moves a file to quarantine immediately if it passes the high-confidence validation gates. Called from fanotify's analyzeFile to quarantine malware without waiting for the 5-second batch dispatcher. The data parameter is the file content already read by the caller (avoids TOCTOU re-read). Pass nil to read from path. Returns the quarantine path and true if the file was quarantined.

func InlineQuarantineGated

func InlineQuarantineGated(cfg *config.Config, f alert.Finding, path string, data []byte) (string, bool)

InlineQuarantineGated applies the operator's quarantine policy before InlineQuarantine moves anything. The realtime fanotify path detects malware continuously, but moving a file is a customer-impacting auto-response action: it must honor the same master switch and quarantine opt-in as the batch AutoQuarantineFiles dispatcher, never act on detection alone. An operator in monitor mode (auto-response off, or quarantine_files off) gets the alert without having files moved out from under them.

func InlineQuarantineGatedIdentified

func InlineQuarantineGatedIdentified(cfg *config.Config, f *alert.Finding, path string, data []byte, scanned os.FileInfo) (string, bool, *alert.Finding)

InlineQuarantineGatedIdentified applies the auto-response policy gate and then quarantines the exact file the caller scanned. See InlineQuarantineIdentified for why the identity matters. Marks the finding evaluated only once it reaches the shared budget gate.

func InlineQuarantineIdentified

func InlineQuarantineIdentified(f alert.Finding, path string, data []byte, scanned os.FileInfo) (string, bool)

InlineQuarantineIdentified is InlineQuarantine with the identity of the file the caller actually scanned. The realtime scanner reads content from the fanotify event descriptor, so passing that descriptor's stat pins the move to the object that was examined: a file replaced between detection and quarantine fails the identity check instead of being moved in place of the malware. A nil identity keeps the older path-based behaviour for callers that began from a path in the first place, such as the batch dispatcher.

func IsBenignPHPStub

func IsBenignPHPStub(path string) bool

IsBenignPHPStub reports whether the reachable code region of a PHP file consists only of whitespace and comments, or terminates with a literal-argument die / exit, or __halt_compiler before any other statement. Files matching either shape cannot execute attacker-controlled code via a web request: PHP either runs to EOF emitting nothing, or hits the terminator and stops with the remaining bytes unreachable.

The recogniser is content-shape only -- it does not look at the path, filename, parent directory, or whether a plugin is installed. An attacker cannot bypass it by naming a payload to mimic a known-plugin file because the gate fails the moment any executable statement appears before a terminator. Conversely a legitimate plugin that writes a stub-shaped working file (BackWPup writes "<?php //<json>" for job state and "<?php\n//path1\n//path2..." for folder caches) is recognised regardless of where it puts the file.

Other detectors -- signature scans, YARA, suspicious filename, the webshell name list -- still run on the file in their own pipelines. Only the path-only "anomalous PHP location" warning is suppressed for files that this recogniser accepts.

func IsBenignPHPStubBytes

func IsBenignPHPStubBytes(buf []byte) bool

IsBenignPHPStubBytes is the buffer-only variant. The realtime fanotify path uses it on the bytes it already read from the file descriptor; IsBenignPHPStub provides the path-based entry point for the polled fileindex scan. Both rely on the same parser so realtime and scheduled scans agree on which files are stubs.

The parser tokenises the leading region of the buffer:

  • Optional UTF-8 BOM and whitespace, then the literal "<?php" opener. The short-echo opener "<?=" is rejected because it emits output. A "<?phpfoo" run-together opener is rejected because PHP requires whitespace (or EOF) after the tag.
  • Repeatedly accept whitespace, line comments ("//..." or "#..." up to newline or "?>"), and balanced block comments ("/* ... */"). A "/*" without a matching "*/" inside the scanned window is rejected -- we cannot prove the rest of the file is comment.
  • Accept die, exit, and __halt_compiler as terminators. die and exit may carry a single literal argument (a non-interpolating string or a decimal integer); __halt_compiler takes none. Once seen, the rest of the buffer is treated as unreachable.
  • Reject any closing "?>" tag (would allow HTML escape and a later "<?php" re-entry that this gate does not analyse).
  • Reject any other identifier (return, if, system, eval, function, class, ...) and any stray punctuation ("$", "(", "=", ";", ...). Those are statements we cannot prove benign.
  • If the loop reaches EOF in a complete buffer having only seen whitespace and comments, accept: PHP outputs nothing and executes nothing.

func IsBenignPHPStubBytesComplete

func IsBenignPHPStubBytesComplete(buf []byte, complete bool) bool

IsBenignPHPStubBytesComplete is like IsBenignPHPStubBytes, but complete tells the parser whether buf contains the entire file. Comment-only stubs require a complete buffer; terminators do not, because bytes after them are unreachable to PHP.

func IsCloudflareIP

func IsCloudflareIP(ip net.IP) bool

IsCloudflareIP reports whether ip is inside the published Cloudflare ranges the daemon last refreshed.

func IsContentReverifiable

func IsContentReverifiable(check string) bool

IsContentReverifiable reports whether a check type is re-evaluated by re-running the content classifier (vs presence-only).

func IsDBObjectKind

func IsDBObjectKind(s string) bool

IsDBObjectKind reports whether s is one of the four valid kinds. Used by the CLI subcommand to validate user input before opening a connection.

func IsDerivedCorrelationCheck

func IsDerivedCorrelationCheck(name string) bool

IsDerivedCorrelationCheck reports whether name is an output of correlation.

func IsExternalDest

func IsExternalDest(dest string, localDomains map[string]bool) bool

IsExternalDest returns true if the destination is an email address with a domain not in the local domains set. A pipe is a command, even when its arguments contain an address.

func IsInfraIP

func IsInfraIP(ip string, infraNets []string) bool

IsInfraIP reports whether ip is infrastructure the daemon must never act against: an operator infra entry (CIDR or address) or a Cloudflare edge. One implementation serves scans and the realtime path alike.

func IsKnownForwarder

func IsKnownForwarder(localPart, domain, dest string, knownForwarders []string) bool

IsKnownForwarder checks if a forwarder rule matches the known forwarders suppression list.

func IsPipeForwarder

func IsPipeForwarder(dest string) bool

IsPipeForwarder returns true if the destination is a pipe forwarder, excluding pipes whose executed command is a cPanel built-in.

func IsVerifiedCMSFile

func IsVerifiedCMSFile(path string) bool

IsVerifiedCMSFile checks if a file at the given path matches a known-clean CMS core file by comparing its SHA256 hash against the cache.

The cache is keyed by SHA256 hash alone (not path+hash) - this is correct:

  • SHA256 preimage resistance makes it computationally infeasible for an attacker to craft a file that produces the same hash as a legitimate WP core file. Birthday attacks do not apply here because the attacker must hit a specific pre-existing hash, not merely find any collision.
  • If file content matches a known WP core file byte-for-byte, it IS that file regardless of where it is located on disk. The path is irrelevant to whether the content is clean.

func IsVerifiedCMSHash

func IsVerifiedCMSHash(hash string) bool

IsVerifiedCMSHash reports whether a content hash belongs to a verified CMS core file. Callers that already hold the content -- the realtime scanner hashes the event descriptor -- use this instead of IsVerifiedCMSFile, whose re-read resolves the path again and can hash something other than what was examined.

func IsWPTranslationCacheBytesComplete

func IsWPTranslationCacheBytesComplete(buf []byte, complete bool) bool

IsWPTranslationCacheBytesComplete reports whether buf is exactly a WordPress PHP translation cache: the "<?php" opener, the keyword "return", a single PHP array literal whose elements are only string/integer scalars (optionally concatenated string literals, as GlotPress joins plural forms with a "\0" separator) or nested arrays of the same, then a ";" and nothing else. WordPress 6.5+ auto-generates these as pure data return maps (*.l10n.php); each one previously opened a sensitive-dir Warning incident.

This is a content-structure recognizer, not a path or filename allowlist. A variable, a function call, string interpolation, a concatenation operand that is not a literal, a closing "?>" tag, or any statement after the array makes it return false, so an attacker cannot smuggle code into a file shaped like a translation cache. complete must be true: a truncated buffer cannot prove the unseen tail carries no code, so it is never suppressed.

func IsWPVersionDataBytesComplete

func IsWPVersionDataBytesComplete(buf []byte, complete bool) bool

IsWPVersionDataBytesComplete recognizes only literal assignments to the variables WordPress reads from its version file. Matching an installed copy alone cannot establish that a short-lived version probe carried no payload.

func LatestPurgeCheckNamesForReducedDeep

func LatestPurgeCheckNamesForReducedDeep() []string

LatestPurgeCheckNamesForReducedDeep returns the emitted finding names owned by the reduced deep set used while fanotify covers filesystem events.

func LatestPurgeCheckNamesForTier

func LatestPurgeCheckNamesForTier(tier Tier) []string

LatestPurgeCheckNamesForTier returns every emitted finding name owned by a tier. The daemon uses this to replace a tier's current scan output without retaining stale findings from prior runs.

func LooksLikeCpanelRestoreStaging

func LooksLikeCpanelRestoreStaging(path string) bool

LooksLikeCpanelRestoreStaging recognises files inside cPanel's pkgacct/restorepkg staging tree. cPanel extracts the user backup as root into /home/cpanelpkgrestore.TMP.work.<id>/ for inspection, then re-extracts it under the user identity into /home/<account>/. Both extractions raise events; the user-context one carries the real signal, so the staging-side alert is a duplicate.

The recogniser requires the marker to sit directly under /home (the only place cPanel ever creates it) plus a non-empty alphanumeric id of >=2 chars. A user account at /home/<user>/ cannot create siblings of itself, so this gate cannot be spoofed by a non-root attacker.

func LooksLikeWPOptimizeProbeByPath

func LooksLikeWPOptimizeProbeByPath(path string) bool

LooksLikeWPOptimizeProbeByPath recognises WP-Optimize's per-server probe files using path structure alone. WP-Optimize writes tiny <?php files to /wp-content/uploads/wpo/.../test.php to test whether the host honours certain Apache/Nginx directives.

Path-only gates (no content read):

  1. Path lies under /wp-content/uploads/wpo/.
  2. The basename is exactly "test.php" (the literal filename WP-Optimize uses for these probes; an attacker dropping /uploads/wpo/webshell.php fails this gate and continues to the standard alert).
  3. The wp-optimize plugin directory is actually present in this site's wp-content/plugins/ tree (filesystem stat).

The realtime path additionally applies a content shape gate before suppressing the duplicate alert, so the path predicate here stays narrow to the literal probe filename and installed plugin directory.

func LookupUID

func LookupUID(account string) int

LookupUID returns the UID for a system account name, or -1 if not found.

func LookupUser

func LookupUser(uid uint32) string

LookupUser returns the username for uid, or "uid:<n>" if not resolvable. Safe for concurrent use; the underlying cache is shared across the daemon.

func MailOwner

func MailOwner(mailboxOrDomain string) string

MailOwner returns the hosting account for a mailbox or bare domain, or "" when the mapping is unavailable. It never returns the mailbox or domain itself as an owner.

func MatchCrontabPatternsDeep

func MatchCrontabPatternsDeep(content string, cfg *config.Config) []string

MatchCrontabPatternsDeep is matchCrontabPatterns plus a single base64 decode pass: it pulls out base64 candidates from content and re-runs pattern matching on the decoded bytes. Catches attackers who wrap the `base64 -d|bash` pipe chain in an outer base64 layer so the literal markers never appear in the cron file as written. Single decode depth; no recursion. cfg nil uses the built-in defaults; pass the live operator config to honour `thresholds.crontab_base64_blob_max_bytes`.

func MergeModSecUserConfSection

func MergeModSecUserConfSection(existing, srcData []byte) (merged []byte, changed bool)

MergeModSecUserConfSection merges CSM's ModSecurity rules payload into the current contents of a modsec2.user.conf, confining CSM to its marker-delimited section so operator-maintained rules outside the section survive every deploy. It is exported because three call sites write this file (the WAF check cycle here, `csm install`, and the daemon startup config deploy); routing them all through one merge guarantees no caller ever whole-file-overwrites operator rules.

existing is the current file contents (nil for a missing file). merged is only meaningful when changed is true; changed=false means the file already carries the wanted section and must not be rewritten.

func MigrateWPCronCrontabs

func MigrateWPCronCrontabs(cfg *config.Config) int

MigrateWPCronCrontabs upgrades CSM-managed wp-cron crontab lines installed by older releases (synchronized */N schedule, no overlap lock) to the current staggered format. The perf_wp_cron finding never re-fires once DISABLE_WP_CRON is set, so already-fixed accounts can only be reached by walking the spool directly. Gated on the same fix_wp_cron opt-in as the install path; returns the number of crontabs rewritten.

func NextSensitiveDigests

func NextSensitiveDigests(prev map[string]SensitiveFileState, paths []string) (map[string]SensitiveFileState, map[string][]byte)

NextSensitiveDigests builds the state snapshot for a refresh cycle and returns the exact readable regular-file content behind each digest. A path whose content cannot be read keeps its previous digest so a transient read error does not surface as a content change. Non-regular objects retain type identity without being read. Paths absent from paths drop out. prev is only read and is never mutated.

func ObserveOperatorBlock

func ObserveOperatorBlock(err error, source string)

ObserveOperatorBlock reports an operator-initiated force block (CLI or web UI) into the outcome metric. Force blocks bypass the dry-run gate, so a nil error means the block landed live.

func PHPConfigRealtimeRootPatterns

func PHPConfigRealtimeRootPatterns(cfg *config.Config) []string

PHPConfigRealtimeRootPatterns returns the startup-time path patterns used to admit php.ini writes into the fanotify analyzer. Explicit account_roots are authoritative. On cPanel, account homes are intentionally broader than the primary public_html root because addon domains can live anywhere below an account and on alternate home mounts.

func PHPConfigSecurityBypasses

func PHPConfigSecurityBypasses(content string) []string

PHPConfigSecurityBypasses returns the strong, low-false-positive signals that a PHP ini file weakens security: disable_functions cleared or neutralized, allow_url_include enabled, or open_basedir removed. Used for newly-seen files, where the noisier per-function diffing of analyzePHPINI would flag benign partial disable lists.

func PHPTerminatesImmediately

func PHPTerminatesImmediately(buf []byte) bool

PHPTerminatesImmediately recognizes a leading exit, die, or __halt_compiler in unencoded PHP source, with at most one plain literal argument. Callers must establish that PHP source conversion is disabled before using this to suppress findings: conversion can remove even a raw terminator keyword. Only the opening tag and whitespace may precede it. Unlike the broader stub parser, it never accepts comments before the terminator. A completed terminator can be recognized from a partial head; EOF alone is not proof.

func PHPTerminatesImmediatelyAt

func PHPTerminatesImmediatelyAt(buf []byte) (int, bool)

PHPTerminatesImmediatelyAt is PHPTerminatesImmediately plus the offset just past the terminator statement, so a caller can judge the unreachable tail.

func PathMatchesIgnore

func PathMatchesIgnore(path string, ignores []string) bool

matchGlob reports whether path is covered by an operator suppression pattern.

Matching is tried in order:

  1. filepath.Match against the basename ("*.php", "*.log") and the full path.
  2. For a leading-any-depth glob pattern, a substring match of the wildcard-stripped residue -- but ONLY when that residue still contains a path separator with literal content (e.g. "*/node_modules/*" -> "/node_modules/"). This preserves the "directory anywhere in the path, at any depth" intent without broadening anchored full-path globs like "/tmp/safe/*" into recursive subtree suppressions.
  3. For a pattern with no wildcards, a literal substring match, so an operator can suppress a directory ("/uploads/") or a filename ("adminer.php").

The separator requirement in step 2 is the fix for an over-suppression footgun: the previous code stripped every "*" and substring-matched the remainder, so "*.php" became the bare token ".php" and silenced every file whose path merely contained ".php" -- turning a narrow pattern into a whole-subtree allowlist an attacker could hide a webshell in. PathMatchesIgnore reports whether path is covered by any of the operator's suppressions.ignore_paths patterns, using the same glob semantics the content checks apply. Exported so the real-time watchers honour the same suppression list instead of maintaining a second interpretation of it.

func PerfCheckNamesForTier

func PerfCheckNamesForTier(tier Tier) []string

PerfCheckNamesForTier returns the perf_* check names registered in the given tier. Used by the daemon to perform an atomic purge-and-merge when storing findings.

func PkgManagerRecentlyActive

func PkgManagerRecentlyActive(now time.Time) bool

PkgManagerRecentlyActive reports whether any package-manager log was modified within the provenance demotion window. Exported for the fanotify /tmp-executable demotion, which gates on the same evidence as rescoreSensitive.

func PluginInventoryQueueStatus

func PluginInventoryQueueStatus(now time.Time) queuehealth.Status

PluginInventoryQueueStatus includes sites waiting for a worker and inventories still executing or committing their result. Concurrent refreshes share no cap.

func PruneExemptAutoSubnets

func PruneExemptAutoSubnets(cfg *config.Config, b IPBlocker) int

PruneExemptAutoSubnets removes auto-response subnet blocks whose CIDR now intersects the DoS-exempt set (operator ranges or mail-provider overlay). Only entries with Source == firewall.SourceAutoResponse are touched; manual, CLI, web-UI, challenge, whitelist, dyndns, system, and unknown-source blocks are left untouched. If b does not implement subnetManager, returns 0. UnblockSubnet errors are logged and the entry is not counted as pruned.

func QuoteIdent

func QuoteIdent(name string) (string, error)

QuoteIdent returns a backtick-quoted MySQL identifier, or an error if the input is empty, longer than 64 bytes, or contains characters outside the safe class. Used at every site where an attacker- controlled object name (trigger / event / routine / schema) would otherwise reach a SQL string concatenation.

The safe class is intentionally narrow: standard MySQL allows more (digits-only names, dotted names, $-prefixed) but the cleaner only needs to handle CMS-shaped identifiers and operator-typed schema names. Rejecting anything weirder is cheaper than reasoning about edge cases in dynamic SQL.

func RealtimeDocumentRootPatterns

func RealtimeDocumentRootPatterns(cfg *config.Config) []string

RealtimeDocumentRootPatterns returns the served roots used by realtime content detectors. cPanel's domain map is authoritative for addon domains and accounts on alternate home mounts; the conventional public_html glob remains the fallback when that map is unavailable.

func RecordSelfWrite

func RecordSelfWrite(path string, content []byte)

RecordSelfWrite registers that CSM remediation just wrote content to a sensitive watched file. A current file is also recorded durably with its identity; a provisional record made before the write stays memory-only.

func RecordUnattributedActiveSet

func RecordUnattributedActiveSet(counts map[string]int)

RecordUnattributedActiveSet records the unattributed rows of the latest-state active set after a merge. Same locking contract as ReportUnattributedCorrelation.

func RegisterDirectSMTPEgressMetrics

func RegisterDirectSMTPEgressMetrics(reg *metrics.Registry)

RegisterDirectSMTPEgressMetrics binds the per-finding counter to reg. Production callers should pass metrics.Default(); tests pass metrics.NewRegistry() to keep registration isolated.

func RemoveAFAlgSeccompDropIns

func RemoveAFAlgSeccompDropIns() ([]string, error)

RemoveAFAlgSeccompDropIns deletes every CSM-managed seccomp drop-in found on disk and runs systemctl daemon-reload + reload-or-restart per touched unit so the seccomp filter is dropped from running processes. Idempotent: a unit without our drop-in is skipped.

Returns the list of units whose drop-in was removed.

func RemoveModSecUserConfSections

func RemoveModSecUserConfSections(existing []byte) (cleaned []byte, changed bool)

RemoveModSecUserConfSections removes only content owned by CSM from the shared ModSecurity user configuration. Operator bytes outside the exact CSM marker lines are preserved.

func ReportUnattributedCorrelation

func ReportUnattributedCorrelation(counts map[string]int)

ReportUnattributedCorrelation records unattributed rows from a per-batch derivation through the process-wide reporter. Callers invoke it after releasing any state store lock; it never re-enters the store.

func ReputationQueueStatus

func ReputationQueueStatus(now time.Time) queuehealth.Status

ReputationQueueStatus reads memory only, including queries outliving a check.

func ResetAttributionHealthForTest

func ResetAttributionHealthForTest()

ResetAttributionHealthForTest replaces the process-wide reporter with a fresh one. Test-only.

func ResolveWPCronRoots

func ResolveWPCronRoots(cfg *config.Config) []string

ResolveWPCronRoots returns the same validated roots used by CheckWPCron. Remediation callers use it so a finding from an addon domain remains inside the exact root authorized by cPanel's map.

func ResolveWebRoots

func ResolveWebRoots(cfg *config.Config) []string

ResolveWebRoots returns the list of directory paths CSM should scan for web-facing content (wp-config.php, .htaccess, public_html trees, etc.).

Resolution order:

  1. If cfg.AccountRoots is set, expand each glob and return the result. Explicit config always wins.
  2. On cPanel hosts (detected via platform.Detect), fall back to /home/*/public_html for backward compatibility.
  3. On non-cPanel hosts with no config, return an empty list. Callers should treat this as "no scanning" and skip cleanly.

Each returned path is an absolute directory that exists on disk.

func RestoreVirtualPatchBackup

func RestoreVirtualPatchBackup(backupPath string, target *safepath.Target, meta QuarantineMeta) error

RestoreVirtualPatchBackup reverts only the captured patch, using the pinned destination directory so tenant renames cannot redirect reads or changes.

func ReverifyStaleFindingsStats

func ReverifyStaleFindingsStats(ctx context.Context, store LatestFindingStore) ([]ContentReverifyDismissal, ReverifySweepStats, bool)

ReverifyStaleFindingsStats is ReverifyStaleFindingsContext with a summary of everything the sweep looked at, including the findings it could not check.

func RunAccountScan

func RunAccountScan(cfg *config.Config, store *state.Store, account string) []alert.Finding

RunAccountScan runs all applicable checks scoped to a single cPanel account. Returns findings for that account only. Does NOT trigger auto-response actions.

This is a thin wrapper around RunAccountScanWithOptions using DefaultAccountScanOptions so all existing callers retain their current behaviour.

func RunAccountScanWithOptions

func RunAccountScanWithOptions(ctx context.Context, cfg *config.Config, store *state.Store, account string, opts AccountScanOptions) []alert.Finding

RunAccountScanWithOptions is the options-aware entry point for per-account scans. Scope is propagated through ctx via ContextWithAccountScope, so parallel scans of different accounts no longer block on a single process-wide mutex and never bleed scope into each other.

func RunAll

func RunAll(cfg *config.Config, store *state.Store) ([]alert.Finding, []string)

RunAll runs critical checks always. Deep checks run if throttle allows or ForceAll is set. The second return value is the per-scan purge name list scoped to the checks that actually executed this cycle.

func RunAllDryRun

func RunAllDryRun(cfg *config.Config, store *state.Store) ([]alert.Finding, []string)

RunAllDryRun is the dry-run variant of RunAll for `csm baseline`.

func RunHardeningAudit

func RunHardeningAudit(cfg *config.Config) *store.AuditReport

RunHardeningAudit runs all hardening checks and returns a report. Pure function — reads system state only, no store access.

func RunReducedDeep

func RunReducedDeep(cfg *config.Config, store *state.Store) ([]alert.Finding, []string)

RunReducedDeep runs only the deep checks that fanotify can't replace. Used by the daemon when fanotify is active.

Filesystem and content scans remain scheduled because fanotify misses renames, permission changes and files planted before daemon startup.

The second return value is the per-scan purge name list scoped to the checks that actually executed this cycle.

func RunReducedDeepWithContext

func RunReducedDeepWithContext(ctx context.Context, cfg *config.Config, store *state.Store) ([]alert.Finding, []string)

RunReducedDeepWithContext is RunReducedDeep with a caller-owned parent context for daemon shutdown cancellation.

func RunTier

func RunTier(cfg *config.Config, store *state.Store, tier Tier) ([]alert.Finding, []string)

RunTier runs only the specified tier of checks. The second return value is the per-scan purge name list (emitted finding aliases owned by the checks that actually executed this cycle); pass it to StoreLatestScanFindings so throttled-out checks keep their prior findings.

Passes the requested dry-run state into runParallel via a scoped parameter rather than the previous package-level toggle, so concurrent periodic scanners no longer race with a manual `csm check` invocation.

func RunTierDryRun

func RunTierDryRun(cfg *config.Config, store *state.Store, tier Tier) ([]alert.Finding, []string)

RunTierDryRun is the dry-run variant of RunTier: auto-response actions are skipped. Used by `csm check*` socket commands and the legacy CLI.

func RunTierDryRunWithContext

func RunTierDryRunWithContext(ctx context.Context, cfg *config.Config, store *state.Store, tier Tier) ([]alert.Finding, []string)

RunTierDryRunWithContext is RunTierDryRun with a caller-owned parent context. Control-socket scans use it to collect coverage gaps without enabling auto-response actions.

func RunTierWithContext

func RunTierWithContext(ctx context.Context, cfg *config.Config, store *state.Store, tier Tier) ([]alert.Finding, []string)

RunTierWithContext is RunTier with a caller-owned parent context. Daemon periodic scans pass their shutdown context here so an interrupted scan does not stall process exit.

func SSHAcceptedLoginFinding

func SSHAcceptedLoginFinding(line string, cfg *config.Config) (alert.Finding, bool)

SSHAcceptedLoginFinding parses an sshd "Accepted <method> for <user> from <ip> port <n>" line and reports it unless the address is infrastructure. The daemon's realtime log watcher calls it so a login seen live and the same line re-read by CheckSSHLogins carry one identity; without that the state store sees two findings and the operator gets one login reported twice.

func SetASNLookup

func SetASNLookup(fn func(ip string) (asn uint, org string))

SetASNLookup wires the GeoLite2-ASN resolver used by the outbound connection scan. Passing nil clears it.

func SetAccountOwnerLookupForTest

func SetAccountOwnerLookupForTest(fn func(domain string) (string, bool)) func()

SetAccountOwnerLookupForTest replaces the domain-to-owner mapping and returns a function that restores it. Test-only seam for packages that cannot reach this package's filesystem fakes.

func SetBotVerifier

func SetBotVerifier(v *threatintel.AsyncBotVerifier, get func(net.IP, string) (bool, bool))

SetBotVerifier installs the daemon-lifetime async verifier and cache reader. Called from daemon.go after the store and goroutine are ready.

func SetChallengeIPList

func SetChallengeIPList(list ChallengeIPList)

SetChallengeIPList sets the challenge IP list for routing.

func SetCloudflareNets

func SetCloudflareNets(cidrs []string)

SetCloudflareNets updates the cached Cloudflare IP ranges. Called by the daemon after fetching CF IPs.

func SetCmdRunner

func SetCmdRunner(r CmdRunner)

SetCmdRunner replaces the command runner. Used by tests to inject mocks.

func SetGlobalThreatDBForTest

func SetGlobalThreatDBForTest(statePath string) func()

SetGlobalThreatDBForTest installs a freshly-constructed threat DB rooted at statePath, bypassing the once-guard, and returns a function that restores the previous global. For tests only: lets a test exercise the threat-DB path without permanently polluting the global for order-dependent tests.

func SetHostingAccountLookupForTest

func SetHostingAccountLookupForTest(fn func(name string) string) func()

SetHostingAccountLookupForTest replaces the user-to-account mapping and returns a function that restores it. Test-only seam for packages that cannot reach this package's passwd and account-root fakes.

func SetIPBlocker

func SetIPBlocker(b IPBlocker)

SetIPBlocker installs the firewall engine for auto-blocking. Safe to call concurrently with AutoBlockIPs: each call publishes the new blocker atomically and any in-flight scan keeps the snapshot it already loaded.

func SetOS

func SetOS(o OS)

SetOS replaces the filesystem provider. Used by tests to inject mocks.

func SetPHPTaintAnalyzer

func SetPHPTaintAnalyzer(a PHPTaintAnalyzer)

SetPHPTaintAnalyzer installs the supervised worker. Passing nil removes it, after which every candidate is reported as an unexamined coverage gap rather than analyzed in this process.

func SetSelfWriteStore

func SetSelfWriteStore(st *state.Store)

SetSelfWriteStore gives the self-write ledger somewhere durable to record what CSM wrote. Without it, a daemon restart -- or a crontab the cPanel wrapper reformats after CSM hands it over -- makes CSM's own write look like a third-party change to the sensitive-file detectors.

func SetWebProbe

func SetWebProbe(p webProbe)

SetWebProbe replaces the reachability prober. Test-only seam.

func ShouldCleanInsteadOfQuarantine

func ShouldCleanInsteadOfQuarantine(path string) bool

ShouldCleanInsteadOfQuarantine returns true if the file should be cleaned (surgical removal) instead of quarantined (full removal). WP core files and plugin files are better cleaned - removing them breaks the site. Unknown standalone files (droppers, webshells) should be quarantined.

func ShouldDemoteSeverity

func ShouldDemoteSeverity(f alert.Finding, res VerifyResult) bool

ShouldDemoteSeverity reports whether a verdict retires a remediated but unproven finding from the live queue. It is never a clear: an attacker must not retire a finding by editing the file. Demoting an already-Warning finding would be churn.

The unattended sweep and the operator's Re-check both ask this, so the two cannot disagree about what a verdict means.

func ShouldRestoreSeverity

func ShouldRestoreSeverity(f alert.Finding, res VerifyResult) bool

ShouldRestoreSeverity reports whether an automatic demotion must be reversed. A demotion holds only while the replacement keeps satisfying the inert-content gate. Restore on a positive match and on every uncertain or newly-active shape alike; otherwise a second edit into a detection gap would leave live malware at Warning.

func StampContentFingerprint

func StampContentFingerprint(f *alert.Finding)

StampContentFingerprint records the detection-time content fingerprint on a content-reverifiable finding so the Re-check / sweep can later distinguish a superseded-heuristic false positive from a file edited after detection. A producer that analyzed an already-open snapshot may supply its exact hash; retain that fingerprint instead of reopening a path that may now name different content. No-op for non-content findings or findings without a path.

func StoreLatestScanFindings

func StoreLatestScanFindings(st *state.Store, purgeChecks []string, findings []alert.Finding)

StoreLatestScanFindings replaces the latest findings owned by a scan, then rebuilds derived correlation findings from the merged current set. One-shot auto-response actions stay in history and alerts, not the active findings view.

func StoreLatestScanFindingsWithCoverage

func StoreLatestScanFindingsWithCoverage(st *state.Store, purgeChecks []string, findings []alert.Finding, coverage *state.ScanCoverage)

StoreLatestScanFindingsWithCoverage retires only completed checks or scopes and preserves current findings for file gaps in the same atomic operation.

func StoreLatestScanFindingsWithGaps

func StoreLatestScanFindingsWithGaps(st *state.Store, purgeChecks []string, findings []alert.Finding, gapPaths map[string]map[string]bool)

StoreLatestScanFindingsWithGaps preserves the latest state for files a completed scan could not examine while replacing its covered state. gapPaths contains the lexical and resolved aliases captured when each gap occurred; the state store applies that frozen set under the same lock as the purge.

func SwapUIDCacheForTest

func SwapUIDCacheForTest(path string) func()

SwapUIDCacheForTest is the exported form of swapDefaultUIDCacheForTest for packages whose producers resolve uids through LookupUser.

func VirtualPatchExposedFindings

func VirtualPatchExposedFindings(_ *config.Config, findings []alert.Finding, apply bool) []alert.Finding

VirtualPatchExposedFindings applies (apply=true) or previews (apply=false) a deny rule for each virtual-patchable web_exposed_* finding, deduplicated by path. It returns one auto_response action finding per file.

func WPCoreQueueStatus

func WPCoreQueueStatus(now time.Time) queuehealth.Status

WPCoreQueueStatus includes selected installations through checksum execution, result collection and verified-file caching. Concurrent scans share no cap.

func WebRootPatterns

func WebRootPatterns(cfg *config.Config) []string

WebRootPatterns returns the configured web-root globs, including the platform default when the operator did not set account_roots.

func WithModSecReload

func WithModSecReload(ctx context.Context, r *ModSecReloadReconciler) context.Context

WithModSecReload lets daemon scans share their startup reconciler. Contexts without one can still deploy rules, but leave activation to the daemon.

func WriteAFAlgMarker

func WriteAFAlgMarker() error

WriteAFAlgMarker forces the canonical marker content to disk regardless of current state. Used by `csm harden --copy-fail` to ensure subsequent EnforceAFAlgBlocked() runs see a valid marker even on first install.

Types

type AFAlgEvent

type AFAlgEvent = afAlgEvent

AFAlgEvent is the package-public view of afAlgEvent used by callers outside internal/checks (the daemon's live audit-log listener emits findings derived from this shape).

func ParseAFAlgEventLine

func ParseAFAlgEventLine(line string) (AFAlgEvent, bool)

ParseAFAlgEventLine is the exported alias of parseAFAlgEvent for the daemon's live listener. The unexported form stays internal so the rest of this package can refer to the type by its short name.

type AFAlgKernelState

type AFAlgKernelState struct {
	BuiltIn         bool // CONFIG_CRYPTO_USER_API_AEAD=y in the running kernel
	Modular         bool // CONFIG_CRYPTO_USER_API_AEAD=m (loadable module exists)
	ConfigReadable  bool // /boot/config-$(uname -r) or /proc/config.gz was parseable
	LivepatchActive bool // KernelCare/kpatch has applied a CVE-2026-31431 patch
}

AFAlgKernelState is the assembled view of how the running kernel exposes AF_ALG. Used by the hardening audit, by csm harden, and by the live-monitor coordinator to decide whether protection is needed.

func ObserveAFAlgKernelState

func ObserveAFAlgKernelState() AFAlgKernelState

ObserveAFAlgKernelState is the exported alias for cmd/csm and the daemon. Production code inside this package uses the unexported form directly.

func (AFAlgKernelState) IsCopyFailExploitable

func (s AFAlgKernelState) IsCopyFailExploitable() bool

IsCopyFailExploitable reports whether this kernel is currently vulnerable to Copy Fail (CVE-2026-31431). Used by the daemon's live-monitor coordinator to skip starting the listener entirely on hosts that don't need protection — saving the inotify watch + tick loop for hosts that actually face the threat.

Conservative defaults: when the kernel config is unreadable, treat the host as exploitable (better to over-monitor than miss). When a KernelCare livepatch is in place, treat as patched regardless of the underlying config — the syscall path itself is fixed.

func (AFAlgKernelState) String

func (s AFAlgKernelState) String() string

String renders the kernel state for inclusion in operator-visible messages. The format is stable and short enough to embed in a single AuditResult.Message line.

type AccountScanOptions

type AccountScanOptions struct {
	MaxFiles       int   // 0 = uncapped path ranking
	ForceContent   bool  // true = bypass clean-file content caches
	ForceFileIndex bool  // true = bypass file-index dir mtime cache, do not write live index
	RespectIgnores bool  // false = also scan suppressions.ignore_paths
	MaxFileBytes   int64 // 0 = use each check's existing per-file limit
}

AccountScanOptions controls how RunAccountScanWithOptions enumerates and content-scans an account. The zero value is NOT the default: MaxFiles 0 means uncapped. Callers use DefaultAccountScanOptions for normal behaviour.

func DefaultAccountScanOptions

func DefaultAccountScanOptions(cfg *config.Config) AccountScanOptions

DefaultAccountScanOptions returns the options that reproduce today's RunAccountScan behaviour. All callers that want the existing cap and cache semantics should use this rather than constructing AccountScanOptions directly.

func FullScanOptions

func FullScanOptions(cfg *config.Config, respectIgnores bool) AccountScanOptions

FullScanOptions builds the canonical option set for an uncapped full-scan audit job: no file cap, force content + file-index (bypass the clean-file and directory-mtime caches), and the configured per-file byte ceiling. respectIgnores is the only caller-chosen knob. Centralised here so the control handler and the WebUI enqueue path cannot drift on this security-relevant set.

func ScanOptionsFromContext

func ScanOptionsFromContext(ctx context.Context) (AccountScanOptions, bool)

ScanOptionsFromContext retrieves the AccountScanOptions stored by ContextWithScanOptions. ok is false when the context carries no options, which callers should treat as "use defaults".

type ApplyBlockRequest

type ApplyBlockRequest struct {
	// ActionID preserves one admission identity across retries.
	ActionID string
	// FindingID is the original audit identity, captured before display truncation.
	// Empty means this decision has no originating finding.
	FindingID    string
	IP           string
	EngineReason string
	Reason       string
	TTL          time.Duration
	Source       string
}

ApplyBlockRequest describes one auto-response IP block. EngineReason is handed to the firewall engine (its provenance inference keys on it); Reason is the human evidence recorded in the threat DB, the tracker, and findings.

type ApplyBlockResult

type ApplyBlockResult struct {
	Outcome  firewall.BlockOutcome
	Findings []alert.Finding
}

ApplyBlockResult carries the engine outcome plus the auto_block findings the caller must route into its alert pipeline (scan batch or the daemon's async block recorder) so digests and alerting see every block source.

func ApplyBlock

func ApplyBlock(cfg *config.Config, req ApplyBlockRequest) (ApplyBlockResult, error)

ApplyBlock is the single chokepoint for auto-response IP blocks issued outside the scan loop (challenge escalation, central intel, incident spray). It performs the block and the same evidence bookkeeping a scan auto-block gets: threat-DB row, blocked-IPs tracker entry, auto_block finding with the Cloudflare coverage warning, and permanent-block escalation counting. The scan loop shares the inner implementation and keeps its own batch semantics (rate limit, pending queue) around it.

Non-scan sources deliberately neither consume nor enforce auto_response.max_blocks_per_hour: challenge escalation and central intel are already gated upstream, and letting them starve or be starved by the scan budget would change containment behavior.

type AttributionReport

type AttributionReport struct {
	// Current holds, per check, the qualifying rows in the latest-state
	// active set that carry no hosting owner and fall inside the correlation
	// window at its most recent merge. Unstamped legacy rows also count.
	// A later merge clears rows that gain an owner or age out of the window.
	Current map[string]int
	// Cumulative sums every unattributed row reported since start, by
	// check, across active-set merges and per-batch derivations.
	Cumulative map[string]int
	// ActiveSetUpdates counts active-set merges since start.
	ActiveSetUpdates int
	// Since is when the first active set was recorded; zero before then.
	Since time.Time
}

AttributionReport is the operator-facing view of correlation attribution. Current is what the latest-state active set looks like right now; Cumulative is the history since the daemon started. The two answer different questions: a producer that lost attribution for weeks and one that missed once look identical in a log line, and neither is visible in a health endpoint without this.

func AttributionHealth

func AttributionHealth() AttributionReport

AttributionHealth reports the process-wide attribution state for the health snapshot and doctor.

type AutoBlockFlushResult

type AutoBlockFlushResult struct {
	Flushed      bool
	BlockedCount int
	SnapshotErr  error
}

AutoBlockFlushResult reports which phases of a coordinated firewall flush completed and whether its best-effort persisted-state snapshot was readable.

func FlushAutoBlockState

func FlushAutoBlockState(statePath string, flush func() error) (AutoBlockFlushResult, error)

FlushAutoBlockState snapshots the engine's pre-flush IPs, runs an operator firewall flush, and clears the auto-block bookkeeping in one critical section. Without this the flush was self-reverting: surviving ThreatDB temp rows re-flagged every flushed IP through ip_reputation on the next scan and re-blocked it, and stale tracker entries suppressed re-block accounting. Tracker entries the engine never held are cleaned up too. Pending entries are kept - they are queued candidates, not blocks.

The firewall mutation and cleanup stay serialized with AutoBlockIPs so a concurrent scan either finishes before the snapshot or starts after cleanup with fresh evidence. Result.Flushed is true with a non-nil error when the firewall was flushed but bookkeeping cleanup was only partial. SnapshotErr is advisory because the tracker-side union still covers tracked auto-blocks.

type CMSHashCache

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

CMSHashCache stores SHA256 hashes of verified-clean CMS core files. After wp core verify-checksums confirms an installation is clean, all its core files are hashed and cached. The real-time scanner checks this cache before reporting signature matches - if a file's hash is in the cache, it's a known-clean CMS file and signature matches on it are false positives.

func GlobalCMSCache

func GlobalCMSCache() *CMSHashCache

GlobalCMSCache returns the singleton cache, creating it on first call.

func (*CMSHashCache) Add

func (c *CMSHashCache) Add(hash string, size int64)

Add inserts a file hash and its content size into the cache.

func (*CMSHashCache) Clear

func (c *CMSHashCache) Clear()

Clear removes all cached hashes (used before rebuilding).

func (*CMSHashCache) Contains

func (c *CMSHashCache) Contains(hash string) bool

Contains checks if a file hash is in the cache.

func (*CMSHashCache) MayContainSize

func (c *CMSHashCache) MayContainSize(size int64) bool

MayContainSize reports whether a verified file of size bytes was cached. It lets realtime scanning reject attacker-sized files before hashing them.

func (*CMSHashCache) Size

func (c *CMSHashCache) Size() int

Size returns the number of cached hashes.

type ChallengeIPList

type ChallengeIPList interface {
	Add(ip string, reason string, duration time.Duration)
	AddNonEscalating(ip string, reason string, duration time.Duration)
	Remove(ip string)
	Contains(ip string) bool
}

ChallengeIPList abstracts the challenge IP list for routing.

func GetChallengeIPList

func GetChallengeIPList() ChallengeIPList

GetChallengeIPList returns the current challenge IP list (for AutoBlockIPs skip check).

type ChallengeRouteRecord

type ChallengeRouteRecord struct {
	IP    string    `json:"ip"`
	Check string    `json:"check"`
	At    time.Time `json:"at"`
}

ChallengeRouteRecord is one IP routed to the challenge, for the web UI's recent-activity list.

type ChallengeUIStatsSnapshot

type ChallengeUIStatsSnapshot struct {
	RoutedByCheck map[string]int         `json:"routed_by_check"`
	Recent        []ChallengeRouteRecord `json:"recent"`
}

ChallengeUIStatsSnapshot is the web-UI view of challenge routing: cumulative per-check counts since daemon start plus the most recent routes. It is a copy, safe for the caller to read without locking.

func ChallengeUIStats

func ChallengeUIStats() ChallengeUIStatsSnapshot

ChallengeUIStats returns a copy of the current challenge routing stats for the web UI. Most recent route is last in Recent.

type CheckDispatchProgress

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

CheckDispatchProgress is memory-only evidence for a scan's orchestration. All fields are guarded by the owning dispatch monitor's mutex.

func WithCheckDispatchProgress

func WithCheckDispatchProgress(ctx context.Context) (context.Context, *CheckDispatchProgress)

WithCheckDispatchProgress binds subsequent check batches to this operation. A new binding isolates late callbacks from a previously canceled operation.

func (*CheckDispatchProgress) Snapshot

Snapshot does not query contexts, the filesystem or the job store.

type CheckFunc

type CheckFunc func(ctx context.Context, cfg *config.Config, store *state.Store) []alert.Finding

CheckFunc is the signature for all check functions. The context is cancelled when the check times out so goroutines can exit.

type CheckInfo

type CheckInfo struct {
	Name     string
	Category string
	Internal bool
	// Correlation says how cross-account correlation treats this check.
	// Every entry must set it; the zero value fails TestEveryCheckIsClassified.
	Correlation CorrelationClass
	// CorrelationReason names the policy that excludes an Ignored check. One
	// of the reason constants in correlation_policy.go.
	CorrelationReason string
	// CorrelationGap documents a known missing producer identity path for an
	// eligible check. It never changes eligibility.
	CorrelationGap string
}

CheckInfo describes a single named check emitted as an alert.Finding.Check. Category groups related checks for display in the settings UI. Internal is true for checks that exist for plumbing (self-tests, plumbing findings) and should not appear in user-facing dropdowns like alerts.email.disabled_checks.

func LookupCheck

func LookupCheck(name string) (CheckInfo, bool)

LookupCheck returns the registry entry for name, if any.

func PublicCheckInfos

func PublicCheckInfos() []CheckInfo

PublicCheckInfos returns all non-Internal checks grouped by category in the canonical category order (see checkCategoryOrder). Within a category names are sorted alphabetically. This is the list the settings UI shows for alerts.email.disabled_checks.

type CleanResult

type CleanResult struct {
	Path       string
	Cleaned    bool
	BackupPath string
	Removals   []string // descriptions of what was removed
	Error      string
	// Refused marks a safety refusal without changing the customer file.
	// It consumes attempt capacity but does not indicate a broken action.
	Refused bool
}

CleanResult describes the outcome of a cleaning attempt.

func CleanInfectedFile

func CleanInfectedFile(path string) CleanResult

CleanInfectedFile attempts to surgically remove malicious code from a PHP file while preserving the legitimate content. Always creates a backup first.

Cleaning strategies (tried in order): 1. @include injection - remove @include lines pointing to /tmp, eval, base64, or via variables 2. Prepend injection - remove malicious code blocks at start of file (entropy-validated) 3. Append injection - remove malicious code after closing ?> or end of PSR-12 file 4. Inline eval injection - remove eval(base64_decode(...)) single-line injections

type CmdRunner

type CmdRunner interface {
	Run(name string, args ...string) ([]byte, error)
	RunAllowNonZero(name string, args ...string) ([]byte, error)
	RunContext(parent context.Context, name string, args ...string) ([]byte, error)
	RunContextStdout(parent context.Context, name string, args ...string) ([]byte, error)
	RunWithEnv(name string, args []string, extraEnv ...string) ([]byte, error)
	LookPath(file string) (string, error)
}

CmdRunner abstracts external command execution used by check functions. Production code uses realCmd{}; tests swap in a mockCmdRunner via SetCmdRunner().

RunContext returns stdout+stderr merged (CombinedOutput) and is fine for tools that only write to stdout. RunContextStdout returns stdout only and should be used when the command prints structured output (JSON, a URL, ...) on stdout and chatter (warnings, PHP notices, MySQL deprecations) on stderr -- mixing them there would corrupt the parse. RunContextStdout also surfaces context.DeadlineExceeded on timeout so callers can distinguish "no output" from "empty output".

type ContentReverifyDismissal

type ContentReverifyDismissal struct {
	Check  string
	Path   string
	Detail string
	// Demoted and Promoted distinguish the outcomes for the audit log: a
	// cleared finding is gone, a demoted one is still listed at a lower
	// severity, a promoted one had an earlier demotion reversed.
	Demoted  bool
	Promoted bool
}

ContentReverifyDismissal records one finding the sweep cleared, for the caller to audit-log (the checks package has no logger of its own).

func ReverifyStaleFindings

func ReverifyStaleFindings(store LatestFindingStore) []ContentReverifyDismissal

ReverifyStaleFindings re-checks every auto-reverifiable finding in the store against current detection logic and dismisses those that are now confirmed stale: for content findings a file that is gone, or identical bytes the current classifier no longer flags; for web_exposed_* findings an exposure that a complete pinned probe no longer confirms. Dispatch goes through the verifier registry, so each family keeps its own safety invariant -- a still-present file is cleared only when its bytes are unchanged since detection, and an exposure only when a complete probe says the server no longer serves it. Returns the dismissed findings for the caller to log. Read-only except for dismissing confirmed-stale findings.

func ReverifyStaleFindingsContext

func ReverifyStaleFindingsContext(ctx context.Context, store LatestFindingStore) ([]ContentReverifyDismissal, bool)

type CorrelationClass

type CorrelationClass uint8

CorrelationClass is a check's role in cross-account correlation. Every registry entry sets one; the zero value fails the completeness test.

const (
	// CorrelationUnclassified is the zero value and never valid.
	CorrelationUnclassified CorrelationClass = iota
	// CorrelationIgnored is never an input to correlation; a reason is required.
	CorrelationIgnored
	// CorrelationSecurityEvent: an attributed Critical counts toward coordinated_attack.
	CorrelationSecurityEvent
	// CorrelationMalwareArtifact is a SecurityEvent that also raises
	// cross_account_malware when the same check appears on two accounts.
	CorrelationMalwareArtifact
	// CorrelationDerived is an output of correlation and never an input.
	CorrelationDerived
)

type CorrelationResult

type CorrelationResult struct {
	// Derived findings: the coordinated_attack result first, then one
	// cross_account_malware result per qualifying check, sorted by check.
	// Timestamps are left unset for the caller to stamp.
	Derived []alert.Finding
	// Unattributed counts qualifying input rows per check that carried no
	// account identity. It is a snapshot for this call, not a running total,
	// using the same window as the aggregates. Batch correlation has no age
	// filter; persisted correlation also counts unstamped legacy rows.
	Unattributed map[string]int
	// CriticalAccounts is the distinct attributed account count after the
	// same eligibility and time filters used to derive coordinated_attack.
	CriticalAccounts int
}

CorrelationResult is the output of one CorrelateFindings call.

func CorrelateBatchFindings

func CorrelateBatchFindings(findings []alert.Finding) CorrelationResult

CorrelateBatchFindings preserves dispatch grouping even when a scan carries forward a prior finding whose original timestamp lies outside the window.

func CorrelateFindings

func CorrelateFindings(findings []alert.Finding) CorrelationResult

CorrelateFindings raises cross-account findings. Eligibility comes from the registry classification and identity from extractAccountFromFinding. Callers initialize platform.Detect before correlation so account roots come from its cache. It does not log or mutate its input.

type Correlator

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

Correlator shares production rules with offline replay. Construct one with NewCorrelator to supply recording roots without triggering host discovery.

func NewCorrelator

func NewCorrelator(window time.Duration, accountRoots []string) Correlator

NewCorrelator uses only the supplied account roots, with no host lookups. Window must be nonnegative; zero reproduces unbounded correlation.

func (Correlator) Correlate

func (c Correlator) Correlate(findings []alert.Finding, at time.Time) CorrelationResult

Correlate derives aggregates at the supplied observation time. A zero time uses the newest non-derived input as its reference. Persisted state supplies the merge time so even an empty scan can expire old evidence.

func (Correlator) InputOf

func (c Correlator) InputOf(f alert.Finding) (account string, eligible bool)

InputOf uses the same classification and identity rules as Correlate. Eligibility is the check class only, before severity and window filtering.

type CoverageGaps

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

CoverageGaps holds file gaps and completed database scopes for a scan. The caller passes it to the atomic purge-and-merge operation so a concurrent update made after the scanner read LatestFindings cannot be retired by a stale carry-forward snapshot.

func WithCoverageGaps

func WithCoverageGaps(ctx context.Context) (context.Context, *CoverageGaps)

WithCoverageGaps requests an atomic coverage-aware store operation from the caller. The runner publishes only its completed snapshot into the handle.

func (*CoverageGaps) Paths

func (g *CoverageGaps) Paths() map[string]map[string]bool

Paths returns an isolated snapshot of the completed run's path gaps. Each inner key is a stable lexical or resolved alias captured during the scan.

func (*CoverageGaps) Snapshot

func (g *CoverageGaps) Snapshot() *state.ScanCoverage

Snapshot includes both file gaps and completed database scopes from the same runner result. Callers must pass this snapshot to the atomic store operation.

type DBCleanResult

type DBCleanResult struct {
	Account     string
	Database    string
	Action      string // "clean-option", "revoke-user", "delete-spam"
	Success     bool
	Message     string
	Details     []string // individual actions taken
	BackupNames []string // names of backup options created
}

DBCleanResult describes the outcome of a database cleanup operation.

func DBCleanOption

func DBCleanOption(account, optionName string, preview bool) DBCleanResult

DBCleanOption removes malicious script injections from a wp_option value. Creates a backup option before modifying. Returns a result describing what was done. If preview is true, reports what would be done without modifying the database.

func DBDeleteSpam

func DBDeleteSpam(account string, preview bool) DBCleanResult

DBDeleteSpam deletes published posts matching spam patterns from a WordPress database. Only deletes posts of type 'post' with status 'publish' to avoid touching pages, attachments, or plugin data. If preview is true, reports counts without deleting.

Two restrictions keep a keyword match from destroying real content: the SQL LIKE result is re-tested Go-side on a word boundary, and only the patterns marked deletable are considered. Injections that live inside an otherwise legitimate page are out of scope here -- deleting the page would destroy the customer's own content, so those need the injected markup stripped instead.

func DBDropObject

func DBDropObject(account, schema, kind, name string, preview bool) DBCleanResult

DBDropObject drops a single trigger / event / stored procedure / stored function from the operator-supplied account+schema, after:

  1. Validating the kind ("trigger" | "event" | "procedure" | "function").
  2. Validating that <schema> is one of the databases this account hosts. The account is taken from /home/<account>/* wp-config.php files; an attacker who can pass an arbitrary <schema> here gets no further than DROP'ping their own database.
  3. QuoteIdent on both <schema> and <name>, so identifier strings never participate in SQL string concatenation.
  4. SHOW CREATE the object and persist the result to the db_object_backups bbolt bucket as the backup -- replaying the CREATE SQL restores the object byte-for-byte.
  5. DROP the object.

preview=true short-circuits before step 4: the function reports what it would do (kind, schema, name, captured CREATE SQL) without touching the database.

Per spec: detection is always-on; drop is operator-driven.

func DBRevokeUser

func DBRevokeUser(account string, userID int, demote, preview bool) DBCleanResult

DBRevokeUser revokes WordPress sessions for a specific user and optionally demotes them to subscriber role. If preview is true, reports what would be done without modifying the database.

func RestoreDBObjectBackup

func RestoreDBObjectBackup(backupKey string) DBCleanResult

RestoreDBObjectBackup re-executes the captured CREATE SQL for a previously-dropped MySQL trigger / event / procedure / function. Looks up the row in the db_object_backups bbolt bucket by exact key; the caller (typically the web UI's cleanup-history page) supplies the key it got from the listing endpoint.

Per spec the operation is operator-driven: there is no auto- restore. The webui handler enforces the same.

type DirectSMTPEgressInput

type DirectSMTPEgressInput struct {
	UID     uint32
	User    string
	PID     uint32
	Comm    string
	Exe     string
	DstIP   net.IP
	DstPort uint16
	MTA     platform.MTAIdents
	Process *processctx.ProcessContext
	// Domain is an optional rDNS-resolved name for DstIP. When set, it
	// is included in the finding details. Populating it is the caller's
	// responsibility (off-path enrichment lands in Task 6).
	Domain string
}

DirectSMTPEgressInput is the input to the evaluator. The caller (BPF connection consumer or legacy poller) builds it from the live event and passes the platform-resolved MTA allowlist as MTA.

Process is optional; when present the resulting finding includes the full process-ancestry tree. UID/User/PID/Comm/Exe are the live event fields used in finding details and account attribution.

type DispatchProgressSnapshot

type DispatchProgressSnapshot struct {
	Active       bool
	Overdue      bool
	LastProgress time.Time
}

DispatchProgressSnapshot measures only the check batches owned by one caller. LastProgress remains available after the last wrapper exits, so its caller can time result handling without borrowing the completed check's budget.

type EnforceAction

type EnforceAction int

EnforceAction is the discrete outcome of the pure enforcement decision. Each value corresponds to one operational step the impure wrapper takes.

const (
	EnforceActionNoop EnforceAction = iota
	EnforceActionRestoreMarker
	EnforceActionUnloadModules
	EnforceActionRestoreAndUnload
)

type EnforceResult

type EnforceResult struct {
	Action         EnforceAction
	MarkerPresent  bool
	MarkerValid    bool
	ModulesLoaded  []string // names of currently-loaded targeted modules at start of call
	MarkerWritten  bool     // wrapper wrote/restored the marker file this call
	ModuleUnloaded bool     // post-call /proc/modules shows targeted modules gone
	Notes          []string // operator-readable lines (warnings, stuck-module names)
}

EnforceResult describes what enforceAFAlgBlocked observed and did, in a shape both the CLI subcommand and the periodic Check function can format for the operator without re-deriving the same conclusions.

ModuleUnloaded reports the OBSERVED post-call state, not the syscall attempt: it is true only when /proc/modules no longer contains the targeted modules after `modprobe -r` ran. Use this field to distinguish "unload succeeded" from "unload attempted but module is in use".

func EnforceAFAlgBlocked

func EnforceAFAlgBlocked() (EnforceResult, error)

EnforceAFAlgBlocked is the exported alias of enforceAFAlgBlocked for use by cmd/csm. The unexported form stays internal to the package so the periodic Check (Task 6) can call it without going through the export.

type IPBlocker

type IPBlocker interface {
	BlockIP(ip string, reason string, timeout time.Duration) error
	UnblockIP(ip string) error
	IsBlocked(ip string) bool
}

IPBlocker abstracts the firewall engine for auto-blocking. When set, blocks go through nftables firewall engine.

type LatestFindingStore

type LatestFindingStore interface {
	LatestFindings() []alert.Finding
	// Each mutation takes the snapshot verification actually looked at, so a
	// scan or realtime alert that refreshed the same key while the re-check was
	// in flight is never overwritten by the older verdict.
	DismissFindingIfLatest(expected alert.Finding) bool
	DemoteLatestFinding(expected alert.Finding, severity alert.Severity) bool
	RestoreLatestFindingSeverity(expected alert.Finding) bool
}

LatestFindingStore is the subset of the state store the sweep needs.

type ModSecReloadReconciler

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

ModSecReloadReconciler is owned by the daemon and shared by startup and all its scans. A CLI opening the store must not implicitly gain reload authority.

func (*ModSecReloadReconciler) Reconcile

func (r *ModSecReloadReconciler) Reconcile(reloadCommand string) error

Reconcile activates sections already written by startup or the installer. Only failed reloads are repeated; metadata failures retry just the write.

type OS

type OS interface {
	ReadFile(name string) ([]byte, error)
	ReadDir(name string) ([]os.DirEntry, error)
	Stat(name string) (os.FileInfo, error)
	Lstat(name string) (os.FileInfo, error)
	Readlink(name string) (string, error)
	Open(name string) (*os.File, error)
	WriteFile(name string, data []byte, perm os.FileMode) error
	MkdirAll(path string, perm os.FileMode) error
	Remove(name string) error
	Glob(pattern string) ([]string, error)
}

OS abstracts filesystem operations (read and write) used by check functions. Production code uses realOS{}; tests swap in a mockOS via SetOS().

type PHPExecutionOverlay

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

PHPExecutionOverlay is an immutable snapshot of inherited .htaccess PHP handler mappings for one directory. Realtime monitors can cache it and test arbitrary filenames without duplicating the periodic scanner's parser.

func ResolvePHPExecutionOverlay

func ResolvePHPExecutionOverlay(docroot, fileDir string) PHPExecutionOverlay

ResolvePHPExecutionOverlay reconstructs the PHP handler mappings inherited by fileDir from docroot through the directory's own .htaccess file.

func (PHPExecutionOverlay) Executes

func (o PHPExecutionOverlay) Executes(nameLower string) bool

Executes reports whether nameLower is handled as PHP by this overlay.

type PHPTaintAnalyzer

type PHPTaintAnalyzer interface {
	Analyze(ctx context.Context, src []byte) phptaint.Report
}

PHPTaintAnalyzer is what the daemon supplies: an analyzer that runs in a process the caller can kill.

type QuarantineMeta

type QuarantineMeta struct {
	OriginalPath    string    `json:"original_path"`
	Owner           int       `json:"owner_uid"`
	Group           int       `json:"group_gid"`
	Mode            string    `json:"mode"`
	Size            int64     `json:"size"`
	QuarantineAt    time.Time `json:"quarantined_at"`
	OriginalModTime time.Time `json:"original_mtime,omitzero"`
	Reason          string    `json:"reason"`
	// FindingID ties the quarantine to the finding that caused it, using the
	// same identifier the audit log and the action log emit. Empty when the
	// quarantine came from an operator command rather than a detection.
	FindingID             string `json:"finding_id,omitempty"`
	MessageID             string `json:"message_id,omitempty"`
	SpoolDir              string `json:"spool_dir,omitempty"`
	RestoreAction         string `json:"restore_action,omitempty"`
	ExpectedCurrentSHA256 string `json:"expected_current_sha256,omitempty"`
}

QuarantineMeta stores original file metadata alongside quarantined files.

func (*QuarantineMeta) UnmarshalJSON

func (m *QuarantineMeta) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts the timestamp spelling used by historical manual fixes. Missing original mtimes remain unknown; archive mtimes are not evidence of when the original was modified.

type RDNSCache

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

RDNSCache is a small TTL cache around reverse DNS lookups. Cached negative results (resolver error / NXDOMAIN) are kept until TTL too, so the detector does not hammer a slow resolver on a known-bad IP. Entries are capped at maxSize; the oldest-by-cachedAt entry is evicted on insert once the cap is reached.

func NewRDNSCache

func NewRDNSCache(cfg RDNSCacheConfig) *RDNSCache

NewRDNSCache returns a ready cache.

func (*RDNSCache) Lookup

func (c *RDNSCache) Lookup(ip net.IP) string

Lookup returns the cached hostname for ip, or "" on miss/error/deadline. Lookup blocks the caller for at most cfg.ResolveDeadline; cache hits return immediately.

func (*RDNSCache) QueueStatuses

func (c *RDNSCache) QueueStatuses(now time.Time) map[string]queuehealth.Status

QueueStatuses reads memory only. Without a deadline, Lookup is synchronous and does not use this bounded resolver pool.

type RDNSCacheConfig

type RDNSCacheConfig struct {
	TTL             time.Duration
	Resolve         func(ip net.IP) (string, error)
	ResolveDeadline time.Duration
	MaxSize         int
	// MaxConcurrent caps deadline-bound lookups, including returned results
	// still owned by their callers. A goroutine blocked on a wedged resolver cannot be
	// cancelled in Go, so without a cap a burst of distinct IPs under deadline
	// saturation spawns one abandonable goroutine per IP. 0 falls back to
	// rdnsCacheDefaultMaxConcurrent.
	MaxConcurrent int
}

RDNSCacheConfig is the config block for NewRDNSCache. Resolve is the function used to perform the actual reverse lookup; production callers wrap net.LookupAddr. ResolveDeadline bounds each lookup; 0 disables the deadline. MaxSize caps the number of cached entries to keep memory bounded on hosts that see a wide spread of remote IPs (BPF SMTP-egress is the motivating case); the oldest entry by cachedAt is evicted before a new one is inserted past the cap. 0 falls back to rdnsCacheDefaultMaxSize.

type RemediationResult

type RemediationResult struct {
	Success     bool   `json:"success"`
	Action      string `json:"action"`      // human-readable description of what was done
	Description string `json:"description"` // what fix was applied
	Error       string `json:"error,omitempty"`
	// Refused distinguishes an unchanged, ineligible target from an I/O failure.
	Refused bool `json:"-"`
	// RemediationStatus lets a caller that supports more than one successful
	// disposition distinguish an in-place clean from whole-file quarantine.
	// It is transport metadata, not part of the generic remediation API.
	RemediationStatus string `json:"-"`
	// Reverted marks a virtual patch that had to be written again because
	// something removed or damaged CSM's earlier block -- typically a backup
	// plugin rewriting the .htaccess it owns.
	Reverted bool `json:"reverted,omitempty"`
}

RemediationResult describes the outcome of a fix action.

func ApplyFix

func ApplyFix(ctx context.Context, checkType, message, details string, filePath ...string) RemediationResult

ApplyFix executes the remediation action for a finding.

func CleanHtaccessFile

func CleanHtaccessFile(path string) RemediationResult

CleanHtaccessFile audits the file, computes the removal range set, backs up the original, and writes the trimmed content. Returns success=false with no Action when no detector matched (i.e., nothing to clean).

Caller is responsible for gating on cfg.AutoResponse.CleanHtaccess before invoking; this function will clean unconditionally if detectors find anything.

func FixDisableWPCron

func FixDisableWPCron(path string, opts WPCronFixOptions) RemediationResult

FixDisableWPCron disables WP-Cron in a wp-config.php and installs a real per-user system cron that runs wp-cron.php on a fixed interval. It scopes writes to the default per-account roots (/home).

func FixDisableWPCronInRoots

func FixDisableWPCronInRoots(path string, allowedRoots []string, opts WPCronFixOptions) RemediationResult

FixDisableWPCronInRoots is FixDisableWPCron with caller-supplied roots so the Web UI can honor configured account_roots and tests can write under t.TempDir().

func FixDisplayErrorsOn

func FixDisplayErrorsOn(path string) RemediationResult

FixDisplayErrorsOn rewrites an INI / .htaccess / .user.ini file so the display_errors directive is set to Off. The original line is preserved commented out for operator review; an override line is appended at the end of the file so the last-write-wins semantics of every supported config format land on Off regardless of earlier statements.

Only .user.ini, php.ini, and .htaccess are accepted. wp-config.php and other PHP source files require code-level edits this routine does not attempt. The caller (web UI) should not advertise the fix for those.

func FixDisplayErrorsOnInRoots

func FixDisplayErrorsOnInRoots(path string, allowedRoots []string) RemediationResult

FixDisplayErrorsOnInRoots is FixDisplayErrorsOn with caller-supplied roots. The Web UI uses this to honor account_roots outside /home.

func FixErrorLogBloat

func FixErrorLogBloat(path string) RemediationResult

FixErrorLogBloat truncates an account-owned error_log file in place. Truncating preserves the inode and file ownership so any PHP process holding the descriptor keeps appending to the same file without an open/reopen race; this is also the safest action because nothing in the host needs the historical lines to keep serving traffic.

func FixErrorLogBloatInRoots

func FixErrorLogBloatInRoots(path string, allowedRoots []string) RemediationResult

FixErrorLogBloatInRoots is FixErrorLogBloat with caller-supplied roots. The Web UI uses this to include configured account_roots while tests can keep writes under t.TempDir().

func QuarantineFindingFile

func QuarantineFindingFile(f alert.Finding) (RemediationResult, bool)

QuarantineFindingFile quarantines the file a malware/webshell finding points at, for the full-scan --quarantine path. It reuses fixQuarantine (move to the quarantine dir + .meta sidecar) and deliberately covers ONLY the pure file-quarantine check set — it never kills processes, cleans databases, or touches the firewall. Returns eligible=false for any finding that is not a quarantinable malware/webshell FILE finding (caller marks those "left_for_review").

The job runs unattended, so it gets the same bar the scheduled auto-response applies: only a Critical finding (two converging indicators) may act, only on a regular file that is not a symlink, never on a whole directory, and a WordPress core, plugin or theme file is cleaned in place rather than moved, because moving it takes the site down while only the injected code had to go.

func VirtualPatchExposedFile

func VirtualPatchExposedFile(filePath string) RemediationResult

VirtualPatchExposedFile writes an .htaccess "Require all denied" rule that blocks HTTP download of a confirmed web-exposed file without modifying the file itself. For an archive inside a known backup-plugin directory the whole directory is denied. Returns Success=false with an "already" error when all applicable rules are already present (idempotent no-op).

type ReverifySweepStats

type ReverifySweepStats struct {
	Considered int
	Cleared    int
	Demoted    int
	Promoted   int
	Unchecked  int
	// TopUncheckedReason is the most common reason a finding could not be
	// re-checked at all, which is where a silent sweep usually goes wrong.
	TopUncheckedReason string
}

ReverifyStaleFindingsContext is the cancellable form used by the daemon so a large exposure queue cannot delay shutdown for every remaining probe. The bool is false after cancellation so the daemon leaves the sweep version uncommitted and retries it on the next start. ReverifySweepStats describes what a sweep actually did. A sweep that changed nothing is otherwise silent, which makes "ran and found nothing" indistinguishable from "never ran" and from "could not check a single finding" -- the difference an operator needs when findings are not draining.

type SeccompCoverageSummary

type SeccompCoverageSummary struct {
	Covered      []string // existing units with the CSM drop-in
	Uncovered    []string // existing units without the drop-in
	NotInstalled []string // candidate units not registered with systemd
}

SeccompCoverageSummary collapses the per-unit scan into the two numbers an operator cares about: how many existing units have the CSM drop-in, and how many do not.

func SummarizeAFAlgSeccompCoverage

func SummarizeAFAlgSeccompCoverage() SeccompCoverageSummary

SummarizeAFAlgSeccompCoverage rolls up ScanAFAlgSeccompState into the three-way summary above. Used by the hardening audit and the CLI status output.

type SeccompUnitState

type SeccompUnitState struct {
	Unit    string // e.g. "lshttpd.service"
	Exists  bool   // unit is registered with systemd on this host
	HasFile bool   // CSM-managed drop-in is present on disk
}

SeccompUnitState describes one unit's mitigation status in operator terms. Returned by ScanAFAlgSeccompState so both the CLI and the hardening audit can render the same view without re-deriving it.

func ScanAFAlgSeccompState

func ScanAFAlgSeccompState() []SeccompUnitState

ScanAFAlgSeccompState walks the candidate unit list and returns one SeccompUnitState per candidate. Units missing from systemd are still reported (Exists=false) so the operator can confirm CSM did not silently skip something they expected.

type SensitiveFileState

type SensitiveFileState struct {
	ContentDigest string
	PathIdentity  string
}

SensitiveFileState is the stable identity of one watchset path. Regular-file inode churn is deliberately excluded, while security metadata and symlink targets remain visible.

type ThreatDB

type ThreatDB struct {

	// Stats for WebUI
	PermanentCount int
	FeedIPCount    int
	FeedNetCount   int
	LastFeedUpdate time.Time
	LastUpdated    time.Time // tracks when feeds were last successfully loaded
	// contains filtered or unexported fields
}

ThreatDB is a local IP reputation database built from: 1. CSM's own block history (permanent) 2. Public threat intelligence feeds (updated daily) 3. AbuseIPDB as fallback for unknown IPs

func GetThreatDB

func GetThreatDB() *ThreatDB

GetThreatDB returns the global threat database.

func InitThreatDB

func InitThreatDB(statePath string, whitelistIPs []string) *ThreatDB

InitThreatDB initializes the global threat database.

func (*ThreatDB) AddOperatorTemporary

func (db *ThreatDB) AddOperatorTemporary(ip, reason string, ttl time.Duration)

AddOperatorTemporary records a timed operator block (the Web UI 24h block) for the lifetime of its firewall block. The evidence is operator-sourced but lapses with the block, so a mistaken 24h block of a customer address does not leave it permanently malicious.

func (*ThreatDB) AddPermanent

func (db *ThreatDB) AddPermanent(ip, reason string)

AddPermanent adds an IP to the permanent local blocklist. Called for deliberate operator blocks - persists across restarts and never expires. Upgrades an existing temp entry to permanent.

func (*ThreatDB) AddTemporary

func (db *ThreatDB) AddTemporary(ip, reason string, ttl time.Duration)

AddTemporary records an auto-blocked IP for the lifetime of its firewall block. Unlike AddPermanent the entry lapses with the block: a permanent record turned every temporary auto-block into a forever "known malicious IP" that re-flagged (and re-blocked) the address on each later access. ttl <= 0 is ignored because auto-block evidence must never become a never-expiring threat row.

func (*ThreatDB) AddWhitelist

func (db *ThreatDB) AddWhitelist(ip string)

AddWhitelist adds an IP to the permanent whitelist.

func (*ThreatDB) Count

func (db *ThreatDB) Count() int

Count returns the total number of entries in the database.

func (*ThreatDB) FeedsStale

func (db *ThreatDB) FeedsStale() bool

FeedsStale returns true if threat feeds have not been updated in over 7 days.

func (*ThreatDB) IsConfigWhitelisted

func (db *ThreatDB) IsConfigWhitelisted(ip string) bool

IsConfigWhitelisted reports whether ip is managed by reputation.whitelist. Runtime removal must not claim to remove these entries because the config remains authoritative and restores them on reload.

func (*ThreatDB) LastFeedRefresh

func (db *ThreatDB) LastFeedRefresh() time.Time

LastFeedRefresh returns when feeds last loaded successfully, preferring the in-memory timestamp over the persisted one. Zero means never.

func (*ThreatDB) Lookup

func (db *ThreatDB) Lookup(ip string) (string, bool)

Lookup checks if an IP is in the local threat database. Returns (source, true) if found, ("", false) if unknown. Whitelisted IPs always return false.

func (*ThreatDB) LookupMatch

func (db *ThreatDB) LookupMatch(ip string) (ThreatMatch, bool)

LookupMatch is Lookup plus the lifetime of the matched evidence, so the Web UI can explain why an unblocked IP still scores as malicious.

func (*ThreatDB) PruneExpiredThreats

func (db *ThreatDB) PruneExpiredThreats() int

PruneExpiredThreats removes threat entries whose temp lifetime has lapsed, in memory and in the persistent store (which also drops legacy no-source auto-block rows written before expiry tagging existed). Called periodically from the daemon heartbeat; this is by-design lifecycle cleanup, not data retention, so it does not sit behind the opt-in retention sweeps.

func (*ThreatDB) PruneExpiredWhitelist

func (db *ThreatDB) PruneExpiredWhitelist() int

PruneExpiredWhitelist removes expired temporary whitelist entries. Called periodically from the daemon heartbeat.

func (*ThreatDB) RemovePermanent

func (db *ThreatDB) RemovePermanent(ip string)

RemovePermanent removes an IP from the permanent blocklist and in-memory DB.

func (*ThreatDB) RemoveTemporary

func (db *ThreatDB) RemoveTemporary(ip string)

RemoveTemporary removes evidence that only lives as long as a firewall block: auto-block rows and timed operator blocks. It leaves permanent evidence untouched and restores feed ownership when the IP is independently present in a threat feed.

func (*ThreatDB) RemoveWhitelist

func (db *ThreatDB) RemoveWhitelist(ip string)

RemoveWhitelist removes an operator-managed whitelist entry. Config-managed entries are replaced only by SetConfigWhitelist.

func (*ThreatDB) SetConfigWhitelist

func (db *ThreatDB) SetConfigWhitelist(ips []string)

SetConfigWhitelist replaces the entries that come from reputation.whitelist in csm.yaml. Called on config reload; the hot-reload path reported success for that field while lookups kept honouring the startup list. Operator- managed entries added at runtime are untouched.

func (*ThreatDB) Stats

func (db *ThreatDB) Stats() map[string]interface{}

Stats returns statistics for the WebUI dashboard.

func (*ThreatDB) TempWhitelist

func (db *ThreatDB) TempWhitelist(ip string, ttl time.Duration)

TempWhitelist adds an IP to the whitelist with a TTL.

func (*ThreatDB) UpdateFeeds

func (db *ThreatDB) UpdateFeeds() error

UpdateFeeds downloads fresh threat intelligence feeds. Downloads outside the lock, then swaps data under lock to avoid blocking lookups. Each feed is swapped independently: a feed that fails to download (or fails validation) keeps its previously loaded IPs and CIDRs, so a transient outage never wipes that feed's coverage. lastUpdate only advances when at least one feed succeeded, otherwise the next cycle would skip the retry and serve zero feed data for the whole 20h window.

func (*ThreatDB) WhitelistedIPs

func (db *ThreatDB) WhitelistedIPs() []WhitelistIP

type ThreatMatch

type ThreatMatch struct {
	Source    string
	Permanent bool
	ExpiresAt time.Time // zero unless the entry lapses
}

ThreatMatch describes why an IP is flagged: which source named it, and whether that evidence is permanent (re-flags the address on every future sighting) or lapses with the block that recorded it.

type Tier

type Tier string

Tier identifies which set of checks to run.

const (
	TierCritical Tier = "critical" // Fast checks - processes, auth, network (~5 seconds)
	TierDeep     Tier = "deep"     // Filesystem scans - webshells, htaccess, WP core (~90 seconds)
	TierAll      Tier = "all"      // Both tiers
)

type ValiasEntry

type ValiasEntry struct {
	LocalPart string
	Domain    string
	Dest      string
}

ValiasEntry is one destination of one valiases alias.

func ParseValiasEntries

func ParseValiasEntries(r io.Reader, fileDomain string) ([]ValiasEntry, error)

ParseValiasEntries reads a valiases file for fileDomain and returns one entry per destination. cPanel keys aliases by full address ("bob@example.com"); a bare key ("bob", "*") belongs to fileDomain.

type VerifyInput

type VerifyInput struct {
	Check, Message, Details, Path string
	ContentSHA256, DetectLogic    string
	Context                       context.Context
	// contains filtered or unexported fields
}

VerifyInput carries everything a finding verifier may need. ContentSHA256 and DetectLogic are populated only for content findings emitted with a fingerprint. Context is optional; long-running verifiers use Background when it is nil.

type VerifyResult

type VerifyResult struct {
	Checked  bool `json:"checked"`
	Resolved bool `json:"resolved"`
	// Demote marks a finding whose flagged content is gone but whose
	// remediation cannot be proven, because the file changed since detection.
	// It is never cleared -- an attacker must not retire a finding by editing
	// the file -- but it stops ranking beside live threats.
	Demote bool   `json:"demote"`
	Detail string `json:"detail"`
}

VerifyResult reports whether a finding's underlying condition still holds.

Checked is false when the finding's check type has no cheap, reliable single-target re-check (the caller should tell the operator to dismiss after manual review or run a full account scan). When Checked is true, Resolved reports whether the condition is gone and the finding can be cleared.

func VerifyFinding

func VerifyFinding(checkType, message, details string, filePath ...string) VerifyResult

VerifyFinding re-evaluates a finding by check type + message/details/path. Preserved signature for CLI/legacy callers; carries no content fingerprint.

func VerifyFindingInput

func VerifyFindingInput(in VerifyInput) VerifyResult

VerifyFindingInput re-evaluates a finding from a full VerifyInput. Callers that verify content findings must provide the stored detection fingerprint.

type WPCronFixOptions

type WPCronFixOptions struct {
	// IntervalMinutes is how often the installed system cron runs wp-cron.php.
	// Clamped to [1,60]; a non-positive value falls back to the 15-minute default.
	IntervalMinutes int
	// PHPBin is the interpreter the cron line invokes. Empty uses an unambiguous
	// cPanel vhost version, then LookPath("php"), then /usr/local/bin/php.
	PHPBin string
}

WPCronFixOptions carries operator-tunable parameters for the WP-Cron remediation. Both the Web UI handler and the daemon auto-response resolve these from config before calling the fix, so the remediation core itself stays free of config coupling.

type WhitelistIP

type WhitelistIP struct {
	IP         string     `json:"ip"`
	ExpiresAt  *time.Time `json:"expires_at,omitempty"` // nil = permanent
	Permanent  bool       `json:"permanent"`
	Configured bool       `json:"configured,omitempty"`
}

WhitelistInfo returns all whitelisted IPs with their expiry info.

Source Files

Jump to

Keyboard shortcuts

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