@7nsane/zift 2.1.0 → 2.2.1
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/collector.js +41 -16
- package/src/engine.js +17 -36
- package/src/rules/definitions.js +30 -17
- package/src/scanner.js +13 -1
package/package.json
CHANGED
package/src/collector.js
CHANGED
|
@@ -12,16 +12,23 @@ class ASTCollector {
|
|
|
12
12
|
collect(code, filePath) {
|
|
13
13
|
const facts = {
|
|
14
14
|
ENV_READ: [],
|
|
15
|
+
MASS_ENV_ACCESS: [],
|
|
15
16
|
FILE_READ_SENSITIVE: [],
|
|
16
17
|
NETWORK_SINK: [],
|
|
18
|
+
DNS_SINK: [],
|
|
19
|
+
RAW_SOCKET_SINK: [],
|
|
17
20
|
DYNAMIC_EXECUTION: [],
|
|
21
|
+
DYNAMIC_REQUIRE: [],
|
|
18
22
|
OBFUSCATION: [],
|
|
19
23
|
FILE_WRITE_STARTUP: [],
|
|
20
24
|
SHELL_EXECUTION: [],
|
|
21
|
-
ENCODER_USE: []
|
|
25
|
+
ENCODER_USE: [],
|
|
26
|
+
REMOTE_FETCH_SIGNAL: [],
|
|
27
|
+
PIPE_TO_SHELL_SIGNAL: []
|
|
22
28
|
};
|
|
23
29
|
const flows = [];
|
|
24
30
|
const sourceCode = code;
|
|
31
|
+
let envAccessCount = 0;
|
|
25
32
|
|
|
26
33
|
let ast;
|
|
27
34
|
try {
|
|
@@ -60,16 +67,16 @@ class ASTCollector {
|
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
if (calleeCode === 'require' && node.arguments.length > 0 && node.arguments[0].type !== 'Literal') {
|
|
63
|
-
facts.
|
|
70
|
+
facts.DYNAMIC_REQUIRE.push({
|
|
64
71
|
file: filePath,
|
|
65
72
|
line: node.loc.start.line,
|
|
66
|
-
type: 'dynamic_require',
|
|
67
73
|
variable: sourceCode.substring(node.arguments[0].start, node.arguments[0].end)
|
|
68
74
|
});
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
const netType = this.getNetworkType(calleeCode);
|
|
78
|
+
if (netType) {
|
|
79
|
+
facts[netType].push({
|
|
73
80
|
file: filePath,
|
|
74
81
|
line: node.loc.start.line,
|
|
75
82
|
callee: calleeCode
|
|
@@ -82,6 +89,19 @@ class ASTCollector {
|
|
|
82
89
|
line: node.loc.start.line,
|
|
83
90
|
callee: calleeCode
|
|
84
91
|
});
|
|
92
|
+
|
|
93
|
+
// Signal Analysis: Dropper Patterns
|
|
94
|
+
node.arguments.forEach(arg => {
|
|
95
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
96
|
+
const val = arg.value.toLowerCase();
|
|
97
|
+
if ((val.includes('curl') || val.includes('wget') || val.includes('fetch')) && (val.includes('http') || val.includes('//'))) {
|
|
98
|
+
facts.REMOTE_FETCH_SIGNAL.push({ file: filePath, line: node.loc.start.line, context: val });
|
|
99
|
+
}
|
|
100
|
+
if (val.includes('| sh') || val.includes('| bash') || val.includes('| cmd') || val.includes('| pwsh')) {
|
|
101
|
+
facts.PIPE_TO_SHELL_SIGNAL.push({ file: filePath, line: node.loc.start.line, context: val });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
if (this.isEncoder(calleeCode)) {
|
|
@@ -110,7 +130,6 @@ class ASTCollector {
|
|
|
110
130
|
|
|
111
131
|
node.arguments.forEach((arg, index) => {
|
|
112
132
|
const argCode = sourceCode.substring(arg.start, arg.end);
|
|
113
|
-
// Improved check: Does the expression contain any variable we know is tainted?
|
|
114
133
|
const isArgTainted = argCode.includes('process.env') || flows.some(f => {
|
|
115
134
|
const regex = new RegExp(`\\b${f.toVar}\\b`);
|
|
116
135
|
return regex.test(argCode);
|
|
@@ -137,11 +156,16 @@ class ASTCollector {
|
|
|
137
156
|
const whitelist = ['NODE_ENV', 'TIMING', 'DEBUG', 'VERBOSE', 'CI', 'APPDATA', 'HOME', 'USERPROFILE', 'PATH', 'PWD'];
|
|
138
157
|
if (whitelist.includes(property)) return;
|
|
139
158
|
|
|
159
|
+
envAccessCount++;
|
|
140
160
|
facts.ENV_READ.push({
|
|
141
161
|
file: filePath,
|
|
142
162
|
line: node.loc.start.line,
|
|
143
163
|
variable: property ? `process.env.${property}` : 'process.env'
|
|
144
164
|
});
|
|
165
|
+
|
|
166
|
+
if (envAccessCount > 5) {
|
|
167
|
+
facts.MASS_ENV_ACCESS.push({ file: filePath, line: node.loc.start.line, count: envAccessCount });
|
|
168
|
+
}
|
|
145
169
|
}
|
|
146
170
|
},
|
|
147
171
|
VariableDeclarator: (node) => {
|
|
@@ -191,20 +215,21 @@ class ASTCollector {
|
|
|
191
215
|
return { facts, flows };
|
|
192
216
|
}
|
|
193
217
|
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
218
|
+
getNetworkType(calleeCode) {
|
|
219
|
+
const dnsSinks = ['dns.lookup', 'dns.resolve', 'dns.resolve4', 'dns.resolve6'];
|
|
220
|
+
const rawSocketSinks = ['net.connect', 'net.createConnection'];
|
|
221
|
+
const networkSinks = ['http.request', 'https.request', 'http.get', 'https.get', 'fetch', 'axios', 'request'];
|
|
222
|
+
|
|
223
|
+
if (dnsSinks.some(sink => calleeCode === sink || calleeCode.endsWith('.' + sink))) return 'DNS_SINK';
|
|
224
|
+
if (rawSocketSinks.some(sink => calleeCode === sink || calleeCode.endsWith('.' + sink))) return 'RAW_SOCKET_SINK';
|
|
225
|
+
if (networkSinks.some(sink => {
|
|
202
226
|
if (calleeCode === sink) return true;
|
|
203
227
|
if (calleeCode.endsWith('.' + sink)) return true;
|
|
204
|
-
// Catch cases like require('https').get
|
|
205
228
|
if (sink.includes('.') && calleeCode.endsWith(sink.split('.')[1]) && calleeCode.includes(sink.split('.')[0])) return true;
|
|
206
229
|
return false;
|
|
207
|
-
})
|
|
230
|
+
})) return 'NETWORK_SINK';
|
|
231
|
+
|
|
232
|
+
return null;
|
|
208
233
|
}
|
|
209
234
|
|
|
210
235
|
isShellSink(calleeCode) {
|
package/src/engine.js
CHANGED
|
@@ -6,7 +6,7 @@ class SafetyEngine {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
evaluate(packageFacts, lifecycleFiles) {
|
|
9
|
-
|
|
9
|
+
let findings = [];
|
|
10
10
|
|
|
11
11
|
// Process each rule
|
|
12
12
|
for (const rule of RULES) {
|
|
@@ -16,6 +16,9 @@ class SafetyEngine {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
// Sort by score (desc) and then by priority (desc)
|
|
20
|
+
findings.sort((a, b) => (b.score - a.score) || (b.priority - a.priority));
|
|
21
|
+
|
|
19
22
|
return findings;
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -29,34 +32,11 @@ class SafetyEngine {
|
|
|
29
32
|
for (const req of rule.requires) {
|
|
30
33
|
let matchedFacts = facts[req] || [];
|
|
31
34
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} else if (req === 'DYNAMIC_EXECUTION') {
|
|
36
|
-
matchedFacts = matchedFacts.filter(f => f.type !== 'dynamic_require');
|
|
37
|
-
}
|
|
35
|
+
// Specialist Rule: Startup Mod (ZFT-012) requires specific file paths (now explicit in definitions but engine may still help)
|
|
36
|
+
// But per review, we should aim for explicit facts.
|
|
37
|
+
// ZFT-012 now just requires FILE_WRITE_STARTUP. Simple.
|
|
38
38
|
|
|
39
39
|
if (matchedFacts.length === 0) return null; // Rule not matched
|
|
40
|
-
|
|
41
|
-
// Specialist Rule: DNS Exfiltration (ZFT-007) requires a DNS-specific sink
|
|
42
|
-
if (rule.alias === 'DNS_EXFILTRATION' && req === 'NETWORK_SINK') {
|
|
43
|
-
matchedFacts = matchedFacts.filter(f => f.callee && f.callee.includes('dns'));
|
|
44
|
-
if (matchedFacts.length === 0) return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Specialist Rule: Raw Socket Tunnel (ZFT-011) requires net.connect or similar
|
|
48
|
-
if (rule.alias === 'RAW_SOCKET_TUNNEL' && req === 'NETWORK_SINK') {
|
|
49
|
-
matchedFacts = matchedFacts.filter(f => f.callee && (f.callee.includes('net.connect') || f.callee.includes('net.createConnection')));
|
|
50
|
-
if (matchedFacts.length === 0) return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Specialist Rule: Startup Mod (ZFT-012) requires specific file paths
|
|
54
|
-
if (rule.alias === 'STARTUP_SCRIPT_MOD' && req === 'FILE_WRITE_STARTUP') {
|
|
55
|
-
const startupFiles = ['package.json', '.npmrc', '.bashrc', '.zshrc'];
|
|
56
|
-
matchedFacts = matchedFacts.filter(f => f.path && startupFiles.some(s => f.path.includes(s)));
|
|
57
|
-
if (matchedFacts.length === 0) return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
40
|
triggers.push(...matchedFacts.map(f => ({ ...f, type: req })));
|
|
61
41
|
}
|
|
62
42
|
|
|
@@ -71,10 +51,10 @@ class SafetyEngine {
|
|
|
71
51
|
}
|
|
72
52
|
}
|
|
73
53
|
|
|
74
|
-
//
|
|
75
|
-
const isInLifecycle = triggers.some(t => lifecycleFiles.has(t.file));
|
|
54
|
+
// Check for Lifecycle Context Fact (Virtual or Actual)
|
|
55
|
+
const isInLifecycle = triggers.some(t => lifecycleFiles.has(t.file)) || (facts['LIFECYCLE_CONTEXT'] && facts['LIFECYCLE_CONTEXT'].length > 0);
|
|
76
56
|
if (isInLifecycle) {
|
|
77
|
-
multiplier
|
|
57
|
+
multiplier *= 2.0;
|
|
78
58
|
}
|
|
79
59
|
|
|
80
60
|
// Encoder Multiplier (1.5x)
|
|
@@ -84,18 +64,18 @@ class SafetyEngine {
|
|
|
84
64
|
}
|
|
85
65
|
|
|
86
66
|
// Cluster Bonus: Source + Sink
|
|
87
|
-
const hasSource = triggers.some(t => t.type.includes('READ'));
|
|
88
|
-
const hasSink = triggers.some(t => t.type.includes('SINK') || t.type === 'DYNAMIC_EXECUTION' || t.type === 'SHELL_EXECUTION');
|
|
67
|
+
const hasSource = triggers.some(t => t.type.includes('READ') || t.type.includes('ACCESS'));
|
|
68
|
+
const hasSink = triggers.some(t => t.type.includes('SINK') || t.type === 'DYNAMIC_EXECUTION' || t.type === 'SHELL_EXECUTION' || t.type === 'DYNAMIC_REQUIRE');
|
|
89
69
|
if (hasSource && hasSink) {
|
|
90
70
|
baseScore += 40;
|
|
91
71
|
}
|
|
92
72
|
|
|
93
73
|
let finalScore = baseScore * multiplier;
|
|
94
74
|
|
|
95
|
-
// Severe Cluster:
|
|
96
|
-
const
|
|
97
|
-
const isDangerousSink = triggers.some(t => t.type === 'NETWORK_SINK' || t.type === 'SHELL_EXECUTION');
|
|
98
|
-
if (
|
|
75
|
+
// Severe Cluster: SENSITIVE_READ + Dangerous Sink + lifecycleContext = Critical (100)
|
|
76
|
+
const isSensitiveRead = triggers.some(t => t.type === 'ENV_READ' || t.type === 'FILE_READ_SENSITIVE');
|
|
77
|
+
const isDangerousSink = triggers.some(t => t.type === 'NETWORK_SINK' || t.type === 'DNS_SINK' || t.type === 'RAW_SOCKET_SINK' || t.type === 'SHELL_EXECUTION');
|
|
78
|
+
if (isSensitiveRead && isDangerousSink && isInLifecycle) {
|
|
99
79
|
finalScore = 100;
|
|
100
80
|
}
|
|
101
81
|
|
|
@@ -103,6 +83,7 @@ class SafetyEngine {
|
|
|
103
83
|
id: rule.id,
|
|
104
84
|
alias: rule.alias,
|
|
105
85
|
name: rule.name,
|
|
86
|
+
priority: rule.priority || 1,
|
|
106
87
|
score: Math.min(finalScore, 100),
|
|
107
88
|
triggers: triggers,
|
|
108
89
|
description: rule.description,
|
package/src/rules/definitions.js
CHANGED
|
@@ -5,6 +5,7 @@ const RULES = [
|
|
|
5
5
|
name: 'Environment Variable Exfiltration',
|
|
6
6
|
requires: ['ENV_READ', 'NETWORK_SINK'],
|
|
7
7
|
optional: ['OBFUSCATION'],
|
|
8
|
+
priority: 1,
|
|
8
9
|
baseScore: 40,
|
|
9
10
|
description: 'Detection of environment variables being read and sent over the network.'
|
|
10
11
|
},
|
|
@@ -13,6 +14,7 @@ const RULES = [
|
|
|
13
14
|
alias: 'SENSITIVE_FILE_EXFILTRATION',
|
|
14
15
|
name: 'Sensitive File Exfiltration',
|
|
15
16
|
requires: ['FILE_READ_SENSITIVE', 'NETWORK_SINK'],
|
|
17
|
+
priority: 1,
|
|
16
18
|
baseScore: 50,
|
|
17
19
|
description: 'Detection of sensitive files (e.g., .ssh, .env) being read and sent over the network.'
|
|
18
20
|
},
|
|
@@ -21,6 +23,7 @@ const RULES = [
|
|
|
21
23
|
alias: 'PERSISTENCE_ATTEMPT',
|
|
22
24
|
name: 'Persistence Attempt',
|
|
23
25
|
requires: ['FILE_WRITE_STARTUP'],
|
|
26
|
+
priority: 2,
|
|
24
27
|
baseScore: 60,
|
|
25
28
|
description: 'Detection of attempts to write to system startup directories.'
|
|
26
29
|
},
|
|
@@ -29,6 +32,7 @@ const RULES = [
|
|
|
29
32
|
alias: 'OBFUSCATED_EXECUTION',
|
|
30
33
|
name: 'Obfuscated Execution',
|
|
31
34
|
requires: ['OBFUSCATION', 'DYNAMIC_EXECUTION'],
|
|
35
|
+
priority: 2,
|
|
32
36
|
baseScore: 40,
|
|
33
37
|
description: 'Detection of high-entropy strings being executed via eval or Function constructor.'
|
|
34
38
|
},
|
|
@@ -38,6 +42,7 @@ const RULES = [
|
|
|
38
42
|
name: 'Shell Command Execution',
|
|
39
43
|
requires: ['SHELL_EXECUTION'],
|
|
40
44
|
optional: ['ENV_READ', 'FILE_READ_SENSITIVE'],
|
|
45
|
+
priority: 1,
|
|
41
46
|
baseScore: 50,
|
|
42
47
|
description: 'Detection of shell command execution (child_process).'
|
|
43
48
|
},
|
|
@@ -45,7 +50,8 @@ const RULES = [
|
|
|
45
50
|
id: 'ZFT-006',
|
|
46
51
|
alias: 'DYNAMIC_REQUIRE_DEPENDENCY',
|
|
47
52
|
name: 'Dynamic Require Dependency',
|
|
48
|
-
requires: ['
|
|
53
|
+
requires: ['DYNAMIC_REQUIRE'],
|
|
54
|
+
priority: 1,
|
|
49
55
|
baseScore: 30,
|
|
50
56
|
description: 'Detection of dynamic require calls where the dependency name is a variable.'
|
|
51
57
|
},
|
|
@@ -53,7 +59,8 @@ const RULES = [
|
|
|
53
59
|
id: 'ZFT-007',
|
|
54
60
|
alias: 'DNS_EXFILTRATION',
|
|
55
61
|
name: 'DNS-Based Exfiltration',
|
|
56
|
-
requires: ['ENV_READ', '
|
|
62
|
+
requires: ['ENV_READ', 'DNS_SINK'],
|
|
63
|
+
priority: 2,
|
|
57
64
|
baseScore: 45,
|
|
58
65
|
description: 'Stealthy environment variable exfiltration via DNS lookups.'
|
|
59
66
|
},
|
|
@@ -61,33 +68,37 @@ const RULES = [
|
|
|
61
68
|
id: 'ZFT-008',
|
|
62
69
|
alias: 'SUSPICIOUS_COLLECTION',
|
|
63
70
|
name: 'Suspicious Information Collection',
|
|
64
|
-
requires: ['
|
|
71
|
+
requires: ['MASS_ENV_ACCESS'],
|
|
65
72
|
optional: ['FILE_READ_SENSITIVE'],
|
|
73
|
+
priority: 1,
|
|
66
74
|
baseScore: 20,
|
|
67
|
-
description: 'Massive environment
|
|
75
|
+
description: 'Massive environment reading without immediate network activity (potential harvesting).'
|
|
68
76
|
},
|
|
69
77
|
{
|
|
70
78
|
id: 'ZFT-009',
|
|
71
79
|
alias: 'REMOTE_DROPPER_PATTERN',
|
|
72
80
|
name: 'Remote Script Dropper',
|
|
73
|
-
requires: ['SHELL_EXECUTION'],
|
|
74
|
-
optional: ['OBFUSCATION'],
|
|
81
|
+
requires: ['SHELL_EXECUTION', 'REMOTE_FETCH_SIGNAL'],
|
|
82
|
+
optional: ['OBFUSCATION', 'PIPE_TO_SHELL_SIGNAL'],
|
|
83
|
+
priority: 3,
|
|
75
84
|
baseScore: 55,
|
|
76
85
|
description: 'Detection of remote script download and execution (curl | sh) patterns.'
|
|
77
86
|
},
|
|
78
87
|
{
|
|
79
88
|
id: 'ZFT-010',
|
|
80
|
-
alias: '
|
|
81
|
-
name: '
|
|
82
|
-
requires: ['
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
alias: 'ENCODED_EXFILTRATION',
|
|
90
|
+
name: 'Encoded Data Exfiltration',
|
|
91
|
+
requires: ['ENV_READ', 'NETWORK_SINK', 'ENCODER_USE'],
|
|
92
|
+
priority: 3,
|
|
93
|
+
baseScore: 70,
|
|
94
|
+
description: 'Sensitive data encoded before transmission to evade detection.'
|
|
85
95
|
},
|
|
86
96
|
{
|
|
87
97
|
id: 'ZFT-011',
|
|
88
98
|
alias: 'RAW_SOCKET_TUNNEL',
|
|
89
99
|
name: 'Raw Socket Tunneling',
|
|
90
|
-
requires: ['
|
|
100
|
+
requires: ['RAW_SOCKET_SINK'],
|
|
101
|
+
priority: 2,
|
|
91
102
|
baseScore: 45,
|
|
92
103
|
description: 'Use of raw network sockets instead of http/dns, often used for reverse shells.'
|
|
93
104
|
},
|
|
@@ -95,17 +106,19 @@ const RULES = [
|
|
|
95
106
|
id: 'ZFT-012',
|
|
96
107
|
alias: 'STARTUP_SCRIPT_MOD',
|
|
97
108
|
name: 'Startup Script Modification',
|
|
98
|
-
requires: ['FILE_WRITE_STARTUP'],
|
|
109
|
+
requires: ['FILE_WRITE_STARTUP'],
|
|
110
|
+
priority: 2,
|
|
99
111
|
baseScore: 60,
|
|
100
112
|
description: 'Detection of attempts to modify package.json scripts or npm configuration.'
|
|
101
113
|
}
|
|
102
114
|
];
|
|
103
115
|
|
|
104
116
|
const CATEGORIES = {
|
|
105
|
-
SOURCES: ['ENV_READ', 'FILE_READ_SENSITIVE'],
|
|
106
|
-
SINKS: ['NETWORK_SINK', 'DYNAMIC_EXECUTION', 'SHELL_EXECUTION'],
|
|
107
|
-
SIGNALS: ['OBFUSCATION', 'ENCODER_USE'],
|
|
108
|
-
PERSISTENCE: ['FILE_WRITE_STARTUP']
|
|
117
|
+
SOURCES: ['ENV_READ', 'FILE_READ_SENSITIVE', 'MASS_ENV_ACCESS'],
|
|
118
|
+
SINKS: ['NETWORK_SINK', 'DNS_SINK', 'RAW_SOCKET_SINK', 'DYNAMIC_EXECUTION', 'SHELL_EXECUTION', 'DYNAMIC_REQUIRE'],
|
|
119
|
+
SIGNALS: ['OBFUSCATION', 'ENCODER_USE', 'REMOTE_FETCH_SIGNAL', 'PIPE_TO_SHELL_SIGNAL'],
|
|
120
|
+
PERSISTENCE: ['FILE_WRITE_STARTUP'],
|
|
121
|
+
CONTEXT: ['LIFECYCLE_CONTEXT']
|
|
109
122
|
};
|
|
110
123
|
|
|
111
124
|
module.exports = { RULES, CATEGORIES };
|
package/src/scanner.js
CHANGED
|
@@ -27,13 +27,20 @@ class PackageScanner {
|
|
|
27
27
|
let allFacts = {
|
|
28
28
|
facts: {
|
|
29
29
|
ENV_READ: [],
|
|
30
|
+
MASS_ENV_ACCESS: [],
|
|
30
31
|
FILE_READ_SENSITIVE: [],
|
|
31
32
|
NETWORK_SINK: [],
|
|
33
|
+
DNS_SINK: [],
|
|
34
|
+
RAW_SOCKET_SINK: [],
|
|
32
35
|
DYNAMIC_EXECUTION: [],
|
|
36
|
+
DYNAMIC_REQUIRE: [],
|
|
33
37
|
OBFUSCATION: [],
|
|
34
38
|
FILE_WRITE_STARTUP: [],
|
|
35
39
|
SHELL_EXECUTION: [],
|
|
36
|
-
ENCODER_USE: []
|
|
40
|
+
ENCODER_USE: [],
|
|
41
|
+
REMOTE_FETCH_SIGNAL: [],
|
|
42
|
+
PIPE_TO_SHELL_SIGNAL: [],
|
|
43
|
+
LIFECYCLE_CONTEXT: []
|
|
37
44
|
},
|
|
38
45
|
flows: []
|
|
39
46
|
};
|
|
@@ -78,6 +85,11 @@ class PackageScanner {
|
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
// Merge facts (Synchronized)
|
|
88
|
+
if (lifecycleFiles.has(file)) {
|
|
89
|
+
facts.LIFECYCLE_CONTEXT = facts.LIFECYCLE_CONTEXT || [];
|
|
90
|
+
facts.LIFECYCLE_CONTEXT.push({ file, reason: 'Lifecycle script context detected' });
|
|
91
|
+
}
|
|
92
|
+
|
|
81
93
|
for (const category in facts) {
|
|
82
94
|
if (allFacts.facts[category]) {
|
|
83
95
|
allFacts.facts[category].push(...facts[category]);
|