@svrnsec/pulse 0.6.0 → 0.8.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.
Files changed (48) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +883 -622
  3. package/SECURITY.md +86 -86
  4. package/bin/svrnsec-pulse.js +7 -7
  5. package/dist/{pulse.cjs.js → pulse.cjs} +6379 -6420
  6. package/dist/pulse.cjs.map +1 -0
  7. package/dist/pulse.esm.js +6380 -6421
  8. package/dist/pulse.esm.js.map +1 -1
  9. package/index.d.ts +895 -846
  10. package/package.json +185 -165
  11. package/pkg/pulse_core.js +174 -173
  12. package/src/analysis/audio.js +213 -213
  13. package/src/analysis/authenticityAudit.js +408 -390
  14. package/src/analysis/coherence.js +502 -502
  15. package/src/analysis/coordinatedBehavior.js +825 -0
  16. package/src/analysis/heuristic.js +428 -428
  17. package/src/analysis/jitter.js +446 -446
  18. package/src/analysis/llm.js +473 -472
  19. package/src/analysis/populationEntropy.js +404 -403
  20. package/src/analysis/provider.js +248 -248
  21. package/src/analysis/refraction.js +392 -0
  22. package/src/analysis/trustScore.js +356 -356
  23. package/src/cli/args.js +36 -36
  24. package/src/cli/commands/scan.js +192 -192
  25. package/src/cli/runner.js +157 -157
  26. package/src/collector/adaptive.js +200 -200
  27. package/src/collector/bio.js +297 -287
  28. package/src/collector/canvas.js +247 -239
  29. package/src/collector/dram.js +203 -203
  30. package/src/collector/enf.js +311 -311
  31. package/src/collector/entropy.js +195 -195
  32. package/src/collector/gpu.js +248 -245
  33. package/src/collector/idleAttestation.js +480 -480
  34. package/src/collector/sabTimer.js +189 -191
  35. package/src/fingerprint.js +475 -475
  36. package/src/index.js +342 -342
  37. package/src/integrations/react-native.js +462 -459
  38. package/src/integrations/react.js +184 -185
  39. package/src/middleware/express.js +155 -155
  40. package/src/middleware/next.js +174 -175
  41. package/src/proof/challenge.js +249 -249
  42. package/src/proof/engagementToken.js +426 -394
  43. package/src/proof/fingerprint.js +268 -268
  44. package/src/proof/validator.js +83 -143
  45. package/src/registry/serializer.js +349 -349
  46. package/src/terminal.js +263 -263
  47. package/src/update-notifier.js +259 -264
  48. package/dist/pulse.cjs.js.map +0 -1
@@ -1,200 +1,200 @@
1
- /**
2
- * @sovereign/pulse — Adaptive Entropy Probe
3
- *
4
- * Runs the WASM probe in batches and stops early once the signal is decisive.
5
- *
6
- * Why this works:
7
- * A KVM VM with QE=1.27 and lag-1 autocorr=0.67 is unambiguously a VM after
8
- * just 50 iterations. Running 200 iterations confirms what was already obvious
9
- * at 50 — it adds no new information but wastes 3 seconds of user time.
10
- *
11
- * Conversely, a physical device with healthy entropy needs more data to
12
- * rule out edge cases, so it runs longer.
13
- *
14
- * Speed profile:
15
- * Obvious VM (QE < 1.5, lag1 > 0.60) → stops at 50 iters → ~0.9s (75% faster)
16
- * Clear HW (QE > 3.5, lag1 < 0.10) → stops at ~100 iters → ~1.8s (50% faster)
17
- * Ambiguous (borderline metrics) → runs full 200 iters → ~3.5s (same)
18
- */
19
-
20
- import { detectQuantizationEntropy } from '../analysis/jitter.js';
21
-
22
- // ---------------------------------------------------------------------------
23
- // Quick classifier (cheap, runs after every batch)
24
- // ---------------------------------------------------------------------------
25
-
26
- /**
27
- * Fast signal-quality check. No Hurst, no thermal analysis — just the three
28
- * metrics that converge quickest: QE, CV, and lag-1 autocorrelation.
29
- *
30
- * @param {number[]} timings
31
- * @returns {{ vmConf: number, hwConf: number, qe: number, cv: number, lag1: number }}
32
- */
33
- export function quickSignal(timings) {
34
- const n = timings.length;
35
- const mean = timings.reduce((s, v) => s + v, 0) / n;
36
- const variance = timings.reduce((s, v) => s + (v - mean) ** 2, 0) / n;
37
- const cv = mean > 0 ? Math.sqrt(variance) / mean : 0;
38
- const qe = detectQuantizationEntropy(timings);
39
-
40
- // Pearson autocorrelation at lag-1 (O(n), fits in a single pass)
41
- let num = 0, da = 0, db = 0;
42
- for (let i = 0; i < n - 1; i++) {
43
- const a = timings[i] - mean;
44
- const b = timings[i + 1] - mean;
45
- num += a * b;
46
- da += a * a;
47
- db += b * b;
48
- }
49
- const lag1 = Math.sqrt(da * db) < 1e-14 ? 0 : num / Math.sqrt(da * db);
50
-
51
- // VM confidence: each factor independently identifies the hypervisor footprint
52
- const vmConf = Math.min(1,
53
- (qe < 1.50 ? 0.40 : qe < 2.00 ? 0.20 : 0.0) +
54
- (lag1 > 0.60 ? 0.35 : lag1 > 0.40 ? 0.18 : 0.0) +
55
- (cv < 0.04 ? 0.25 : cv < 0.07 ? 0.10 : 0.0)
56
- );
57
-
58
- // HW confidence: must see all three positive signals together
59
- const hwConf = Math.min(1,
60
- (qe > 3.50 ? 0.38 : qe > 3.00 ? 0.22 : 0.0) +
61
- (Math.abs(lag1) < 0.10 ? 0.32 : Math.abs(lag1) < 0.20 ? 0.15 : 0.0) +
62
- (cv > 0.10 ? 0.30 : cv > 0.07 ? 0.14 : 0.0)
63
- );
64
-
65
- return { vmConf, hwConf, qe, cv, lag1 };
66
- }
67
-
68
- // ---------------------------------------------------------------------------
69
- // collectEntropyAdaptive
70
- // ---------------------------------------------------------------------------
71
-
72
- /**
73
- * @param {object} opts
74
- * @param {number} [opts.minIterations=50] - never stop before this
75
- * @param {number} [opts.maxIterations=200] - hard cap
76
- * @param {number} [opts.batchSize=25] - WASM call granularity
77
- * @param {number} [opts.vmThreshold=0.85] - stop early if VM confidence ≥ this
78
- * @param {number} [opts.hwThreshold=0.80] - stop early if HW confidence ≥ this
79
- * @param {number} [opts.hwMinIterations=75] - physical needs more data to confirm
80
- * @param {number} [opts.matrixSize=64]
81
- * @param {Function} [opts.onBatch] - called after each batch with interim signal
82
- * @param {string} [opts.wasmPath]
83
- * @param {Function} wasmModule - pre-initialised WASM module
84
- * @returns {Promise<AdaptiveEntropyResult>}
85
- */
86
- export async function collectEntropyAdaptive(wasmModule, opts = {}) {
87
- const {
88
- minIterations = 50,
89
- maxIterations = 200,
90
- batchSize = 25,
91
- vmThreshold = 0.85,
92
- hwThreshold = 0.80,
93
- hwMinIterations = 75,
94
- matrixSize = 64,
95
- onBatch,
96
- } = opts;
97
-
98
- const wasm = wasmModule;
99
- const allTimings = [];
100
- const batches = []; // per-batch timing snapshots
101
- let stoppedAt = null; // { reason, iterations, vmConf, hwConf }
102
- let checksum = 0;
103
-
104
- const t_start = Date.now();
105
-
106
- while (allTimings.length < maxIterations) {
107
- const n = Math.min(batchSize, maxIterations - allTimings.length);
108
- const result = wasm.run_entropy_probe(n, matrixSize);
109
- const chunk = Array.from(result.timings);
110
-
111
- allTimings.push(...chunk);
112
- checksum += result.checksum;
113
-
114
- const sig = quickSignal(allTimings);
115
- batches.push({ iterations: allTimings.length, ...sig });
116
-
117
- // Fire progress callback with live signal so callers can stream to UI
118
- if (typeof onBatch === 'function') {
119
- try {
120
- onBatch({
121
- iterations: allTimings.length,
122
- maxIterations,
123
- pct: Math.round(allTimings.length / maxIterations * 100),
124
- vmConf: sig.vmConf,
125
- hwConf: sig.hwConf,
126
- qe: sig.qe,
127
- cv: sig.cv,
128
- lag1: sig.lag1,
129
- // Thresholds: 0.70 — high enough that a legitimate device won't be
130
- // shown a false early verdict from a noisy first batch.
131
- // 'borderline' surfaces when one axis is moderate but not decisive.
132
- earlyVerdict: sig.vmConf > 0.70 ? 'vm'
133
- : sig.hwConf > 0.70 ? 'physical'
134
- : (sig.vmConf > 0.45 || sig.hwConf > 0.45) ? 'borderline'
135
- : 'uncertain',
136
- });
137
- } catch {}
138
- }
139
-
140
- // ── Early-exit checks ──────────────────────────────────────────────────
141
- if (allTimings.length < minIterations) continue;
142
-
143
- if (sig.vmConf >= vmThreshold) {
144
- stoppedAt = { reason: 'VM_SIGNAL_DECISIVE', vmConf: sig.vmConf, hwConf: sig.hwConf };
145
- break;
146
- }
147
-
148
- if (allTimings.length >= hwMinIterations && sig.hwConf >= hwThreshold) {
149
- stoppedAt = { reason: 'PHYSICAL_SIGNAL_DECISIVE', vmConf: sig.vmConf, hwConf: sig.hwConf };
150
- break;
151
- }
152
- }
153
-
154
- const elapsed = Date.now() - t_start;
155
- const iterationsRan = allTimings.length;
156
- const iterationsSaved = maxIterations - iterationsRan;
157
- const speedupFactor = maxIterations / iterationsRan;
158
-
159
- // ── Resolution probe using cached WASM call ────────────────────────────
160
- const resResult = wasm.run_entropy_probe(1, 4); // tiny probe for resolution
161
- const resProbe = Array.from(resResult.resolution_probe ?? []);
162
-
163
- const resDeltas = [];
164
- for (let i = 1; i < resProbe.length; i++) {
165
- const d = resProbe[i] - resProbe[i - 1];
166
- if (d > 0) resDeltas.push(d);
167
- }
168
-
169
- return {
170
- timings: allTimings,
171
- iterations: iterationsRan,
172
- maxIterations,
173
- checksum: checksum.toString(),
174
- resolutionProbe: resProbe,
175
- timerGranularityMs: resDeltas.length
176
- ? resDeltas.reduce((a, b) => Math.min(a, b), Infinity)
177
- : null,
178
- earlyExit: stoppedAt ? {
179
- ...stoppedAt,
180
- iterationsSaved,
181
- timeSavedMs: Math.round(iterationsSaved * (elapsed / iterationsRan)),
182
- speedupFactor: +speedupFactor.toFixed(2),
183
- } : null,
184
- batches,
185
- elapsedMs: elapsed,
186
- collectedAt: t_start,
187
- matrixSize,
188
- phased: false, // adaptive replaces phased for speed
189
- };
190
- }
191
-
192
- /**
193
- * @typedef {object} AdaptiveEntropyResult
194
- * @property {number[]} timings
195
- * @property {number} iterations - how many actually ran
196
- * @property {number} maxIterations - cap that was set
197
- * @property {object|null} earlyExit - null if ran to completion
198
- * @property {object[]} batches - per-batch signal snapshots
199
- * @property {number} elapsedMs
200
- */
1
+ /**
2
+ * @svrnsec/pulse — Adaptive Entropy Probe
3
+ *
4
+ * Runs the WASM probe in batches and stops early once the signal is decisive.
5
+ *
6
+ * Why this works:
7
+ * A KVM VM with QE=1.27 and lag-1 autocorr=0.67 is unambiguously a VM after
8
+ * just 50 iterations. Running 200 iterations confirms what was already obvious
9
+ * at 50 — it adds no new information but wastes 3 seconds of user time.
10
+ *
11
+ * Conversely, a physical device with healthy entropy needs more data to
12
+ * rule out edge cases, so it runs longer.
13
+ *
14
+ * Speed profile:
15
+ * Obvious VM (QE < 1.5, lag1 > 0.60) → stops at 50 iters → ~0.9s (75% faster)
16
+ * Clear HW (QE > 3.5, lag1 < 0.10) → stops at ~100 iters → ~1.8s (50% faster)
17
+ * Ambiguous (borderline metrics) → runs full 200 iters → ~3.5s (same)
18
+ */
19
+
20
+ import { detectQuantizationEntropy } from '../analysis/jitter.js';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Quick classifier (cheap, runs after every batch)
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Fast signal-quality check. No Hurst, no thermal analysis — just the three
28
+ * metrics that converge quickest: QE, CV, and lag-1 autocorrelation.
29
+ *
30
+ * @param {number[]} timings
31
+ * @returns {{ vmConf: number, hwConf: number, qe: number, cv: number, lag1: number }}
32
+ */
33
+ export function quickSignal(timings) {
34
+ const n = timings.length;
35
+ const mean = timings.reduce((s, v) => s + v, 0) / n;
36
+ const variance = timings.reduce((s, v) => s + (v - mean) ** 2, 0) / n;
37
+ const cv = mean > 0 ? Math.sqrt(variance) / mean : 0;
38
+ const qe = detectQuantizationEntropy(timings);
39
+
40
+ // Pearson autocorrelation at lag-1 (O(n), fits in a single pass)
41
+ let num = 0, da = 0, db = 0;
42
+ for (let i = 0; i < n - 1; i++) {
43
+ const a = timings[i] - mean;
44
+ const b = timings[i + 1] - mean;
45
+ num += a * b;
46
+ da += a * a;
47
+ db += b * b;
48
+ }
49
+ const lag1 = Math.sqrt(da * db) < 1e-14 ? 0 : num / Math.sqrt(da * db);
50
+
51
+ // VM confidence: each factor independently identifies the hypervisor footprint
52
+ const vmConf = Math.min(1,
53
+ (qe < 1.50 ? 0.40 : qe < 2.00 ? 0.20 : 0.0) +
54
+ (lag1 > 0.60 ? 0.35 : lag1 > 0.40 ? 0.18 : 0.0) +
55
+ (cv < 0.04 ? 0.25 : cv < 0.07 ? 0.10 : 0.0)
56
+ );
57
+
58
+ // HW confidence: must see all three positive signals together
59
+ const hwConf = Math.min(1,
60
+ (qe > 3.50 ? 0.38 : qe > 3.00 ? 0.22 : 0.0) +
61
+ (Math.abs(lag1) < 0.10 ? 0.32 : Math.abs(lag1) < 0.20 ? 0.15 : 0.0) +
62
+ (cv > 0.10 ? 0.30 : cv > 0.07 ? 0.14 : 0.0)
63
+ );
64
+
65
+ return { vmConf, hwConf, qe, cv, lag1 };
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // collectEntropyAdaptive
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /**
73
+ * @param {object} opts
74
+ * @param {number} [opts.minIterations=50] - never stop before this
75
+ * @param {number} [opts.maxIterations=200] - hard cap
76
+ * @param {number} [opts.batchSize=25] - WASM call granularity
77
+ * @param {number} [opts.vmThreshold=0.85] - stop early if VM confidence ≥ this
78
+ * @param {number} [opts.hwThreshold=0.80] - stop early if HW confidence ≥ this
79
+ * @param {number} [opts.hwMinIterations=75] - physical needs more data to confirm
80
+ * @param {number} [opts.matrixSize=64]
81
+ * @param {Function} [opts.onBatch] - called after each batch with interim signal
82
+ * @param {string} [opts.wasmPath]
83
+ * @param {Function} wasmModule - pre-initialised WASM module
84
+ * @returns {Promise<AdaptiveEntropyResult>}
85
+ */
86
+ export async function collectEntropyAdaptive(wasmModule, opts = {}) {
87
+ const {
88
+ minIterations = 50,
89
+ maxIterations = 200,
90
+ batchSize = 25,
91
+ vmThreshold = 0.85,
92
+ hwThreshold = 0.80,
93
+ hwMinIterations = 75,
94
+ matrixSize = 64,
95
+ onBatch,
96
+ } = opts;
97
+
98
+ const wasm = wasmModule;
99
+ const allTimings = [];
100
+ const batches = []; // per-batch timing snapshots
101
+ let stoppedAt = null; // { reason, iterations, vmConf, hwConf }
102
+ let checksum = 0;
103
+
104
+ const t_start = Date.now();
105
+
106
+ while (allTimings.length < maxIterations) {
107
+ const n = Math.min(batchSize, maxIterations - allTimings.length);
108
+ const result = wasm.run_entropy_probe(n, matrixSize);
109
+ const chunk = Array.from(result.timings);
110
+
111
+ allTimings.push(...chunk);
112
+ checksum += result.checksum;
113
+
114
+ const sig = quickSignal(allTimings);
115
+ batches.push({ iterations: allTimings.length, ...sig });
116
+
117
+ // Fire progress callback with live signal so callers can stream to UI
118
+ if (typeof onBatch === 'function') {
119
+ try {
120
+ onBatch({
121
+ iterations: allTimings.length,
122
+ maxIterations,
123
+ pct: Math.round(allTimings.length / maxIterations * 100),
124
+ vmConf: sig.vmConf,
125
+ hwConf: sig.hwConf,
126
+ qe: sig.qe,
127
+ cv: sig.cv,
128
+ lag1: sig.lag1,
129
+ // Thresholds: 0.70 — high enough that a legitimate device won't be
130
+ // shown a false early verdict from a noisy first batch.
131
+ // 'borderline' surfaces when one axis is moderate but not decisive.
132
+ earlyVerdict: sig.vmConf > 0.70 ? 'vm'
133
+ : sig.hwConf > 0.70 ? 'physical'
134
+ : (sig.vmConf > 0.45 || sig.hwConf > 0.45) ? 'borderline'
135
+ : 'uncertain',
136
+ });
137
+ } catch (e) { if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') console.error('[pulse] onBatch error:', e); }
138
+ }
139
+
140
+ // ── Early-exit checks ──────────────────────────────────────────────────
141
+ if (allTimings.length < minIterations) continue;
142
+
143
+ if (sig.vmConf >= vmThreshold) {
144
+ stoppedAt = { reason: 'VM_SIGNAL_DECISIVE', vmConf: sig.vmConf, hwConf: sig.hwConf };
145
+ break;
146
+ }
147
+
148
+ if (allTimings.length >= hwMinIterations && sig.hwConf >= hwThreshold) {
149
+ stoppedAt = { reason: 'PHYSICAL_SIGNAL_DECISIVE', vmConf: sig.vmConf, hwConf: sig.hwConf };
150
+ break;
151
+ }
152
+ }
153
+
154
+ const elapsed = Date.now() - t_start;
155
+ const iterationsRan = allTimings.length;
156
+ const iterationsSaved = maxIterations - iterationsRan;
157
+ const speedupFactor = maxIterations / iterationsRan;
158
+
159
+ // ── Resolution probe using cached WASM call ────────────────────────────
160
+ const resResult = wasm.run_entropy_probe(1, 4); // tiny probe for resolution
161
+ const resProbe = Array.from(resResult.resolution_probe ?? []);
162
+
163
+ const resDeltas = [];
164
+ for (let i = 1; i < resProbe.length; i++) {
165
+ const d = resProbe[i] - resProbe[i - 1];
166
+ if (d > 0) resDeltas.push(d);
167
+ }
168
+
169
+ return {
170
+ timings: allTimings,
171
+ iterations: iterationsRan,
172
+ maxIterations,
173
+ checksum: checksum.toString(),
174
+ resolutionProbe: resProbe,
175
+ timerGranularityMs: resDeltas.length
176
+ ? resDeltas.reduce((a, b) => Math.min(a, b), Infinity)
177
+ : null,
178
+ earlyExit: stoppedAt ? {
179
+ ...stoppedAt,
180
+ iterationsSaved,
181
+ timeSavedMs: Math.round(iterationsSaved * (elapsed / iterationsRan)),
182
+ speedupFactor: +speedupFactor.toFixed(2),
183
+ } : null,
184
+ batches,
185
+ elapsedMs: elapsed,
186
+ collectedAt: t_start,
187
+ matrixSize,
188
+ phased: false, // adaptive replaces phased for speed
189
+ };
190
+ }
191
+
192
+ /**
193
+ * @typedef {object} AdaptiveEntropyResult
194
+ * @property {number[]} timings
195
+ * @property {number} iterations - how many actually ran
196
+ * @property {number} maxIterations - cap that was set
197
+ * @property {object|null} earlyExit - null if ran to completion
198
+ * @property {object[]} batches - per-batch signal snapshots
199
+ * @property {number} elapsedMs
200
+ */