File size: 12,961 Bytes
d2507b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 | package bundle
import "sort"
// This file is the host-agnostic CAPABILITY / RISK surfacing layer that AUGMENTS
// (never replaces) the existing malicious/benign bundle label. It is a pure
// taxonomy + presentation layer over res.Findings already extracted by the
// analyzers: NO AST, NO new detection, NO Severity changes.
//
// The core robustness property: capabilities are driven by HOST-AGNOSTIC
// findings (what a script CAN DO), not by the corpus's own defanged exfil host.
// exfil-host-reference (which matches attacker.example literally) is demoted to
// evidence_only NETWORK_EGRESS support and never acts as a capability driver, so
// swapping attacker.example for a novel host does NOT silence the exfil-capable
// combo (which fires via the host-agnostic exfil-env-to-network source+sink
// co-occurrence).
// CapabilityName identifies one host-agnostic thing a bundled script can do.
type CapabilityName string
const (
CapSecretAccess CapabilityName = "SECRET_ACCESS"
CapNetworkEgress CapabilityName = "NETWORK_EGRESS"
CapOpaqueExecution CapabilityName = "OPAQUE_EXECUTION"
CapDynamicFetchExec CapabilityName = "DYNAMIC_FETCH_EXEC"
CapCodeExecution CapabilityName = "CODE_EXECUTION"
CapRegistryTamper CapabilityName = "REGISTRY_TAMPER"
CapFSDestructive CapabilityName = "FS_DESTRUCTIVE"
CapObfuscation CapabilityName = "OBFUSCATION"
CapAntiAnalysis CapabilityName = "ANTI_ANALYSIS"
CapPersistence CapabilityName = "PERSISTENCE"
)
// RiskTier is the combos-only risk ranking surfaced alongside the back-compat
// label. REVIEW is pinned to the existing escalation decision; ELEVATED/INFO/
// CLEAN rank the non-escalating remainder by capability power.
type RiskTier string
const (
TierClean RiskTier = "CLEAN"
TierInfo RiskTier = "INFO"
TierElevated RiskTier = "ELEVATED"
TierReview RiskTier = "REVIEW"
)
// Evidence is a single finding-derived datum supporting a capability. It carries
// the originating sibling/member, line, signal, and human-readable detail so the
// capability card can show operators exactly where the behavior was observed.
type Evidence struct {
File string `json:"file"`
Line int `json:"line"`
Signal string `json:"signal"`
Detail string `json:"detail"`
}
// Capability is one host-agnostic capability present in a bundle, with its
// power class ("high"|"low") and the evidence that produced it.
type Capability struct {
Name string `json:"name"`
Power string `json:"power"`
Evidence []Evidence `json:"evidence"`
}
// BigNasty is one big-nasty capability COMBINATION that fired, with a
// plain-English explanation of why it is dangerous. The big_nasties list is
// always computed and reported: it explains WHY a REVIEW verdict fired.
type BigNasty struct {
Name string `json:"name"`
Why string `json:"why"`
}
// CapabilityProfile is the full host-agnostic risk surface for a bundle: the
// capabilities present, the big-nasty combos that fired, and the derived tier.
type CapabilityProfile struct {
Capabilities []Capability `json:"capabilities,omitempty"`
BigNasties []BigNasty `json:"big_nasties,omitempty"`
Tier RiskTier `json:"risk_tier"`
}
// signalCapabilityMap maps each emitted Finding.Signal to the host-agnostic
// capabilities it drives. exfil-host-reference is deliberately NOT here as a
// driver β it is host-specific (matches attacker.example) and lives in
// evidenceOnlySignals/evidenceDecorates, only decorating NETWORK_EGRESS.
var signalCapabilityMap = map[string][]CapabilityName{
"env-credential-access": {CapSecretAccess},
"exfil-env-to-network": {CapSecretAccess, CapNetworkEgress},
"remote-code-execution": {CapDynamicFetchExec},
"reverse-shell": {CapNetworkEgress, CapCodeExecution},
"scheduled-network-callback": {CapNetworkEgress, CapPersistence},
"persistence": {CapPersistence},
"eval-decoded-payload": {CapOpaqueExecution, CapObfuscation},
"ships-opaque-executable": {CapOpaqueExecution},
"opaque-bytecode": {CapOpaqueExecution},
"ships-compiled-bytecode": {CapOpaqueExecution},
"suspicious-native-symbols": {CapOpaqueExecution},
"opaque-archive": {CapOpaqueExecution},
"opaque-archive-member": {CapOpaqueExecution},
"opaque-image": {CapOpaqueExecution},
"opaque-notebook": {CapOpaqueExecution},
"compiled-source-mismatch": {CapOpaqueExecution},
"compiled-without-matching-source": {CapOpaqueExecution},
"archive-contains-executable": {CapOpaqueExecution, CapAntiAnalysis},
"delegates-to-bundled-script": {CapCodeExecution},
"delegates-to-data": {CapCodeExecution},
"delegates-to-image": {CapCodeExecution},
"symlinked-executable-reference": {CapCodeExecution},
"allowed-tools-nl-directive": {CapCodeExecution},
"registry-rewrite": {CapRegistryTamper},
"destructive-command": {CapFSDestructive},
"embedded-encoded-blob": {CapObfuscation},
"image-embedded-blob": {CapObfuscation},
"data-embedded-directive": {CapObfuscation},
"image-embedded-directive": {CapObfuscation},
"padding-evasion": {CapObfuscation},
"archive-path-traversal": {CapAntiAnalysis},
"archive-bomb-guard": {CapAntiAnalysis},
"archive-too-deep": {CapAntiAnalysis},
"archive-member-limit": {CapAntiAnalysis},
}
// evidenceOnlySignals contribute evidence (or scan-coverage notes) but never act
// as a capability driver. exfil-host-reference is host-specific and is treated
// as supporting evidence for NETWORK_EGRESS only; references-unscanned-filetype
// and analyzer-error are scan-limitation notes (no capability at all).
var evidenceOnlySignals = map[string]bool{
"exfil-host-reference": true, // host-specific (attacker.example); decorates NETWORK_EGRESS only
"references-unscanned-filetype": true, // scan-limitation note
"analyzer-error": true, // scan-limitation note
}
// evidenceDecorates maps a host-specific evidence_only signal to the capability
// its evidence attaches to. It NEVER creates the capability β the entry only
// appends evidence when that capability is already (or also) present-driven, or
// surfaces the host-specific marker under that capability's evidence list. The
// capability's PRESENCE for combo purposes is still governed by host-agnostic
// drivers in signalCapabilityMap.
var evidenceDecorates = map[string]CapabilityName{
"exfil-host-reference": CapNetworkEgress,
}
// highPowerCaps marks the capabilities whose mere presence (without a combo)
// warrants ELEVATED. Low-power capabilities (CODE_EXECUTION-of-readable-script,
// OBFUSCATION, ANTI_ANALYSIS) only reach INFO on their own.
var highPowerCaps = map[CapabilityName]bool{
CapSecretAccess: true,
CapNetworkEgress: true,
CapOpaqueExecution: true,
CapDynamicFetchExec: true,
CapRegistryTamper: true,
CapFSDestructive: true,
}
// BuildCapabilityProfile is a PURE function over the already-extracted findings.
// It does NO new detection and NO AST: it folds each Finding through the
// host-agnostic signal->capability map, attaches host-specific evidence to the
// capabilities it decorates, classifies power, and computes the big-nasty
// combos. The returned profile's Tier is left zero-valued (CLEAN's zero value is
// the empty string, not TierClean) and MUST be set by ComputeTier at the call
// site, which pins REVIEW to the existing escalation decision.
func BuildCapabilityProfile(findings []Finding) CapabilityProfile {
present := map[CapabilityName][]Evidence{}
signals := map[string]bool{}
for _, f := range findings {
signals[f.Signal] = true
ev := Evidence{File: f.File, Line: f.Line, Signal: f.Signal, Detail: f.Detail}
for _, c := range signalCapabilityMap[f.Signal] {
present[c] = append(present[c], ev)
}
// Host-specific evidence decorates an existing/also-driven capability but
// never drives it on its own β append only when the capability is already
// present (driven by a host-agnostic finding). This guarantees no
// capability can be conjured purely by the host-specific match.
if dec, ok := evidenceDecorates[f.Signal]; ok {
if _, driven := present[dec]; driven {
present[dec] = append(present[dec], ev)
}
}
}
caps := make([]Capability, 0, len(present))
for name, evs := range present {
power := "low"
if highPowerCaps[name] {
power = "high"
}
caps = append(caps, Capability{Name: string(name), Power: power, Evidence: evs})
}
sortCaps(caps)
return CapabilityProfile{
Capabilities: caps,
BigNasties: detectBigNasties(present, signals),
}
}
// sortCaps orders capabilities deterministically by name so JSON/SARIF output
// and test assertions are stable across runs (map iteration is unordered).
func sortCaps(caps []Capability) {
sort.Slice(caps, func(i, j int) bool {
return caps[i].Name < caps[j].Name
})
}
// detectBigNasties computes the host-agnostic capability COMBINATIONS that drive
// the "why" behind a REVIEW. The combos are a documented SUPERSET of
// bundleEscalates's behavioral triggers so the plain-English explanation never
// contradicts the verdict. Critically, exfil-capable fires on the fused
// exfil-env-to-network finding (SECRET_ACCESS+NETWORK_EGRESS) OR on
// env-credential-access co-occurring with any NETWORK_EGRESS-driving sink β it
// NEVER requires exfil-host-reference, so it survives a host swap.
func detectBigNasties(present map[CapabilityName][]Evidence, signals map[string]bool) []BigNasty {
has := func(c CapabilityName) bool { _, ok := present[c]; return ok }
var out []BigNasty
if signals["reverse-shell"] {
out = append(out, BigNasty{
Name: "remote-shell",
Why: "Opens a reverse/bind shell β hands an attacker an interactive shell on the host, independent of any destination string.",
})
}
if signals["scheduled-network-callback"] {
out = append(out, BigNasty{
Name: "scheduled-phone-home",
Why: "Installs a persistent/scheduled job that performs network I/O β a recurring callout that survives restarts.",
})
}
if has(CapSecretAccess) && has(CapNetworkEgress) {
out = append(out, BigNasty{
Name: "exfil-capable",
Why: "Reads a credential AND opens a network sink β can steal secrets and ship them off-box, regardless of destination host.",
})
}
if has(CapOpaqueExecution) {
out = append(out, BigNasty{
Name: "runs-uninspectable-code",
Why: "Executes code that cannot be read (compiled/opaque artifact or source-mismatched binary, the xz pattern).",
})
}
if has(CapDynamicFetchExec) {
out = append(out, BigNasty{
Name: "fetch-and-run",
Why: "Downloads remote code and runs it immediately β payload is whatever the server returns at runtime.",
})
}
if has(CapObfuscation) && (has(CapNetworkEgress) || has(CapCodeExecution) || has(CapOpaqueExecution)) {
out = append(out, BigNasty{
Name: "hidden-and-active",
Why: "Hidden/encoded content combined with an active execution or egress capability β concealing live behavior.",
})
}
if has(CapRegistryTamper) {
out = append(out, BigNasty{
Name: "external-registry-redirect",
Why: "Rewrites package/registry config to redirect installs to an external endpoint β supply-chain redirect.",
})
}
if has(CapFSDestructive) {
out = append(out, BigNasty{
Name: "data-destruction",
Why: "Issues mass-destructive filesystem commands capable of wiping user data.",
})
}
return out
}
// ComputeTier ranks a bundle into a RiskTier. REVIEW is PINNED to the existing
// label decision (escalatedOrMalicious := scorerMalicious || bundleEscalates) so
// the derived tier β and therefore the derived label β can NEVER diverge from
// today's bundle label on the corpus. The combo taxonomy does NOT gate REVIEW;
// it only ranks the NON-escalating remainder into ELEVATED/INFO/CLEAN by the
// power of the host-agnostic capabilities present.
func ComputeTier(escalatedOrMalicious bool, prof CapabilityProfile) RiskTier {
if escalatedOrMalicious {
return TierReview
}
high := false
low := false
for _, c := range prof.Capabilities {
if c.Power == "high" {
high = true
} else {
low = true
}
}
switch {
case high:
return TierElevated
case low:
return TierInfo
default:
return TierClean
}
}
// DeriveLabel keeps the back-compat bundle label byte-identical to today's:
// malicious iff REVIEW. Since REVIEW is pinned to (scorerMalicious ||
// bundleEscalates), this equals the exact OR that MakeBundleVerdict uses to set
// the label today, guaranteeing ZERO regression on the 240-bundle eval.
func DeriveLabel(tier RiskTier) string {
if tier == TierReview {
return "malicious"
}
return "benign"
}
|