Repository navigation
Expand file tree
/
Copy pathbacktest-vlt.py
More file actions
1863 lines (1638 loc) · 88.4 KB
/
Copy pathbacktest-vlt.py
File metadata and controls
1863 lines (1638 loc) · 88.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""Native vlt / public Socket patch compatibility, with no service doubles.
Every (vlt release, mode, project shape) cell builds an isolated project with
a REAL vlt release (`npm pack`, sha512 checked against the registry, run as
`node vlt.js`), runs the socket-patch CLI against the public free
minimist@1.2.2 patch on the production service, and proves the result with
the same vlt. The verdict is checked against `expected_verdict`, an oracle of
the DOCUMENTED boundaries (docs/testing/vlt-compatibility.md, DESIGN §2.1),
never the CLI's own codes, so a CLI regression that refuses a supported cell
(or patches a refused one) fails the cell.
Modes: hosted (`scan|get --mode hosted`), vendored (`--mode vendored`) and
agent (`--mode agent`).
Checks (hosted): serveEncodingIdentity (the cell's own curl probe of the
artifact, fetched the way vlt fetches it), cliSuccess, publishedPatch,
referenceWritten, lockFormatPreserved, othersUntouched, freshCi /
freshOrdinary (cold caches: patched bytes, lock byte-stable), tamperedDigest
(a wrong slot [2] fails a cold `vlt ci`), vexManifestless, repeatStableLock,
warmOrdinary / warmCi (the project's own tree after the heal),
rollbackByteIdentical and rollbackOriginalBytes.
Checks (vendored): cliSuccess, publishedPatch, referenceWritten,
lockFormatPreserved, freshCi, freshOrdinary, vexManifestless,
repeatStableLock, warmOrdinary / warmCi (the project's own tree, which still
links the registry copy; from 0.0.0-30 `vlt install` keeps an optional one),
repair (a deleted payload is rebuilt and installs
patched),
rollbackByteIdentical (`vendor --revert`) and rollbackOriginalBytes.
Checks (agent): cliSuccess, manifestWritten, lockUnchanged, patchedBytes,
survivesNoopInstall, repeatStableLock, rollbackByteIdentical and
rollbackOriginalBytes.
Hosted cells whose expected verdict is `patched` first probe the artifact
(`accept-encoding: gzip;q=1.0, identity;q=0.5`, no decoding). While the
service re-encodes it, vlt would fail `EINTEGRITY`, so the only correct CLI
outcome is the clean refusal (`redirect_vlt_artifact_unverifiable`, nothing
written): the cell records `blocked-by-server-encoding`. That verdict is
never recorded without the probe seeing a non-identity `Content-Encoding`;
any other hosted failure is an error.
Rows are written in the shape of depscan's capture-vlt.py `result.json`
(`os, vlt, mode, shape, verdict, codes, manifestSha256, patches` plus the
`tree/` of SBOM inputs), so `audit-captures.py` and `generate-fixtures.py
--captures` read them directly.
`--identity-mirror` serves the production API and artifacts through a
loopback mirror that fetches the artifact with `accept-encoding: identity`
(the state the serve fix produces). Its rows name the mirror and pin
loopback URLs: a local preview of the hosted proof, never an import source.
`--downgrade-cli <published socket-patch>` runs the downgrade scenario
instead of the matrix: vlt ledgers written by `--cli` must be either left
untouched or fully reverted by the published release, never half-reverted.
`--canary-checks` runs the nightly canary's watchdogs: every published vlt
release is listed as supported or excluded, and a lock with a
`lockfileVersion` other than 0 or 1 is refused (`redirect_vlt_lock_unsupported`).
`--diff-locks DIR...` compares the `vlt-lock.json` of the same cell across
operating systems in downloaded result rows (they must be byte-identical).
`--serve-probe` is the serve watchdog: it fetches the public minimist
artifact the way vlt does and fails unless it is identity-encoded with the
API's sha512.
"""
import argparse
import base64
import concurrent.futures
from datetime import datetime, timezone
import gzip
import hashlib
import http.server
import json
import os
from pathlib import Path
import platform
import re
import shutil
import subprocess
import sys
import threading
import time
import urllib.error
import urllib.request
ROOT = Path(__file__).resolve().parents[1]
LEG_MANIFEST = ROOT / 'crates' / 'socket-patch-cli' / 'tests' / 'vlt-leg-manifest.json'
HISTORICAL_INTEGRITY = Path(__file__).with_name('vlt-historical-integrity.json')
VERSIONS = ['0.0.0-16', '0.0.0-32', '1.0.0-rc.14', '1.0.0-rc.32', '1.0.4', '1.0.10', '1.2.0']
MODES = ['hosted', 'vendored', 'agent']
PURL = 'pkg:npm/minimist@1.2.2'
UUID = '642d7f02-ebc1-4ab0-99e2-07f5dd8463cb'
NAME = 'minimist'
VERSION = '1.2.2'
TARGET = f'{NAME}@{VERSION}'
API_URL = 'https://patches-api.socket.dev'
PATCH_HOST = 'https://patch.socket.dev'
NPM_REGISTRY = 'https://registry.npmjs.org/'
# An alias whose URL is not the default registry (vlt treats an alias whose
# URL equals the lock's scalar `registry` as the default one).
ACME_REGISTRY = 'https://registry.yarnpkg.com/'
VLT_ACCEPT_ENCODING = 'gzip;q=1.0, identity;q=0.5'
BLOCKED = 'blocked-by-server-encoding'
BLOCKED_CODES = ['redirect_vlt_artifact_unverifiable']
VERDICTS = ['patched', 'safe-refusal', 'unsafe', 'error', 'unsupported', BLOCKED]
COMMAND_TIMEOUT = 600
VEX_PRODUCT = 'pkg:npm/vlt-patch-backtest@1.0.0'
OPTIONAL_HELD = 'unpatched copies of optional dependencies'
A0_SKIPPED_OPTIONAL = 'Could not read package.json file'
OPTIONAL_ONLY_LOCK = ('installs no optional dependency from the lock of a project that declares '
'only optional dependencies')
# ---------------------------------------------------------------------------
# Releases.
def version_key(version):
"""Semver precedence of a vlt release (`0.0.0-16`, `1.0.0-rc.14`, `1.2.0`)."""
match = re.fullmatch(r'(\d+)\.(\d+)\.(\d+)(?:-(rc\.)?(\d+))?', version)
if not match:
raise ValueError(f'not a vlt release version: {version!r}')
core = tuple(int(match.group(i)) for i in (1, 2, 3))
if match.group(5) is None:
return core + (1, 0)
return core + (0, int(match.group(5)))
def at_least(version, floor):
return version_key(version) >= version_key(floor)
def between(version, low, high):
return version_key(low) <= version_key(version) <= version_key(high)
def era_of(version):
"""DESIGN §1.1 era of the release that wrote the lock."""
for floor, era in (('1.2.0', 'F'), ('1.0.8', 'E'), ('1.0.0-rc.33', 'D'),
('1.0.0-rc.15', 'C'), ('1.0.0-rc.9', 'B'), ('0.0.0-19', 'A')):
if at_least(version, floor):
return era
return 'A0'
def lockfile_version(era):
return None if era == 'A0' else 0 if era in ('A', 'B') else 1
def release_lists(manifest_path=LEG_MANIFEST):
"""(supported, excluded) releases of the leg manifest, which is derived
from the Releases table of docs/testing/vlt-compatibility.md."""
data = json.loads(Path(manifest_path).read_text(encoding='utf-8'))
return list(data['supported']), list(data['excluded'])
def release_status(version, supported, excluded):
"""'supported', 'excluded' or 'unlisted' (DESIGN §1.1 exclusion list:
the named releases plus every `0.0.0-0.<timestamp>` build)."""
if version in excluded or re.fullmatch(r'0\.0\.0-0\.\d+', version):
return 'excluded'
return 'supported' if version in supported else 'unlisted'
def unlisted_releases(published, supported, excluded):
"""Published releases the matrix does not know: the canary watchdog."""
return [v for v in published if release_status(v, supported, excluded) == 'unlisted']
# ---------------------------------------------------------------------------
# Shapes (a subset of depscan's capture-vlt.py SHAPES, with the same names).
# The oracle follows the committed boundary table; capture-vlt.py does not yet
# match it for optional-only cells on 0.0.0-24 … 0.0.0-31. The production
# patch is minimist@1.2.2 only, so shapes that need a scoped, transitive-only
# or peer-bearing patch are left to the hermetic suites and the local capture.
def manifest(name='vlt-patch-backtest', **sections):
data = dict(name=name, version='1.0.0', private=True)
for key in ('dependencies', 'devDependencies', 'optionalDependencies'):
if sections.get(key):
data[key] = sections[key]
return json.dumps(data, indent=2) + '\n'
def shape(files, runtime, modes=MODES, members=(), **extra):
"""`files` maps a project path to package.json text; `runtime` lists the
(importer dir, dependency name) pairs whose installed copy must hold the
patched bytes."""
return dict(files=files, runtime=list(runtime), modes=list(modes), members=list(members),
**extra)
SHAPES = {
'direct': shape({'package.json': manifest(dependencies={NAME: VERSION})}, [('.', NAME)]),
'dev': shape({'package.json': manifest(devDependencies={NAME: VERSION})}, [('.', NAME)]),
'optional': shape({'package.json': manifest(optionalDependencies={NAME: VERSION})},
[('.', NAME)], optional=True, optional_only=True),
'optional-mixed': shape({'package.json': manifest(dependencies={'left-pad': '1.3.0'},
optionalDependencies={NAME: VERSION})},
[('.', NAME)], optional=True),
'alias': shape({'package.json': manifest(dependencies={'mm': f'npm:{TARGET}'})},
[('.', 'mm')], no_bare_ids=True),
'two-versions': shape({'package.json': manifest(
dependencies={NAME: VERSION, 'other': 'npm:minimist@1.2.8'})}, [('.', NAME)]),
'workspace': shape({'package.json': manifest(dependencies={NAME: VERSION}),
'packages/a/package.json': manifest('a', dependencies={NAME: VERSION})},
[('.', NAME), ('packages/a', NAME)], members=['packages/a']),
'workspace-member-vendored': shape(
{'package.json': manifest(),
'packages/a/package.json': manifest('a', dependencies={NAME: VERSION})},
[('packages/a', NAME)], modes=['vendored'], members=['packages/a']),
'crlf-lock': shape({'package.json': manifest(dependencies={NAME: VERSION})}, [('.', NAME)],
crlf_lock=True),
'custom-registry': shape({'package.json': manifest(dependencies={NAME: f'acme:{TARGET}'})},
[('.', NAME)], registries={'acme': ACME_REGISTRY},
no_bare_ids=True, from_version='0.0.0-14'),
'lockfile-only': shape({'package.json': manifest(dependencies={NAME: VERSION})},
[('.', NAME)], lockfile_only=True),
'hosted-then-vendored': shape({'package.json': manifest(dependencies={NAME: VERSION})},
[('.', NAME)], modes=['vendored'], before='hosted',
from_version='0.0.0-19'),
'vendored-then-hosted': shape({'package.json': manifest(dependencies={NAME: VERSION})},
[('.', NAME)], modes=['hosted'], before='vendored',
from_version='0.0.0-19'),
'get-uuid': shape({'package.json': manifest(dependencies={NAME: VERSION})}, [('.', NAME)],
driver='get'),
}
# ---------------------------------------------------------------------------
# The oracle: the documented boundaries only, never the CLI's codes.
def applicable(version, mode, shape_name):
spec = SHAPES[shape_name]
if mode not in spec['modes']:
return False
if spec.get('optional_only') and between(version, '0.0.0-24', '0.0.0-29'):
return False # these releases write no vlt-lock.json for an optional-only project
return 'from_version' not in spec or at_least(version, spec['from_version'])
def expected_verdict(version, mode, shape_name):
"""The verdict DESIGN §2.1 promises, or None when the cell is not part of
the matrix."""
if not applicable(version, mode, shape_name):
return None
spec = SHAPES[shape_name]
if mode == 'agent':
# Agent mode patches installed files: the scan lists a lockfile-only
# package and skips it.
return 'safe-refusal' if spec.get('lockfile_only') else 'patched'
if shape_name == 'custom-registry':
return 'safe-refusal'
if mode == 'vendored' and era_of(version) == 'A0':
return 'safe-refusal'
return 'patched'
def lock_era(version, shape_name):
"""The era the CLI sniffs from the lock: an era-A lock whose only
registry ids are `npm:` aliases or a named registry has no `··` id, which
is what tells A from B."""
era = era_of(version)
return 'B' if era == 'A' and SHAPES[shape_name].get('no_bare_ids') else era
def hosted_lock_warnings(version, shape_name):
"""The lock-level warnings DESIGN §3.8 derives from the lock and vlt.json
the cell writes (a default-valued `registry` never reaches the lock)."""
spec = SHAPES[shape_name]
era = era_of(version)
text = write_vlt_json(version, spec)
data = json.loads(text) if text else {}
settings = data.get('config', data)
registry = settings.get('registry')
scalar = isinstance(registry, str) and registry != NPM_REGISTRY
npm = isinstance((settings.get('registries') or {}).get('npm'), str)
v0 = lockfile_version(era) in (None, 0)
legacy_ids = era in ('A0', 'A') and (scalar or not spec.get('no_bare_ids'))
codes = []
if era == 'A0':
codes.append('redirect_vlt_lockfile_version_missing')
if v0 and legacy_ids and 'modifiers' not in data:
codes.append('redirect_vlt_old_lockfile_ignored')
if scalar and (v0 or not npm):
codes.append('redirect_vlt_scalar_registry_ignored')
return codes
def expected_codes(version, mode, shape_name):
"""Codes DESIGN §2.1/§2.3 require for the cell."""
if not applicable(version, mode, shape_name):
return []
era = lock_era(version, shape_name)
codes = []
if mode == 'agent':
if SHAPES[shape_name].get('lockfile_only'):
codes.append('package_not_installed')
elif mode == 'hosted':
codes += hosted_lock_warnings(version, shape_name)
if shape_name == 'custom-registry':
codes.append('redirect_vlt_custom_registry_skipped')
elif era == 'A0':
codes.append('vendor_lockfile_version_unsupported')
elif shape_name == 'custom-registry':
codes.append('vendor_lock_entry_unsupported')
elif era == 'A':
codes.append('vendor_vlt_legacy_lockfile')
return codes
def known_vlt_limitation(version, mode, shape_name):
"""The measured vlt limitation that may leave a must-patch cell
unproven (docs/testing/vlt-compatibility.md boundary table), or None."""
if mode in ('hosted', 'vendored') and SHAPES[shape_name].get('optional_only') and \
at_least(version, '0.0.0-30') and not at_least(version, '1.0.5'):
return OPTIONAL_ONLY_LOCK
return None
def optional_warm_kept(version, mode, shape_name):
"""From 0.0.0-30 a plain `vlt install` keeps an installed optional
dependency whose spec moved to the vendored `file:` directory; `vlt ci`
relinks it (boundary table)."""
return mode == 'vendored' and bool(SHAPES[shape_name].get('optional')) and at_least(
version, '0.0.0-30')
def integrity_enforced(version, optional, code, err, installed, patched_bytes_used):
"""The tampered checkout failed on the digest. vlt skips an optional
dependency that fails to fetch, and 0.0.0-16 then fails reading its
package.json: when the untampered checkout installed the patched bytes,
only the wrong digest kept the target out (depscan's capture-vlt.py
rule)."""
if code != 0 and 'integrity' in err.lower():
return True
skipped = code == 0 or (era_of(version) == 'A0' and A0_SKIPPED_OPTIONAL in err)
return bool(optional) and not installed and skipped and patched_bytes_used is True
def churn_exempt(version, mode, shape_name):
"""rc.14 rewrites an alias-named `file:` dependency's peer-edge bareSpec on
its first `ci` (DESIGN §8.3 byte-stability oracle)."""
return era_of(version) == 'B' and mode == 'vendored' and shape_name in ('alias',
'two-versions')
def cells(versions, modes, shapes):
return [(v, m, s) for v in versions for m in modes for s in shapes
if expected_verdict(v, m, s) is not None]
def observed_verdict(row):
if row.get('error'):
return 'error' if row.get('supported', True) else 'unsupported'
if row.get('blocked'):
return BLOCKED
if row.get('safeRefusal'):
return 'safe-refusal'
if row.get('passed'):
return 'patched'
if row.get('vltLimitations') and not row.get('failingChecks'):
return 'unsupported'
return 'unsafe'
def required_codes(row):
"""The codes the row must carry. A blocked cell's dep is withheld from
every rewriter (and a conversion never reaches its second run), so only
the refusal's own code is due, not the cell's lock-level or vendored
codes."""
if row.get('verdict') == BLOCKED:
return BLOCKED_CODES
return row.get('expectedCodes', [])
def matches_expectation(row):
"""Observed == expected. Two allowances: a must-patch hosted cell whose
probe saw a content-encoded artifact and whose CLI refused cleanly
(BLOCKED), and a must-patch cell whose only unproven checks are the
documented vlt limitation (`expectedLimitation`)."""
if not set(required_codes(row)) <= set(row.get('codes', [])):
return False
verdict, expected = row.get('verdict'), row.get('expectedVerdict')
if verdict == expected:
return True
if verdict == BLOCKED:
return expected == 'patched' and bool(row.get('blockedByProbe'))
return (expected == 'patched' and verdict == 'unsupported'
and bool(row.get('expectedLimitation')) and bool(row.get('vltLimitations'))
and not row.get('failingChecks') and not row.get('error'))
# ---------------------------------------------------------------------------
# vlt configuration per era (DESIGN §8.3 registry table).
def write_vlt_json(version, spec=None, registry=NPM_REGISTRY):
"""vlt.json text for the release, or None when none is needed.
≤ 0.0.0-13 flat keys; 0.0.0-14 … rc.6 `config.registry`; rc.7 … rc.29 the
public registry (lock-driven installs re-resolve from it anyway);
rc.30 … rc.32 `config.registry`; ≥ rc.33 `config.registries.npm` (plus
`config.registry` through 1.0.4). vlt strips a `registry` equal to its
built-in npmjs default from the lock, so that value is only written where
the release requires registry config (≥ rc.33)."""
spec = spec or {}
config = {}
registries = dict(spec.get('registries') or {})
if at_least(version, '1.0.0-rc.33'):
registries.setdefault('npm', registry)
if not at_least(version, '1.0.5'):
config['registry'] = registry
elif registry != NPM_REGISTRY:
hermetic = not between(version, '1.0.0-rc.7', '1.0.0-rc.29')
config['registry'] = registry if hermetic else NPM_REGISTRY
if registries:
config['registries'] = registries
flat = not at_least(version, '0.0.0-14')
data = dict(config) if flat else ({'config': config} if config else {})
if spec.get('members') and at_least(version, '0.0.0-13'):
data['workspaces'] = 'packages/*'
if between(version, '0.0.0-16', '0.0.0-24'):
data['modifiers'] = {}
return json.dumps(data) + '\n' if data else None
def project_files(version, spec, registry=NPM_REGISTRY):
files = dict(spec['files'])
text = write_vlt_json(version, spec, registry)
if text is not None:
files['vlt.json'] = text
if spec.get('members') and not at_least(version, '0.0.0-13'):
files['vlt-workspaces.json'] = json.dumps({'packages': 'packages/*'}) + '\n'
return files
# ---------------------------------------------------------------------------
# Processes, hashes and files.
def sha256(data):
return hashlib.sha256(data).hexdigest()
def git_hash(data):
return hashlib.sha256(f'blob {len(data)}\0'.encode() + data).hexdigest()
def sri_sha512(data):
return 'sha512-' + base64.b64encode(hashlib.sha512(data).digest()).decode()
def tail(text, limit=4000):
return text if len(text) <= limit else text[-limit:]
def save(path, value):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, indent=2, sort_keys=True) + '\n', encoding='utf-8')
def run(command, cwd, env, log, check=False, timeout=COMMAND_TIMEOUT):
started = time.time()
try:
proc = subprocess.run([str(c) for c in command], cwd=cwd, env=env, capture_output=True,
timeout=timeout)
code = proc.returncode
out = proc.stdout.decode(errors='replace')
err = proc.stderr.decode(errors='replace')
except subprocess.TimeoutExpired as error:
code = -9
out = (error.stdout or b'').decode(errors='replace')
err = f'timed out after {timeout}s'
log.parent.mkdir(parents=True, exist_ok=True)
with log.open('a', encoding='utf-8') as handle:
handle.write(f'$ (cd {cwd} && {" ".join(map(str, command))}) '
f'# exit {code} in {time.time() - started:.1f}s\n')
for text in (out, err):
handle.write(tail(text, 20000) + ('\n' if text and not text.endswith('\n') else ''))
if check and code != 0:
raise RuntimeError(f'{Path(str(command[0])).name} {" ".join(map(str, command[1:4]))} '
f'exited {code}: {tail(err or out, 2000)}')
return code, out, err
def line_endings(data):
crlf = data.count(b'\r\n')
lf = data.count(b'\n') - crlf
return 'crlf' if crlf and not lf else 'lf' if lf and not crlf else 'mixed' if crlf else 'none'
def to_crlf(data):
return data.replace(b'\r\n', b'\n').replace(b'\n', b'\r\n')
def skipped_dir(parts):
"""node_modules is never committed, except the vendored payload's own
`<leaf>/node_modules/<name>` dir under .socket/vendor/; neither is a
`.VLT.DELETE.<n>.<name>` dir vlt rc.14 deletes in the background."""
if any(part.startswith('.VLT.DELETE.') for part in parts):
return True
if 'node_modules' not in parts:
return False
if parts[:2] != ('.socket', 'vendor'):
return True
# vlt links a vendored dir's own dependencies inside it.
first = parts.index('node_modules')
return 'node_modules' in parts[first + 1:]
# The CLI's run lock, never committed.
UNCOMMITTED = {'.socket/apply.lock'}
def is_link(path):
"""A symlink, or a Windows junction (which `is_symlink` does not report)."""
return os.path.islink(path) or bool(getattr(os.path, 'isjunction', lambda _: False)(path))
def walk(project):
"""Every regular file under `project` outside the skipped node_modules
trees, never descending into a link or junction."""
for top, dirs, names in os.walk(project):
base = Path(top)
rel_top = base.relative_to(project).parts
dirs[:] = sorted(d for d in dirs if not is_link(base / d)
and not skipped_dir(rel_top + (d,)))
for name in sorted(names):
path = base / name
if not is_link(path) and path.is_file():
yield path, Path(*rel_top, name)
def snapshot(project):
"""The committable files: what a fresh checkout would hold."""
files = {}
for path, rel in walk(project):
if rel.as_posix() not in UNCOMMITTED:
files[rel.as_posix()] = path.read_bytes()
return dict(sorted(files.items()))
def captured(name):
"""The SBOM inputs depscan's fixtures import (DESIGN §7.3 tree list)."""
parts = name.split('/')
leaf = parts[-1]
if leaf in ('package.json', 'vlt-lock.json', 'vlt.json', 'vlt-workspaces.json'):
return True
if leaf == 'npm-shrinkwrap.json' and parts[:2] == ['.socket', 'vendor']:
return True
return name in ('.socket/manifest.json', '.socket/vendor/state.json',
'.socket/vendor/redirect-state.json')
def capture_tree(project, tree):
"""Copy the SBOM inputs into `tree`; return their sha256 by path."""
if tree.exists():
shutil.rmtree(tree)
digests = {}
for name, data in snapshot(project).items():
if not captured(name):
continue
destination = tree / name
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(data)
digests[name] = sha256(data)
return digests
def write_checkout(files, destination):
if destination.exists():
shutil.rmtree(destination)
for name, data in files.items():
path = destination / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
destination.mkdir(parents=True, exist_ok=True)
return destination
def remove_node_modules(project):
"""Delete every installed tree (the root's and each workspace member's),
never a vendored payload's own `node_modules/<name>` directory."""
for top, dirs, _ in os.walk(project):
base = Path(top)
rel_top = base.relative_to(project).parts
if rel_top[:2] == ('.socket', 'vendor'):
dirs[:] = []
continue
for d in list(dirs):
path = base / d
if d == 'node_modules':
dirs.remove(d)
if is_link(path):
os.unlink(path) if os.path.islink(path) else os.rmdir(path)
else:
shutil.rmtree(path, ignore_errors=True)
elif is_link(path):
dirs.remove(d)
# ---------------------------------------------------------------------------
# vlt-lock.json.
def split_dep_id(dep_id):
"""(registry segment, name, version, extra) of a registry DepID, or None.
Both grammars: `~<reg>~<name>@<ver>[~<extra>]` and the legacy
`·<reg>·<name>@<ver>[·<extra>]` (unscoped names only need no decoding;
scoped ones use `+` / `§` for `/`)."""
if not dep_id or dep_id[0] not in '~·':
return None
delim = dep_id[0]
parts = dep_id[1:].split(delim)
if len(parts) < 2:
return None
registry, spec = parts[0], parts[1]
extra = delim.join(parts[2:]) or None
spec = spec.replace('§' if delim == '·' else '+', '/')
at = spec.rfind('@')
if at <= 0:
return None
return registry, spec[:at], spec[at + 1:], extra
def target_instances(lock_text, any_registry=False):
"""DepIDs of minimist@1.2.2's registry nodes (default registry only
unless `any_registry`)."""
lock = json.loads(lock_text)
found = []
for dep_id in lock.get('nodes') or {}:
identity = split_dep_id(dep_id)
if not identity or identity[1:3] != (NAME, VERSION):
continue
if any_registry or identity[0] in ('', 'npm'):
found.append(dep_id)
return found
def tamper_lock(lock_bytes, dep_ids):
"""The lock with a wrong slot [2] on every `dep_ids` node line."""
wrong = sri_sha512(b'tampered by backtest-vlt.py')
out = []
for line in lock_bytes.split(b'\n'):
text = line.decode('utf-8')
for dep_id in dep_ids:
if text.lstrip().startswith(json.dumps(dep_id, ensure_ascii=False) + ':'):
text = re.sub(r'"sha512-[A-Za-z0-9+/=]+"', json.dumps(wrong), text, count=1)
out.append(text.encode('utf-8'))
return b'\n'.join(out)
# ---------------------------------------------------------------------------
# The production service.
def api_post(api_url, path, body, timeout=60):
request = urllib.request.Request(
api_url.rstrip('/') + path, data=json.dumps(body).encode(), method='POST',
headers={'content-type': 'application/json', 'accept': 'application/json',
'user-agent': 'socket-patch-backtest-vlt'})
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read())
def api_get(api_url, path, timeout=60):
request = urllib.request.Request(api_url.rstrip('/') + path, headers={
'accept': 'application/json', 'user-agent': 'socket-patch-backtest-vlt'})
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read())
def with_retries(fn, attempts=5):
for attempt in range(1, attempts + 1):
try:
return fn()
except (urllib.error.URLError, TimeoutError, ConnectionError, json.JSONDecodeError) as error:
final = isinstance(error, urllib.error.HTTPError) and error.code < 500 \
and error.code != 429
if final or attempt == attempts:
raise
time.sleep(5 * attempt)
def fetch_grant(api_url=API_URL):
"""The public tarball artifact of the patch: (url, sha512)."""
data = with_retries(lambda: api_post(api_url, '/patch/package',
{'uuids': [UUID], 'freeOnly': True}))
return select_tarball(data)
def select_tarball(data):
result = (data.get('results') or {}).get(UUID) or {}
if result.get('status') not in ('granted', 'reused'):
raise RuntimeError(f'patch {UUID} is not granted: {result.get("status")!r}')
for artifact in result.get('artifacts') or []:
if artifact.get('kind') == 'tarball':
sha512 = (artifact.get('integrity') or {}).get('sha512')
if not sha512:
raise RuntimeError('the tarball artifact has no sha512 integrity')
return artifact.get('url') or result.get('url'), sha512
raise RuntimeError('no tarball artifact in the grant')
def fetch_record(api_url=API_URL):
return with_retries(lambda: api_get(api_url, f'/patch/view/{UUID}'))
def parse_header_blocks(text):
"""The last response's headers of a `curl -D` dump (redirects add blocks)."""
blocks = [b for b in re.split(r'\r?\n\r?\n', text) if b.strip()]
if not blocks:
return None, {}
lines = blocks[-1].splitlines()
match = re.match(r'HTTP/\S+\s+(\d+)', lines[0]) if lines else None
headers = {}
for line in lines[1:]:
key, _, value = line.partition(':')
headers[key.strip().lower()] = value.strip()
return (int(match.group(1)) if match else None), headers
def encoding_is_identity(value):
return value is None or value.strip() == '' or value.strip().lower() == 'identity'
def probe_artifact(url, expected_sha512, workdir):
"""Fetch the artifact the way vlt does (DESIGN §1.9): its accept-encoding,
no Authorization, up to 10 redirects, no decoding; hash the wire body."""
workdir.mkdir(parents=True, exist_ok=True)
headers_file, body_file = workdir / 'probe-headers.txt', workdir / 'probe-body.bin'
code, _, err = run(['curl', '-sS', '-L', '--max-redirs', '10', '--max-time', '60',
'-D', headers_file, '-o', body_file,
'-H', f'accept-encoding: {VLT_ACCEPT_ENCODING}', url],
workdir, None, workdir / 'probe.log')
status, headers = parse_header_blocks(
headers_file.read_text(errors='replace') if headers_file.is_file() else '')
body = body_file.read_bytes() if body_file.is_file() else b''
body_file.unlink(missing_ok=True)
encoding = headers.get('content-encoding')
sha512 = sri_sha512(body) if body else None
return dict(url=url, curlExit=code, curlError=tail(err, 500) if code else None, status=status,
contentEncoding=encoding, cacheControl=headers.get('cache-control'),
sha512=sha512, expectedSha512=expected_sha512,
identity=code == 0 and status == 200 and encoding_is_identity(encoding),
verifies=code == 0 and status == 200 and encoding_is_identity(encoding)
and sha512 == expected_sha512)
class IdentityMirror:
"""A loopback view of production after the serve fix: API responses are
forwarded with the patch host rewritten to the mirror, and artifacts are
fetched with `accept-encoding: identity`, so they arrive un-encoded."""
def __init__(self, api_url=API_URL, patch_host=PATCH_HOST):
mirror = self
class Handler(http.server.BaseHTTPRequestHandler):
def forward(self, method):
length = int(self.headers.get('content-length') or 0)
data = self.rfile.read(length) if length else None
parts = self.path.split('?')[0].strip('/').split('/')
artifact = len(parts) >= 5 and parts[0] == 'patch'
base = patch_host if artifact else api_url
headers = {'user-agent': 'socket-patch-backtest-vlt-mirror',
'accept-encoding': 'identity'}
for key in ('content-type', 'accept'):
if self.headers.get(key):
headers[key] = self.headers[key]
request = urllib.request.Request(base + self.path, data=data, method=method,
headers=headers)
try:
with urllib.request.urlopen(request, timeout=120) as response:
body, status = response.read(), response.status
kind = response.headers.get('content-type', '')
encoding = response.headers.get('content-encoding')
except urllib.error.HTTPError as error:
body, status = error.read(), error.code
kind = error.headers.get('content-type', '')
encoding = error.headers.get('content-encoding')
if not encoding_is_identity(encoding):
body = gzip.decompress(body)
if not artifact:
body = body.replace(patch_host.encode(), mirror.url.encode())
self.send_response(status)
self.send_header('content-type', kind or 'application/octet-stream')
self.send_header('content-length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
self.forward('GET')
def do_POST(self):
self.forward('POST')
def log_message(self, *_):
pass
self.server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler)
self.url = f'http://127.0.0.1:{self.server.server_address[1]}'
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
def __enter__(self):
self.thread.start()
return self
def __exit__(self, *_):
self.server.shutdown()
self.server.server_close()
# ---------------------------------------------------------------------------
# vlt releases.
def npm_command():
return shutil.which('npm') or shutil.which('npm.cmd') or 'npm'
def pinned_integrity(version):
if not HISTORICAL_INTEGRITY.is_file():
return None
return json.loads(HISTORICAL_INTEGRITY.read_text(encoding='utf-8')).get(version)
def install_tool(root, version, log=None):
"""`<root>/<version>/node_modules/vlt/vlt.js`: `npm pack` (retried), the
tarball's sha512 checked against the registry's `dist.integrity` and the
committed pin (fail closed), then a prefix install without scripts. An
existing install is reused after its `--version` check."""
directory = root / version
js = directory / 'node_modules' / 'vlt' / 'vlt.js'
log = log or root / f'install-{version}.log'
if not js.is_file():
directory.mkdir(parents=True, exist_ok=True)
env = dict(os.environ)
npm = npm_command()
tarball = None
for attempt in range(1, 6):
code, out, err = run([npm, 'pack', f'vlt@{version}', '--pack-destination', directory,
'--silent'], directory, env, log)
names = [n.strip() for n in out.splitlines() if n.strip().endswith('.tgz')]
if code == 0 and names:
tarball = directory / names[-1]
break
if attempt == 5:
raise RuntimeError(f'npm pack vlt@{version} failed: {tail(err or out, 1000)}')
time.sleep(5 * attempt)
_, out, _ = run([npm, 'view', f'vlt@{version}', 'dist.integrity'], directory, env, log,
check=True)
expected = out.strip()
actual = sri_sha512(tarball.read_bytes())
pinned = pinned_integrity(version)
if actual != expected or (pinned is not None and pinned != expected):
raise RuntimeError(f'vlt@{version} tarball {actual} != registry {expected}'
+ (f' / pinned {pinned}' if pinned else ''))
run([npm, 'install', '--prefix', directory, '--no-audit', '--no-fund', '--ignore-scripts',
'--no-package-lock', tarball], directory, env, log, check=True)
tarball.unlink(missing_ok=True)
_, out, _ = run(['node', '--no-warnings', js, '--version'], directory, dict(os.environ), log,
check=True)
if out.strip() != version:
raise RuntimeError(f'expected vlt {version} at {js}, got {out.strip()!r}')
return js
class Vlt:
"""One vlt release, run as `node --no-warnings vlt.js` with private state."""
def __init__(self, js, version):
self.js = Path(js)
self.version = version
self.sha256 = sha256(self.js.read_bytes())
def env(self, home):
env = {k: v for k, v in os.environ.items()
if not k.upper().startswith(('VLT_', 'NPM_CONFIG_', 'XDG_', 'SOCKET_'))}
for key, sub in (('XDG_CACHE_HOME', 'cache'), ('XDG_CONFIG_HOME', 'config'),
('XDG_DATA_HOME', 'data'), ('XDG_STATE_HOME', 'state'),
('XDG_RUNTIME_DIR', 'runtime')):
(home / sub).mkdir(parents=True, exist_ok=True)
env[key] = str(home / sub)
env.update(HOME=str(home), USERPROFILE=str(home), VLT_CACHE=str(home / 'cache' / 'vlt'),
VLT_TELEMETRY='0', CI='1', NO_COLOR='1', LANG='C', LC_ALL='C')
return env
def run(self, args, cwd, home, log):
return run(['node', '--no-warnings', self.js, *args], cwd, self.env(home), log)
def locked_install(self):
return ['ci'] if at_least(self.version, '0.0.0-19') else ['install']
def reinstall(self, project, home, log):
"""Replace node_modules from the lock: `vlt ci`, or before it exists,
delete node_modules and `vlt install`."""
if not at_least(self.version, '0.0.0-19'):
remove_node_modules(project)
return self.run(self.locked_install(), project, home, log)
# ---------------------------------------------------------------------------
# CLI envelopes.
def parse_envelope(output):
start = output.find('{')
if start < 0:
return None
try:
return json.loads(output[start:])
except json.JSONDecodeError:
return None
def envelope_entries(envelope):
"""`(code, detail)` for every coded entry of a v5 envelope: `error`
(`{code, message}`), every event with an `errorCode` (its `reason`, or its
`error` for a `failed` event, as the detail), every top-level
`warnings[]` `{code, detail}` and, on a `--vex` run, `vex.warnings`."""
if not isinstance(envelope, dict):
return []
entries = []
error = envelope.get('error')
if isinstance(error, dict) and error.get('code'):
entries.append((error['code'], error.get('message')))
for event in envelope.get('events') or []:
if event.get('errorCode'):
entries.append((event['errorCode'], event.get('reason') or event.get('error')))
for warning in [*(envelope.get('warnings') or []),
*((envelope.get('vex') or {}).get('warnings') or [])]:
if isinstance(warning, dict) and warning.get('code'):
entries.append((warning['code'], warning.get('detail')))
return entries
def all_codes(value):
"""Every `code` / `errorCode` string anywhere in a JSON document, depth
first (the Rust tests' `all_codes`): for a PUBLISHED release's output,
which may predate the v5 envelope."""
if isinstance(value, dict):
return [c for k, v in value.items()
for c in ([v] if k in ('code', 'errorCode') and isinstance(v, str) else [])
+ all_codes(v)]
if isinstance(value, list):
return [c for item in value for c in all_codes(item)]
return []
def envelope_codes(envelope):
"""Every code the envelope reports (see `envelope_entries`)."""
return [code for code, _ in envelope_entries(envelope)]
def envelope_details(envelope, code):
"""The detail strings of every `code` entry (see `envelope_entries`)."""
return [detail for c, detail in envelope_entries(envelope)
if c == code and isinstance(detail, str)]
def vex_statements(doc, purl):
def about(statement):
return any(str(sub.get('@id', '')).split('?')[0] == purl
for product in statement.get('products', [])
for sub in product.get('subcomponents', []))
return [st for st in (doc or {}).get('statements', []) if about(st)]
def vex_attested(doc, purl, uuid, marker, vulns):
"""`doc` attests `purl` for exactly the ids of `vulns` ({id: [cves]}),
each not_affected, with every alias and the `Patched via Socket patch
<uuid> (<marker>)` impact part."""
statements = vex_statements(doc, purl)
if not vulns or sorted(st['vulnerability']['name'] for st in statements) != sorted(vulns):
return False
part = f'Patched via Socket patch {uuid} ({marker})'
for st in statements:
aliases = set(st['vulnerability'].get('aliases', []))
if (st.get('status') != 'not_affected'
or not set(vulns[st['vulnerability']['name']]) <= aliases
or part not in str(st.get('impact_statement', '')).split('; ')):
return False
return True
def record_vulns(record):
return {vid: list(v.get('cves', [])) for vid, v in (record.get('vulnerabilities') or {}).items()}
def cli_env(extra=None):
env = {k: v for k, v in os.environ.items()
if not k.upper().startswith(('SOCKET_', 'VLT_', 'NPM_CONFIG_'))}
env.update(SOCKET_NO_CONFIG='1', SOCKET_NO_UPDATE_CHECK='1', NO_COLOR='1', LANG='C',
LC_ALL='C')
env.update(extra or {})
return env
# ---------------------------------------------------------------------------
# One cell.
class Cell:
def __init__(self, ctx, version, mode, shape_name):
self.ctx = ctx
self.version = version
self.mode = mode
self.shape_name = shape_name