@cdxgen/cdxgen 13.6.0 → 13.7.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 (264) hide show
  1. package/README.md +15 -8
  2. package/bin/audit.js +7 -4
  3. package/bin/cdxgen.js +256 -37
  4. package/bin/convert.js +146 -22
  5. package/bin/evinse.js +25 -3
  6. package/bin/repl.js +47 -3
  7. package/bin/sign.js +211 -34
  8. package/bin/tracebom.js +18 -6
  9. package/bin/validate.js +18 -3
  10. package/bin/verify.js +113 -5
  11. package/data/behavior-taxonomy.schema.json +1491 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
  13. package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
  14. package/data/pqc-migration.json +239 -0
  15. package/data/remediations.json +52 -0
  16. package/data/rules/cbom-security.yaml +4 -3
  17. package/data/rules/package-integrity.yaml +28 -0
  18. package/lib/audit/index.js +25 -3
  19. package/lib/cli/bomAssembly.js +80 -1
  20. package/lib/cli/cliOptions.js +121 -17
  21. package/lib/cli/index.js +609 -898
  22. package/lib/cli/jsBom.js +3 -0
  23. package/lib/cli/jvmBom.js +365 -40
  24. package/lib/cli/managedBom.js +19 -3
  25. package/lib/cli/nativeBom.js +10 -0
  26. package/lib/core/env.js +26 -0
  27. package/lib/core/fs.js +126 -3
  28. package/lib/core/globPatterns.js +218 -0
  29. package/lib/core/processTree.js +99 -12
  30. package/lib/core/projectConfig.js +332 -25
  31. package/lib/core/toolRegistry.js +15 -1
  32. package/lib/ecosystems/denoutils.js +2 -2
  33. package/lib/ecosystems/golem.js +3 -8
  34. package/lib/ecosystems/gradleutils.js +1082 -6
  35. package/lib/ecosystems/jsEvidence.js +90 -38
  36. package/lib/ecosystems/kosi.js +3 -5
  37. package/lib/ecosystems/npmVersionResolution.js +376 -0
  38. package/lib/ecosystems/parsers-python.js +89 -28
  39. package/lib/ecosystems/parsers-rust.js +117 -15
  40. package/lib/ecosystems/remote/huggingface.js +22 -0
  41. package/lib/ecosystems/rusi.js +2 -4
  42. package/lib/ecosystems/scalasem.js +2 -1
  43. package/lib/evinser/evinser.js +196 -34
  44. package/lib/evinser/scalasem.js +5 -1
  45. package/lib/helpers/agenticToolutils.js +112 -1
  46. package/lib/helpers/bomSigner.js +43 -9
  47. package/lib/helpers/jssSigner.js +1276 -0
  48. package/lib/inventory/agentBlueprint.js +193 -0
  49. package/lib/inventory/agentToolNames.js +145 -0
  50. package/lib/inventory/aiCollector.js +153 -7
  51. package/lib/inventory/aiInventory.js +44 -8
  52. package/lib/inventory/aiProviders.js +324 -0
  53. package/lib/inventory/bomUtils.js +8 -1
  54. package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
  55. package/lib/inventory/citations.js +31 -2
  56. package/lib/inventory/communityAiConfigParser.js +978 -80
  57. package/lib/inventory/cyclonedx20.js +152 -0
  58. package/lib/inventory/cyclonedx20Common.js +286 -0
  59. package/lib/inventory/cyclonedx20Derived.js +159 -0
  60. package/lib/inventory/cyclonedx20Parties.js +552 -0
  61. package/lib/inventory/cyclonedx20Read.js +768 -0
  62. package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
  63. package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
  64. package/lib/inventory/cyclonedx20ReadContext.js +380 -0
  65. package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
  66. package/lib/inventory/cyclonedx20ReadSections.js +599 -0
  67. package/lib/inventory/cyclonedx20Render.js +766 -0
  68. package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
  69. package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
  70. package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
  71. package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
  72. package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
  73. package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
  74. package/lib/inventory/cyclonedx20RenderSections.js +499 -0
  75. package/lib/inventory/cyclonedx20Tables.js +594 -0
  76. package/lib/inventory/deps.js +106 -13
  77. package/lib/inventory/dosai.js +236 -1
  78. package/lib/inventory/dosaiParsers.js +3 -6
  79. package/lib/inventory/dotnetRuntime.js +164 -14
  80. package/lib/inventory/ecosystemFormulationParsers.js +979 -0
  81. package/lib/inventory/envcontext.js +132 -38
  82. package/lib/inventory/evidenceUtils.js +2 -1
  83. package/lib/inventory/formulationParsers.js +95 -13
  84. package/lib/inventory/hbom.js +45 -6
  85. package/lib/inventory/hbomLoader.js +166 -0
  86. package/lib/inventory/mcpConfigParser.js +5 -0
  87. package/lib/inventory/occurrenceKey.js +90 -0
  88. package/lib/inventory/pqcMigration.js +426 -0
  89. package/lib/inventory/rustFormulationParser.js +21 -10
  90. package/lib/inventory/source.js +65 -6
  91. package/lib/parsers/huggingfaceManifest.js +9 -1
  92. package/lib/parsers/huggingfaceUtils.js +18 -0
  93. package/lib/parsers/kiroPermissions.js +166 -0
  94. package/lib/server/server.js +36 -34
  95. package/lib/stages/postgen/postgen.js +41 -1
  96. package/lib/stages/postgen/specVersionCompat.js +76 -0
  97. package/lib/validator/bomValidator.js +8 -1
  98. package/lib/validator/complianceEngine.js +30 -3
  99. package/lib/validator/complianceRules.js +1416 -4
  100. package/lib/validator/generated/validate-2.0.mjs +57554 -13851
  101. package/lib/validator/index.js +48 -7
  102. package/lib/validator/reporters/json.js +6 -0
  103. package/package.json +9 -9
  104. package/types/bin/repl.d.ts.map +1 -1
  105. package/types/lib/audit/index.d.ts +6 -2
  106. package/types/lib/audit/index.d.ts.map +1 -1
  107. package/types/lib/cli/bomAssembly.d.ts +16 -0
  108. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  109. package/types/lib/cli/cliOptions.d.ts +6 -0
  110. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  111. package/types/lib/cli/index.d.ts.map +1 -1
  112. package/types/lib/cli/jsBom.d.ts +2 -1
  113. package/types/lib/cli/jsBom.d.ts.map +1 -1
  114. package/types/lib/cli/jvmBom.d.ts +15 -0
  115. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  116. package/types/lib/cli/managedBom.d.ts.map +1 -1
  117. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  118. package/types/lib/core/boundedFile.d.ts +30 -0
  119. package/types/lib/core/boundedFile.d.ts.map +1 -1
  120. package/types/lib/core/env.d.ts +10 -0
  121. package/types/lib/core/env.d.ts.map +1 -1
  122. package/types/lib/core/fs.d.ts +4 -0
  123. package/types/lib/core/fs.d.ts.map +1 -1
  124. package/types/lib/core/globPatterns.d.ts +70 -0
  125. package/types/lib/core/globPatterns.d.ts.map +1 -0
  126. package/types/lib/core/paths.d.ts +9 -0
  127. package/types/lib/core/paths.d.ts.map +1 -1
  128. package/types/lib/core/processTree.d.ts +16 -5
  129. package/types/lib/core/processTree.d.ts.map +1 -1
  130. package/types/lib/core/projectConfig.d.ts +47 -8
  131. package/types/lib/core/projectConfig.d.ts.map +1 -1
  132. package/types/lib/core/toolRegistry.d.ts.map +1 -1
  133. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  134. package/types/lib/ecosystems/gradleutils.d.ts +248 -8
  135. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  136. package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
  137. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  139. package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
  140. package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
  141. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  142. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  143. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  144. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  145. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  146. package/types/lib/ecosystems/scalasem.d.ts +58 -16
  147. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  148. package/types/lib/evinser/evinser.d.ts +61 -9
  149. package/types/lib/evinser/evinser.d.ts.map +1 -1
  150. package/types/lib/evinser/scalasem.d.ts.map +1 -1
  151. package/types/lib/helpers/agenticToolutils.d.ts +53 -19
  152. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
  153. package/types/lib/helpers/bomSigner.d.ts +20 -3
  154. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  155. package/types/lib/helpers/jssSigner.d.ts +288 -0
  156. package/types/lib/helpers/jssSigner.d.ts.map +1 -0
  157. package/types/lib/helpers/vsixutils.d.ts +303 -19
  158. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  159. package/types/lib/inventory/agentBlueprint.d.ts +70 -0
  160. package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
  161. package/types/lib/inventory/agentToolNames.d.ts +51 -0
  162. package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
  163. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  164. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  165. package/types/lib/inventory/aiProviders.d.ts +85 -0
  166. package/types/lib/inventory/aiProviders.d.ts.map +1 -0
  167. package/types/lib/inventory/bomUtils.d.ts.map +1 -1
  168. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
  169. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
  170. package/types/lib/inventory/citations.d.ts +5 -0
  171. package/types/lib/inventory/citations.d.ts.map +1 -1
  172. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  173. package/types/lib/inventory/cyclonedx20.d.ts +99 -0
  174. package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
  175. package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
  176. package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
  177. package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
  178. package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
  179. package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
  180. package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
  181. package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
  182. package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
  183. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
  184. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
  185. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
  186. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
  187. package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
  188. package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
  189. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
  190. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
  191. package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
  192. package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
  193. package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
  194. package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
  195. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
  196. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
  197. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
  198. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
  199. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
  200. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
  201. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
  202. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
  203. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
  204. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
  205. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
  206. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
  207. package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
  208. package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
  209. package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
  210. package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
  211. package/types/lib/inventory/deps.d.ts +35 -2
  212. package/types/lib/inventory/deps.d.ts.map +1 -1
  213. package/types/lib/inventory/dosai.d.ts +19 -0
  214. package/types/lib/inventory/dosai.d.ts.map +1 -1
  215. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  216. package/types/lib/inventory/dotnetRuntime.d.ts +51 -4
  217. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -1
  218. package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
  219. package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
  220. package/types/lib/inventory/envcontext.d.ts +22 -6
  221. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  222. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  224. package/types/lib/inventory/hbom.d.ts +11 -0
  225. package/types/lib/inventory/hbom.d.ts.map +1 -1
  226. package/types/lib/inventory/hbomLoader.d.ts +54 -0
  227. package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
  228. package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
  229. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
  230. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  231. package/types/lib/inventory/occurrenceKey.d.ts +52 -0
  232. package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
  233. package/types/lib/inventory/pqcMigration.d.ts +84 -0
  234. package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
  235. package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
  236. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  237. package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
  238. package/types/lib/inventory/scalaDedupe.d.ts +43 -0
  239. package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
  240. package/types/lib/inventory/source.d.ts +15 -0
  241. package/types/lib/inventory/source.d.ts.map +1 -1
  242. package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
  243. package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
  244. package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
  245. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  246. package/types/lib/parsers/kiroPermissions.d.ts +62 -0
  247. package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
  248. package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
  249. package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
  250. package/types/lib/server/server.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/postgen.d.ts +7 -0
  252. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  253. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  254. package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
  255. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  256. package/types/lib/validator/bomValidator.d.ts +5 -1
  257. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  258. package/types/lib/validator/complianceEngine.d.ts +4 -4
  259. package/types/lib/validator/complianceEngine.d.ts.map +1 -1
  260. package/types/lib/validator/complianceRules.d.ts +14 -2
  261. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  262. package/types/lib/validator/index.d.ts +10 -0
  263. package/types/lib/validator/index.d.ts.map +1 -1
  264. package/types/lib/validator/reporters/json.d.ts.map +1 -1
@@ -0,0 +1,821 @@
1
+ // Reader for the GitHub Actions workflow dependency lockfile written by
2
+ // `gh actions-lock` (`.github/workflows/actions.lock`).
3
+ //
4
+ // The lockfile is a small YAML document describing the pinned action
5
+ // dependency graph of a repository's workflows. Three schema versions are
6
+ // published: v0.0.1 carries `OWNER/REPO@REF:ALGO-HEX` pin keys and resolves
7
+ // refs through `tag`/`branch` entry fields; v0.0.2 moved the ref into the
8
+ // entry and shortened the pin keys to `OWNER/REPO@REF`; v0.0.3 added an
9
+ // optional per-entry `hostname` for pins bound to github.com or a GHE.com
10
+ // data-residency tenant. Older files are normalised to the v0.0.2 shape so
11
+ // callers see one key format.
12
+ //
13
+ // This module only reads and validates the lockfile. Joining the entries to
14
+ // the components found by the workflow parser happens elsewhere, so the
15
+ // return value stays free of CycloneDX concepts. The reader never throws:
16
+ // every problem is reported as a warning and collapses the result to the
17
+ // empty shape.
18
+
19
+ import { statSync } from "node:fs";
20
+
21
+ import { parse as parseYaml } from "yaml";
22
+
23
+ import { DEBUG_MODE } from "../../core/activity.js";
24
+ import { safeExistsSync, safeReadFileSync } from "../../core/fs.js";
25
+ import { thoughtLog } from "../../core/logger.js";
26
+ import { scanTextForHiddenUnicode } from "../unicodeScan.js";
27
+
28
+ /**
29
+ * Maximum accepted lockfile size. `gh actions-lock` writes files that stay
30
+ * far below this; anything larger is treated as corrupt rather than parsed.
31
+ * @type {number}
32
+ */
33
+ export const MAX_ACTIONS_LOCKFILE_BYTES = 1024 * 1024;
34
+
35
+ const SUPPORTED_VERSIONS = new Set(["v0.0.1", "v0.0.2", "v0.0.3"]);
36
+
37
+ const TOP_LEVEL_FIELDS = new Set(["version", "workflows", "dependencies"]);
38
+
39
+ const ENTRY_FIELDS_V0_0_1 = new Set([
40
+ "tag",
41
+ "branch",
42
+ "commit",
43
+ "owner_id",
44
+ "repo_id",
45
+ "uses",
46
+ ]);
47
+
48
+ const ENTRY_FIELDS_V0_0_2 = new Set([
49
+ "ref",
50
+ "commit",
51
+ "owner_id",
52
+ "repo_id",
53
+ "uses",
54
+ ]);
55
+
56
+ const ENTRY_FIELDS_V0_0_3 = new Set([...ENTRY_FIELDS_V0_0_2, "hostname"]);
57
+
58
+ const SHA1_HEX_LENGTH = 40;
59
+ const SHA256_HEX_LENGTH = 64;
60
+ const HEX_DIGEST_PATTERN = /^[a-f0-9]+$/;
61
+ const GHE_HOSTNAME_SUFFIX = ".ghe.com";
62
+ // One label of a GHE.com tenant host: lowercase letters, digits and inner
63
+ // hyphens, 1 to 63 characters long.
64
+ const GHE_HOSTNAME_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
65
+ const DRIVE_ROOT_PATTERN = /^[A-Za-z]:/;
66
+ const YAML_ERROR_LINE_PATTERN = /at line (\d+)/;
67
+ const ALIAS_DISABLED_MARKER = "Alias resolution is disabled";
68
+ const MAX_WARNING_DETAIL_LENGTH = 200;
69
+ const MAX_CYCLE_NODES_IN_WARNING = 5;
70
+
71
+ /**
72
+ * One resolved action entry of the lockfile, normalised to the v0.0.2 shape.
73
+ *
74
+ * @typedef {Object} ActionsLockDependency
75
+ * @property {string} owner Owner name, lower case.
76
+ * @property {string} repo Repository name, lower case.
77
+ * @property {string} ref Ref the commit was resolved from, with its case
78
+ * preserved. For a v0.0.1 entry this is the tag, else the branch. It can
79
+ * differ from the ref in the pin key: the key keeps the ref the workflow
80
+ * wrote (for example `v4`) while this field records the resolved one (for
81
+ * example `v4.3.1`).
82
+ * @property {string} hashAlgo "sha1" or "sha256".
83
+ * @property {string} commit Hex digest without the algorithm prefix.
84
+ * @property {number} ownerId Numeric id of the action's owner.
85
+ * @property {number} repoId Numeric id of the action's repository.
86
+ * @property {string|undefined} hostname Set only when the entry records one
87
+ * (v0.0.3); undefined means the home host.
88
+ * @property {string[]} uses Normalised pin keys of the direct dependencies;
89
+ * empty for leaf actions.
90
+ */
91
+
92
+ /**
93
+ * Parsed lockfile, or the empty shape when the file is unreadable or invalid.
94
+ *
95
+ * @typedef {Object} ActionsLockfile
96
+ * @property {string|null} version Version as written in the file; null for an
97
+ * empty result.
98
+ * @property {string|undefined} sourceVersion Set only when an older version
99
+ * was normalised.
100
+ * @property {Object<string, string[]>} workflows Map of repository-relative
101
+ * workflow path (always with "/" separators) to its flat pin list.
102
+ * @property {Object<string, ActionsLockDependency>} dependencies Map of
103
+ * normalised pin key to the resolved entry.
104
+ * @property {string[]} warnings Human-readable problems found while reading.
105
+ */
106
+
107
+ /**
108
+ * Raised internally when the lockfile content is invalid. Never escapes the
109
+ * module; the caller converts it into the empty result plus warnings.
110
+ */
111
+ class ActionsLockParseAbort extends Error {
112
+ constructor(message) {
113
+ super(message);
114
+ this.name = "ActionsLockParseAbort";
115
+ }
116
+ }
117
+
118
+ function recordWarning(context, message) {
119
+ const label = context.filePath || "actions lockfile";
120
+ const text = `${label}: ${message}`;
121
+ context.warnings.push(text);
122
+ thoughtLog(`actions lockfile warning: ${text}`);
123
+ if (DEBUG_MODE) {
124
+ console.warn(`cdxgen: actions lockfile issue: ${text}`);
125
+ }
126
+ }
127
+
128
+ function fail(context, message) {
129
+ recordWarning(context, message);
130
+ throw new ActionsLockParseAbort(message);
131
+ }
132
+
133
+ function truncateForWarning(text) {
134
+ const singleLine = String(text).split("\n")[0];
135
+ if (singleLine.length <= MAX_WARNING_DETAIL_LENGTH) {
136
+ return singleLine;
137
+ }
138
+ return `${singleLine.slice(0, MAX_WARNING_DETAIL_LENGTH)}...`;
139
+ }
140
+
141
+ function emptyLockResult(warnings) {
142
+ return {
143
+ version: null,
144
+ sourceVersion: undefined,
145
+ workflows: {},
146
+ dependencies: {},
147
+ warnings,
148
+ };
149
+ }
150
+
151
+ function isLowerHexOfLength(text, length) {
152
+ return text.length === length && HEX_DIGEST_PATTERN.test(text);
153
+ }
154
+
155
+ /**
156
+ * Validate an algo-prefixed commit digest such as `sha1-<40 hex>`.
157
+ *
158
+ * @param {unknown} commit Candidate value
159
+ * @returns {boolean} Whether the value is a well-formed digest
160
+ */
161
+ function isValidCommitDigest(commit) {
162
+ if (typeof commit !== "string") {
163
+ return false;
164
+ }
165
+ if (commit.startsWith("sha1-")) {
166
+ return isLowerHexOfLength(commit.slice(5), SHA1_HEX_LENGTH);
167
+ }
168
+ if (commit.startsWith("sha256-")) {
169
+ return isLowerHexOfLength(commit.slice(7), SHA256_HEX_LENGTH);
170
+ }
171
+ return false;
172
+ }
173
+
174
+ /**
175
+ * Validate a v0.0.3 `hostname`: `github.com` or a lowercase
176
+ * `<tenant>.ghe.com` name.
177
+ *
178
+ * @param {unknown} hostname Candidate value
179
+ * @returns {boolean} Whether the value is an accepted host name
180
+ */
181
+ function isValidHostname(hostname) {
182
+ if (hostname === "github.com") {
183
+ return true;
184
+ }
185
+ if (typeof hostname !== "string" || !hostname.endsWith(GHE_HOSTNAME_SUFFIX)) {
186
+ return false;
187
+ }
188
+ const label = hostname.slice(0, -GHE_HOSTNAME_SUFFIX.length);
189
+ return GHE_HOSTNAME_LABEL_PATTERN.test(label);
190
+ }
191
+
192
+ /**
193
+ * Split a pin key into owner, repo and ref. Only the shape is checked here;
194
+ * the v0.0.1 digest suffix is handled by the caller.
195
+ *
196
+ * @param {unknown} pinKey Candidate pin key
197
+ * @param {boolean} isV0_0_1 Whether the file uses v0.0.1 keys
198
+ * @returns {{ owner: string, repo: string, ref: string }|undefined} Parts, or
199
+ * undefined when the key is not a valid pin
200
+ */
201
+ function splitPinKey(pinKey, isV0_0_1) {
202
+ if (typeof pinKey !== "string") {
203
+ return undefined;
204
+ }
205
+ const atIndex = pinKey.indexOf("@");
206
+ if (atIndex <= 0) {
207
+ return undefined;
208
+ }
209
+ const ownerRepo = pinKey.slice(0, atIndex);
210
+ const rest = pinKey.slice(atIndex + 1);
211
+ if (!rest.length) {
212
+ return undefined;
213
+ }
214
+ const slashIndex = ownerRepo.indexOf("/");
215
+ if (slashIndex <= 0) {
216
+ return undefined;
217
+ }
218
+ // A second slash is a sub-action path such as actions/cache/save@v4.
219
+ if (ownerRepo.indexOf("/", slashIndex + 1) !== -1) {
220
+ return undefined;
221
+ }
222
+ if (ownerRepo.includes(":")) {
223
+ return undefined;
224
+ }
225
+ // A second "@" means the ref itself contains "@".
226
+ if (rest.includes("@")) {
227
+ return undefined;
228
+ }
229
+ // Only v0.0.1 keys carry the `:ALGO-HEX` suffix; everywhere else a colon
230
+ // cannot appear in the ref.
231
+ if (!isV0_0_1 && rest.includes(":")) {
232
+ return undefined;
233
+ }
234
+ const owner = ownerRepo.slice(0, slashIndex).toLowerCase();
235
+ const repo = ownerRepo.slice(slashIndex + 1).toLowerCase();
236
+ if (!owner.length || !repo.length) {
237
+ return undefined;
238
+ }
239
+ return { owner, repo, ref: rest };
240
+ }
241
+
242
+ /**
243
+ * Split the ref part of a v0.0.1 pin into the ref and the digest suffix.
244
+ *
245
+ * @param {string} refWithSuffix Text after the "@" of a v0.0.1 pin key
246
+ * @returns {{ ref: string, digest: string }|undefined} Parts, or undefined
247
+ * when the digest suffix is missing
248
+ */
249
+ function splitV0_0_1Ref(refWithSuffix) {
250
+ const colonIndex = refWithSuffix.indexOf(":");
251
+ if (colonIndex <= 0) {
252
+ return undefined;
253
+ }
254
+ const ref = refWithSuffix.slice(0, colonIndex);
255
+ const digest = refWithSuffix.slice(colonIndex + 1);
256
+ if (!ref.length) {
257
+ return undefined;
258
+ }
259
+ return { ref, digest };
260
+ }
261
+
262
+ /**
263
+ * Parse a pin key as written in the file into its normalised v0.0.2 form,
264
+ * failing with a warning when the key is malformed.
265
+ *
266
+ * @param {unknown} rawPin Pin key as written in the file
267
+ * @param {boolean} isV0_0_1 Whether the file uses v0.0.1 keys
268
+ * @param {Object} context Warning context
269
+ * @param {string} where Where the key was found, for the warning
270
+ * @returns {{ owner: string, repo: string, key: string, keyDigest: string|undefined }}
271
+ * Lower-cased owner and repository, the normalised key, and the v0.0.1
272
+ * digest suffix when there is one
273
+ */
274
+ function parsePinKey(rawPin, isV0_0_1, context, where) {
275
+ const pin = splitPinKey(rawPin, isV0_0_1);
276
+ if (!pin) {
277
+ fail(
278
+ context,
279
+ `invalid pin key '${truncateForWarning(rawPin)}' in ${where}`,
280
+ );
281
+ }
282
+ let ref = pin.ref;
283
+ let keyDigest;
284
+ if (isV0_0_1) {
285
+ const split = splitV0_0_1Ref(pin.ref);
286
+ if (!split) {
287
+ fail(
288
+ context,
289
+ `pin key '${truncateForWarning(rawPin)}' in ${where} lacks a sha1- or sha256- digest suffix`,
290
+ );
291
+ }
292
+ ref = split.ref;
293
+ keyDigest = split.digest;
294
+ }
295
+ return {
296
+ owner: pin.owner,
297
+ repo: pin.repo,
298
+ key: `${pin.owner}/${pin.repo}@${ref}`,
299
+ keyDigest,
300
+ };
301
+ }
302
+
303
+ function fullCommitOf(entry) {
304
+ return `${entry.hashAlgo}-${entry.commit}`;
305
+ }
306
+
307
+ /**
308
+ * Resolve one pin reference found in a workflow list or a uses list: parse
309
+ * the key, normalise it to the v0.0.2 shape, and require that the referenced
310
+ * entry exists with a matching v0.0.1 digest.
311
+ *
312
+ * @param {string} rawPin Pin key as written in the file
313
+ * @param {Map<string, Object>} entries Normalised dependency entries
314
+ * @param {boolean} isV0_0_1 Whether the file uses v0.0.1 keys
315
+ * @param {Object} context Warning context
316
+ * @param {string} where Where the reference was found, for the warning
317
+ * @returns {string} Normalised pin key
318
+ */
319
+ function normalisePinReference(rawPin, entries, isV0_0_1, context, where) {
320
+ const { key: normalisedKey, keyDigest } = parsePinKey(
321
+ rawPin,
322
+ isV0_0_1,
323
+ context,
324
+ where,
325
+ );
326
+ const entry = entries.get(normalisedKey);
327
+ if (!entry) {
328
+ fail(
329
+ context,
330
+ `pin '${normalisedKey}' referenced in ${where} is not defined in dependencies`,
331
+ );
332
+ }
333
+ if (keyDigest && keyDigest !== fullCommitOf(entry)) {
334
+ fail(
335
+ context,
336
+ `pin key '${normalisedKey}' in ${where} carries digest '${keyDigest}' but the entry commits to '${fullCommitOf(entry)}'`,
337
+ );
338
+ }
339
+ return normalisedKey;
340
+ }
341
+
342
+ /**
343
+ * Validate the `dependencies` mapping and normalise every entry to the
344
+ * v0.0.2 shape. The uses lists are kept raw here because they may reference
345
+ * entries that have not been seen yet.
346
+ *
347
+ * @param {Object} doc Parsed lockfile document
348
+ * @param {string} version Lockfile version as written
349
+ * @param {Object} context Warning context
350
+ * @returns {Map<string, Object>} Normalised entries by pin key
351
+ */
352
+ function parseDependencyEntries(doc, version, context) {
353
+ const entries = new Map();
354
+ const raw = doc.dependencies;
355
+ if (raw === undefined) {
356
+ return entries;
357
+ }
358
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
359
+ fail(context, "dependencies must be a mapping of pin keys");
360
+ }
361
+ const isV0_0_1 = version === "v0.0.1";
362
+ const allowedFields = isV0_0_1
363
+ ? ENTRY_FIELDS_V0_0_1
364
+ : version === "v0.0.2"
365
+ ? ENTRY_FIELDS_V0_0_2
366
+ : ENTRY_FIELDS_V0_0_3;
367
+ for (const rawKey of Object.keys(raw)) {
368
+ const pin = parsePinKey(rawKey, isV0_0_1, context, "dependencies");
369
+ const normalisedKey = pin.key;
370
+ const keyDigest = pin.keyDigest;
371
+ if (entries.has(normalisedKey)) {
372
+ fail(
373
+ context,
374
+ isV0_0_1
375
+ ? `dependencies contains two entries that normalise to the same pin key '${normalisedKey}' (the keys differ only in owner or repository case, or in the digest suffix)`
376
+ : `dependencies contains two entries that normalise to the same pin key '${normalisedKey}' (the keys differ only in owner or repository case)`,
377
+ );
378
+ }
379
+ const entry = raw[rawKey];
380
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
381
+ fail(context, `entry '${normalisedKey}' must be a mapping`);
382
+ }
383
+ for (const fieldName of Object.keys(entry)) {
384
+ if (!allowedFields.has(fieldName)) {
385
+ fail(
386
+ context,
387
+ `unknown field '${fieldName}' in entry '${normalisedKey}'`,
388
+ );
389
+ }
390
+ }
391
+ const commit = entry.commit;
392
+ if (commit === undefined) {
393
+ fail(context, `entry '${normalisedKey}' is missing the commit field`);
394
+ }
395
+ if (!isValidCommitDigest(commit)) {
396
+ fail(
397
+ context,
398
+ `entry '${normalisedKey}' has a malformed commit digest (expected sha1- with ${SHA1_HEX_LENGTH} or sha256- with ${SHA256_HEX_LENGTH} lowercase hex characters)`,
399
+ );
400
+ }
401
+ if (keyDigest && keyDigest !== commit) {
402
+ fail(
403
+ context,
404
+ `pin key '${normalisedKey}' carries digest '${keyDigest}' but the entry commits to '${commit}'`,
405
+ );
406
+ }
407
+ const ownerId = entry.owner_id;
408
+ const repoId = entry.repo_id;
409
+ if (!Number.isInteger(ownerId) || ownerId < 1) {
410
+ fail(
411
+ context,
412
+ `entry '${normalisedKey}' needs a positive integer owner_id`,
413
+ );
414
+ }
415
+ if (!Number.isInteger(repoId) || repoId < 1) {
416
+ fail(
417
+ context,
418
+ `entry '${normalisedKey}' needs a positive integer repo_id`,
419
+ );
420
+ }
421
+ let entryRef;
422
+ if (isV0_0_1) {
423
+ const tag = entry.tag;
424
+ const branch = entry.branch;
425
+ if (tag !== undefined && (typeof tag !== "string" || !tag.length)) {
426
+ fail(context, `entry '${normalisedKey}' has a non-string tag`);
427
+ }
428
+ if (
429
+ branch !== undefined &&
430
+ (typeof branch !== "string" || !branch.length)
431
+ ) {
432
+ fail(context, `entry '${normalisedKey}' has a non-string branch`);
433
+ }
434
+ if (tag === undefined && branch === undefined) {
435
+ fail(
436
+ context,
437
+ `entry '${normalisedKey}' needs a tag or a branch to resolve its ref`,
438
+ );
439
+ }
440
+ entryRef = tag !== undefined ? tag : branch;
441
+ } else {
442
+ const refField = entry.ref;
443
+ if (refField === undefined) {
444
+ fail(context, `entry '${normalisedKey}' is missing the ref field`);
445
+ }
446
+ if (typeof refField !== "string" || !refField.length) {
447
+ fail(context, `entry '${normalisedKey}' needs a non-empty ref string`);
448
+ }
449
+ entryRef = refField;
450
+ }
451
+ let hostname;
452
+ if (version === "v0.0.3" && entry.hostname !== undefined) {
453
+ if (!isValidHostname(entry.hostname)) {
454
+ fail(
455
+ context,
456
+ `entry '${normalisedKey}' has an invalid hostname '${truncateForWarning(entry.hostname)}' (expected github.com or a lowercase <tenant>.ghe.com name)`,
457
+ );
458
+ }
459
+ hostname = entry.hostname;
460
+ }
461
+ const rawUses = entry.uses;
462
+ if (rawUses !== undefined && !Array.isArray(rawUses)) {
463
+ fail(
464
+ context,
465
+ `entry '${normalisedKey}' has a uses list that is not an array`,
466
+ );
467
+ }
468
+ entries.set(normalisedKey, {
469
+ owner: pin.owner,
470
+ repo: pin.repo,
471
+ ref: entryRef,
472
+ hashAlgo: commit.startsWith("sha256-") ? "sha256" : "sha1",
473
+ commit: commit.slice(commit.indexOf("-") + 1),
474
+ ownerId,
475
+ repoId,
476
+ hostname,
477
+ rawUses: rawUses || [],
478
+ });
479
+ }
480
+ return entries;
481
+ }
482
+
483
+ /**
484
+ * Resolve the uses lists of every entry into normalised pin keys.
485
+ *
486
+ * @param {Map<string, Object>} entries Normalised dependency entries
487
+ * @param {boolean} isV0_0_1 Whether the file uses v0.0.1 keys
488
+ * @param {Object} context Warning context
489
+ * @returns {Map<string, string[]>} Normalised direct edges by pin key
490
+ */
491
+ function resolveUsesEdges(entries, isV0_0_1, context) {
492
+ const usesMap = new Map();
493
+ for (const [key, entry] of entries) {
494
+ const edges = entry.rawUses.map((rawPin) =>
495
+ normalisePinReference(
496
+ rawPin,
497
+ entries,
498
+ isV0_0_1,
499
+ context,
500
+ `the uses list of '${key}'`,
501
+ ),
502
+ );
503
+ usesMap.set(key, edges);
504
+ }
505
+ return usesMap;
506
+ }
507
+
508
+ /**
509
+ * Find a cycle in the uses graph with an iterative depth-first walk, so a
510
+ * deep graph cannot overflow the call stack. Returns the cycle as a list of
511
+ * pin keys ending where it started, or undefined when the graph is acyclic.
512
+ *
513
+ * @param {Map<string, string[]>} usesMap Normalised direct edges by pin key
514
+ * @returns {string[]|undefined} The cycle, or undefined
515
+ */
516
+ function findUsesCycle(usesMap) {
517
+ const WHITE = 0;
518
+ const GRAY = 1;
519
+ const BLACK = 2;
520
+ const color = new Map();
521
+ for (const key of usesMap.keys()) {
522
+ color.set(key, WHITE);
523
+ }
524
+ for (const start of usesMap.keys()) {
525
+ if (color.get(start) !== WHITE) {
526
+ continue;
527
+ }
528
+ color.set(start, GRAY);
529
+ const stack = [[start, 0]];
530
+ while (stack.length) {
531
+ const frame = stack[stack.length - 1];
532
+ const edges = usesMap.get(frame[0]);
533
+ if (frame[1] >= edges.length) {
534
+ color.set(frame[0], BLACK);
535
+ stack.pop();
536
+ continue;
537
+ }
538
+ const next = edges[frame[1]];
539
+ frame[1] += 1;
540
+ const nextColor = color.get(next);
541
+ if (nextColor === GRAY) {
542
+ const path = stack.map((frameEntry) => frameEntry[0]);
543
+ return path.slice(path.indexOf(next)).concat(next);
544
+ }
545
+ if (nextColor === WHITE) {
546
+ color.set(next, GRAY);
547
+ stack.push([next, 0]);
548
+ }
549
+ }
550
+ }
551
+ return undefined;
552
+ }
553
+
554
+ /**
555
+ * Validate one workflow path key and return the form stored in the result.
556
+ * Backslash separators are normalised so Windows-built keys round-trip with
557
+ * "/" on every platform; absolute paths, "." and ".." segments are rejected,
558
+ * so every stored key is in the canonical repository-relative form. The
559
+ * comparison stays case-sensitive, like git paths.
560
+ *
561
+ * @param {string} rawPath Workflow path as written in the file
562
+ * @param {Object} context Warning context
563
+ * @returns {string} Repository-relative path with "/" separators
564
+ */
565
+ function normaliseWorkflowPath(rawPath, context) {
566
+ if (typeof rawPath !== "string" || !rawPath.length) {
567
+ fail(context, "workflows contains an empty path key");
568
+ }
569
+ const normalised = rawPath.split("\\").join("/");
570
+ if (!normalised.length) {
571
+ fail(context, "workflows contains an empty path key");
572
+ }
573
+ if (normalised.startsWith("/") || DRIVE_ROOT_PATTERN.test(normalised)) {
574
+ fail(context, `workflow path '${truncateForWarning(rawPath)}' is absolute`);
575
+ }
576
+ const segments = normalised.split("/");
577
+ if (segments.some((segment) => !segment.length)) {
578
+ fail(
579
+ context,
580
+ `workflow path '${truncateForWarning(rawPath)}' has an empty path segment`,
581
+ );
582
+ }
583
+ if (segments.includes("..")) {
584
+ fail(
585
+ context,
586
+ `workflow path '${truncateForWarning(rawPath)}' escapes the repository with a '..' segment`,
587
+ );
588
+ }
589
+ if (segments.includes(".")) {
590
+ fail(
591
+ context,
592
+ `workflow path '${truncateForWarning(rawPath)}' has a '.' segment; keys must be plain repository-relative paths`,
593
+ );
594
+ }
595
+ return normalised;
596
+ }
597
+
598
+ /**
599
+ * Validate the `workflows` mapping and resolve its pin lists to normalised
600
+ * keys that must exist in dependencies.
601
+ *
602
+ * @param {Object} doc Parsed lockfile document
603
+ * @param {Map<string, Object>} entries Normalised dependency entries
604
+ * @param {boolean} isV0_0_1 Whether the file uses v0.0.1 keys
605
+ * @param {Object} context Warning context
606
+ * @returns {Object<string, string[]>} Workflow paths mapped to pin key lists
607
+ */
608
+ function resolveWorkflows(doc, entries, isV0_0_1, context) {
609
+ const workflows = {};
610
+ const raw = doc.workflows;
611
+ if (raw === undefined) {
612
+ return workflows;
613
+ }
614
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
615
+ fail(context, "workflows must be a mapping of workflow paths to pin lists");
616
+ }
617
+ for (const rawPath of Object.keys(raw)) {
618
+ const storedPath = normaliseWorkflowPath(rawPath, context);
619
+ if (Object.hasOwn(workflows, storedPath)) {
620
+ fail(
621
+ context,
622
+ `workflows contains two keys that normalise to the same path '${storedPath}'`,
623
+ );
624
+ }
625
+ const pinList = raw[rawPath];
626
+ if (!Array.isArray(pinList)) {
627
+ fail(
628
+ context,
629
+ `workflow '${storedPath}' has a pin list that is not an array`,
630
+ );
631
+ }
632
+ const normalisedList = pinList.map((rawPin, index) => {
633
+ if (typeof rawPin !== "string") {
634
+ fail(
635
+ context,
636
+ `workflow '${storedPath}' has a non-string pin at position ${index}`,
637
+ );
638
+ }
639
+ return normalisePinReference(
640
+ rawPin,
641
+ entries,
642
+ isV0_0_1,
643
+ context,
644
+ `the pin list of '${storedPath}'`,
645
+ );
646
+ });
647
+ // Workflow paths come from the lockfile, so a path could collide with a
648
+ // JS object name such as __proto__; defineProperty keeps it a plain key.
649
+ Object.defineProperty(workflows, storedPath, {
650
+ value: normalisedList,
651
+ enumerable: true,
652
+ writable: true,
653
+ configurable: true,
654
+ });
655
+ }
656
+ return workflows;
657
+ }
658
+
659
+ function failYaml(context, error) {
660
+ const message = typeof error?.message === "string" ? error.message : "";
661
+ if (error?.code === "DUPLICATE_KEY") {
662
+ const lineMatch = YAML_ERROR_LINE_PATTERN.exec(message);
663
+ fail(
664
+ context,
665
+ `duplicate mapping key${lineMatch ? ` at line ${lineMatch[1]}` : ""}`,
666
+ );
667
+ }
668
+ if (message.includes(ALIAS_DISABLED_MARKER)) {
669
+ fail(context, "YAML aliases are not allowed in the lockfile");
670
+ }
671
+ fail(context, `invalid YAML (${truncateForWarning(message)})`);
672
+ }
673
+
674
+ /**
675
+ * Parse an actions lockfile from its text content.
676
+ *
677
+ * Every problem, including a file that cannot be parsed at all, is reported
678
+ * as a warning and collapses the result to the empty shape; the function
679
+ * never throws.
680
+ *
681
+ * @param {string} text Lockfile content
682
+ * @param {string} [filePath] Path the content came from, used in warnings
683
+ * @returns {ActionsLockfile} Parsed lockfile, or the empty shape
684
+ */
685
+ export function parseActionsLockfileString(text, filePath) {
686
+ const warnings = [];
687
+ const context = { filePath, warnings };
688
+ try {
689
+ if (typeof text !== "string") {
690
+ fail(context, "the lockfile content is not text");
691
+ }
692
+ const unicodeScan = scanTextForHiddenUnicode(text, { syntax: "yaml" });
693
+ if (unicodeScan.hasHiddenUnicode) {
694
+ recordWarning(
695
+ context,
696
+ `hidden Unicode characters found at line(s) ${unicodeScan.lineNumbers.join(", ")} (${unicodeScan.codePoints.join(", ")})`,
697
+ );
698
+ }
699
+ let doc;
700
+ try {
701
+ // Aliases are banned outright: a document is a few hundred flat lines,
702
+ // so there is nothing an alias could legitimately expand to.
703
+ doc = parseYaml(text, { maxAliasCount: 0 });
704
+ } catch (error) {
705
+ failYaml(context, error);
706
+ }
707
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
708
+ fail(
709
+ context,
710
+ doc === null
711
+ ? "the lockfile is empty"
712
+ : "the lockfile must be a YAML mapping",
713
+ );
714
+ }
715
+ const version = doc.version;
716
+ if (version === undefined) {
717
+ fail(context, "missing the required version field");
718
+ }
719
+ if (typeof version !== "string" || !SUPPORTED_VERSIONS.has(version)) {
720
+ fail(
721
+ context,
722
+ `unsupported lockfile version '${truncateForWarning(version)}'; supported versions are v0.0.1, v0.0.2 and v0.0.3`,
723
+ );
724
+ }
725
+ for (const key of Object.keys(doc)) {
726
+ if (!TOP_LEVEL_FIELDS.has(key)) {
727
+ fail(context, `unknown top-level field '${key}'`);
728
+ }
729
+ }
730
+ const isV0_0_1 = version === "v0.0.1";
731
+ const entries = parseDependencyEntries(doc, version, context);
732
+ const usesMap = resolveUsesEdges(entries, isV0_0_1, context);
733
+ const cycle = findUsesCycle(usesMap);
734
+ if (cycle) {
735
+ // A crafted file can close a cycle over thousands of entries; keep the
736
+ // warning to the first few hops instead of the whole walk.
737
+ const cyclePath =
738
+ cycle.length > MAX_CYCLE_NODES_IN_WARNING
739
+ ? `${cycle.slice(0, MAX_CYCLE_NODES_IN_WARNING).join(" -> ")} -> ...`
740
+ : cycle.join(" -> ");
741
+ fail(context, `uses cycle detected: ${cyclePath}`);
742
+ }
743
+ const workflows = resolveWorkflows(doc, entries, isV0_0_1, context);
744
+ const dependencies = {};
745
+ for (const [key, entry] of entries) {
746
+ dependencies[key] = {
747
+ owner: entry.owner,
748
+ repo: entry.repo,
749
+ ref: entry.ref,
750
+ hashAlgo: entry.hashAlgo,
751
+ commit: entry.commit,
752
+ ownerId: entry.ownerId,
753
+ repoId: entry.repoId,
754
+ hostname: entry.hostname,
755
+ uses: usesMap.get(key) || [],
756
+ };
757
+ }
758
+ return {
759
+ version,
760
+ sourceVersion: isV0_0_1 ? "v0.0.1" : undefined,
761
+ workflows,
762
+ dependencies,
763
+ warnings,
764
+ };
765
+ } catch (error) {
766
+ if (!(error instanceof ActionsLockParseAbort)) {
767
+ recordWarning(
768
+ context,
769
+ `unexpected failure while parsing the lockfile (${error?.name || "Error"})`,
770
+ );
771
+ }
772
+ return emptyLockResult(warnings);
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Read and parse an actions lockfile such as
778
+ * `.github/workflows/actions.lock`.
779
+ *
780
+ * The file is size-checked before it is read, and every problem is reported
781
+ * as a warning and collapses the result to the empty shape; the function
782
+ * never throws.
783
+ *
784
+ * @param {string} filePath Path to the lockfile
785
+ * @returns {ActionsLockfile} Parsed lockfile, or the empty shape
786
+ */
787
+ export function parseActionsLockfile(filePath) {
788
+ const warnings = [];
789
+ const context = { filePath, warnings };
790
+ if (!safeExistsSync(filePath)) {
791
+ recordWarning(context, "the lockfile does not exist");
792
+ return emptyLockResult(warnings);
793
+ }
794
+ let stats;
795
+ try {
796
+ stats = statSync(filePath);
797
+ } catch (error) {
798
+ recordWarning(
799
+ context,
800
+ `the lockfile could not be inspected (${error?.name || "Error"})`,
801
+ );
802
+ return emptyLockResult(warnings);
803
+ }
804
+ if (!stats.isFile()) {
805
+ recordWarning(context, "the lockfile path is not a regular file");
806
+ return emptyLockResult(warnings);
807
+ }
808
+ if (stats.size > MAX_ACTIONS_LOCKFILE_BYTES) {
809
+ recordWarning(
810
+ context,
811
+ `the lockfile is ${stats.size} bytes, larger than the limit of ${MAX_ACTIONS_LOCKFILE_BYTES} bytes`,
812
+ );
813
+ return emptyLockResult(warnings);
814
+ }
815
+ const text = safeReadFileSync(filePath, "utf-8");
816
+ if (typeof text !== "string") {
817
+ recordWarning(context, "the lockfile could not be read");
818
+ return emptyLockResult(warnings);
819
+ }
820
+ return parseActionsLockfileString(text, filePath);
821
+ }