muaddib-scanner 2.10.8 → 2.10.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/intent-graph.js +22 -5
- package/src/scanner/dataflow.js +13 -0
- package/src/scoring.js +11 -0
package/package.json
CHANGED
package/src/intent-graph.js
CHANGED
|
@@ -173,25 +173,42 @@ function isSDKPattern(envVarName, fileContent) {
|
|
|
173
173
|
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(domain)) return false;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
// 1. Try curated allowlist first
|
|
176
|
+
// 1. Try curated allowlist first (strict: ALL domains must match)
|
|
177
|
+
// Curated allowlist is authoritative — no relaxation here to prevent
|
|
178
|
+
// attacker injecting a legitimate domain alongside their C2 domain.
|
|
177
179
|
for (const mapping of SDK_ENV_DOMAIN_MAP) {
|
|
178
180
|
if (mapping.envPattern.test(envVarName)) {
|
|
179
|
-
// All domains must match expected SDK domains
|
|
180
181
|
return domains.every(d => domainMatchesSuffix(d, mapping.domains));
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
// R2: credential-suffixed env vars get relaxed domain matching (at least ONE match).
|
|
186
|
+
// SDKs commonly call their own API + CDN/logging/analytics domains.
|
|
187
|
+
// Safety: suspicious domains and raw IPs are already rejected above.
|
|
188
|
+
// Only applies to the heuristic fallback — curated allowlist stays strict.
|
|
189
|
+
const CREDENTIAL_SUFFIXES = ['_API_KEY', '_SECRET', '_TOKEN', '_SECRET_KEY', '_ACCESS_KEY'];
|
|
190
|
+
const upperName = envVarName.toUpperCase();
|
|
191
|
+
const hasCredentialSuffix = CREDENTIAL_SUFFIXES.some(s => upperName.endsWith(s));
|
|
192
|
+
|
|
184
193
|
// 2. Heuristic fallback: extract brand keyword and check domain labels
|
|
185
194
|
const brand = extractBrandFromEnvVar(envVarName);
|
|
186
195
|
if (!brand || brand.length < 3) return false; // Too short for reliable matching
|
|
187
196
|
|
|
188
197
|
const brandLower = brand.toLowerCase();
|
|
189
|
-
//
|
|
198
|
+
// 2a. Strict check: every domain matches brand (existing behavior)
|
|
190
199
|
// e.g., brand "ACME" matches "api.acme.com" (label "acme") but not "api.acmetech.com"
|
|
191
|
-
|
|
200
|
+
if (domains.every(d => {
|
|
201
|
+
const labels = d.split('.');
|
|
202
|
+
return labels.some(label => label === brandLower);
|
|
203
|
+
})) return true;
|
|
204
|
+
|
|
205
|
+
// 2b. R2 relaxed: credential suffix + at least one domain matches brand
|
|
206
|
+
if (hasCredentialSuffix && domains.some(d => {
|
|
192
207
|
const labels = d.split('.');
|
|
193
208
|
return labels.some(label => label === brandLower);
|
|
194
|
-
});
|
|
209
|
+
})) return true;
|
|
210
|
+
|
|
211
|
+
return false;
|
|
195
212
|
}
|
|
196
213
|
|
|
197
214
|
|
package/src/scanner/dataflow.js
CHANGED
|
@@ -954,6 +954,19 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
954
954
|
}
|
|
955
955
|
}
|
|
956
956
|
|
|
957
|
+
// Graduation: HIGH → MEDIUM for env/telemetry-only sources (no credential file reads,
|
|
958
|
+
// no fingerprint reads, no command output). Distant env/telemetry → network_send
|
|
959
|
+
// is the dominant FP pattern (SDK/API usage, binary wrappers, config libraries).
|
|
960
|
+
// Real credential exfiltration uses credential_read or fingerprint_read sources.
|
|
961
|
+
if (severity === 'HIGH') {
|
|
962
|
+
const hasHighRiskSource = sources.some(s =>
|
|
963
|
+
s.type === 'credential_read' || s.type === 'fingerprint_read' || s.type === 'command_output'
|
|
964
|
+
);
|
|
965
|
+
if (!hasHighRiskSource) {
|
|
966
|
+
severity = 'MEDIUM';
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
957
970
|
const sourceDesc = hasCommandOutput ? 'command output' : 'credentials read';
|
|
958
971
|
threats.push({
|
|
959
972
|
type: 'suspicious_dataflow',
|
package/src/scoring.js
CHANGED
|
@@ -40,6 +40,11 @@ const MAX_RISK_SCORE = 100;
|
|
|
40
40
|
// to limit noise while preserving some signal. CRITICAL and HIGH prototype_hook findings still score normally.
|
|
41
41
|
const PROTO_HOOK_MEDIUM_CAP = 15;
|
|
42
42
|
|
|
43
|
+
// R4: suspicious_dataflow(MEDIUM) is a co-occurrence signal, not a standalone detection.
|
|
44
|
+
// Multiple env_read/telemetry distant flows in the same file should not inflate the score.
|
|
45
|
+
// Compounds (lifecycle_dataflow) provide the real signal and score separately.
|
|
46
|
+
const DATAFLOW_MEDIUM_CAP = 3;
|
|
47
|
+
|
|
43
48
|
// Confidence-weighted scoring factors (v2.7.10)
|
|
44
49
|
// High-confidence detections (eval, IOC, shell injection) score at full weight.
|
|
45
50
|
// Medium-confidence heuristics (lifecycle_script, obfuscation, high_entropy) are discounted.
|
|
@@ -128,6 +133,7 @@ function isPackageLevelThreat(threat) {
|
|
|
128
133
|
function computeGroupScore(threats) {
|
|
129
134
|
let score = 0;
|
|
130
135
|
let protoHookMediumPoints = 0;
|
|
136
|
+
let dataflowMediumPoints = 0;
|
|
131
137
|
|
|
132
138
|
for (const t of threats) {
|
|
133
139
|
const weight = _severityWeights[t.severity] || 0;
|
|
@@ -138,11 +144,16 @@ function computeGroupScore(threats) {
|
|
|
138
144
|
protoHookMediumPoints += weight * factor;
|
|
139
145
|
continue;
|
|
140
146
|
}
|
|
147
|
+
if (t.type === 'suspicious_dataflow' && t.severity === 'MEDIUM') {
|
|
148
|
+
dataflowMediumPoints += weight * factor;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
141
151
|
|
|
142
152
|
score += weight * factor;
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
score += Math.min(protoHookMediumPoints, PROTO_HOOK_MEDIUM_CAP);
|
|
156
|
+
score += Math.min(dataflowMediumPoints, DATAFLOW_MEDIUM_CAP);
|
|
146
157
|
return Math.min(MAX_RISK_SCORE, Math.round(score));
|
|
147
158
|
}
|
|
148
159
|
|