codegate-ai 0.9.1 → 0.10.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 (22) hide show
  1. package/dist/layer2-static/detectors/dependabot-auto-merge.d.ts +7 -0
  2. package/dist/layer2-static/detectors/dependabot-auto-merge.js +118 -0
  3. package/dist/layer2-static/detectors/workflow-artifact-trust-chain.d.ts +7 -0
  4. package/dist/layer2-static/detectors/workflow-artifact-trust-chain.js +89 -0
  5. package/dist/layer2-static/detectors/workflow-call-boundary.d.ts +7 -0
  6. package/dist/layer2-static/detectors/workflow-call-boundary.js +92 -0
  7. package/dist/layer2-static/detectors/workflow-dynamic-matrix-injection.d.ts +7 -0
  8. package/dist/layer2-static/detectors/workflow-dynamic-matrix-injection.js +149 -0
  9. package/dist/layer2-static/detectors/workflow-local-action-mutation.d.ts +7 -0
  10. package/dist/layer2-static/detectors/workflow-local-action-mutation.js +125 -0
  11. package/dist/layer2-static/detectors/workflow-oidc-untrusted-context.d.ts +7 -0
  12. package/dist/layer2-static/detectors/workflow-oidc-untrusted-context.js +166 -0
  13. package/dist/layer2-static/detectors/workflow-pr-target-checkout-head.d.ts +7 -0
  14. package/dist/layer2-static/detectors/workflow-pr-target-checkout-head.js +99 -0
  15. package/dist/layer2-static/detectors/workflow-secret-exfiltration.d.ts +8 -0
  16. package/dist/layer2-static/detectors/workflow-secret-exfiltration.js +97 -0
  17. package/dist/layer2-static/engine.js +89 -0
  18. package/dist/layer2-static/workflow/analysis.d.ts +24 -0
  19. package/dist/layer2-static/workflow/analysis.js +201 -0
  20. package/dist/layer2-static/workflow/parser.js +30 -10
  21. package/dist/layer2-static/workflow/types.d.ts +6 -0
  22. package/package.json +1 -1
@@ -0,0 +1,166 @@
1
+ import { buildFindingEvidence } from "../evidence.js";
2
+ import { collectUntrustedReachableJobIds } from "../workflow/analysis.js";
3
+ import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
4
+ const CLOUD_AUTH_ACTIONS = new Set([
5
+ "aws-actions/configure-aws-credentials",
6
+ "azure/login",
7
+ "google-github-actions/auth",
8
+ ]);
9
+ const AUDIENCE_KEYS = new Set(["audience", "token_audience", "id_token_audience"]);
10
+ function hasWritePermission(value) {
11
+ if (typeof value === "string") {
12
+ return value.trim().toLowerCase() === "write-all";
13
+ }
14
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
15
+ return false;
16
+ }
17
+ return Object.entries(value).some(([key, permission]) => {
18
+ if (key.trim().toLowerCase() === "id-token") {
19
+ return false;
20
+ }
21
+ if (typeof permission !== "string") {
22
+ return false;
23
+ }
24
+ return permission.trim().toLowerCase() === "write";
25
+ });
26
+ }
27
+ function hasIdTokenWrite(value) {
28
+ if (typeof value === "string") {
29
+ return value.trim().toLowerCase() === "write-all";
30
+ }
31
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
32
+ return false;
33
+ }
34
+ const record = value;
35
+ const idTokenPermission = record["id-token"];
36
+ return (typeof idTokenPermission === "string" && idTokenPermission.trim().toLowerCase() === "write");
37
+ }
38
+ function normalizeUses(value) {
39
+ if (!value) {
40
+ return null;
41
+ }
42
+ const normalized = value.trim().toLowerCase();
43
+ if (normalized.length === 0) {
44
+ return null;
45
+ }
46
+ const atIndex = normalized.indexOf("@");
47
+ return atIndex === -1 ? normalized : normalized.slice(0, atIndex);
48
+ }
49
+ function hasCloudOidcAuthStep(jobSteps) {
50
+ return jobSteps.some((step) => {
51
+ const normalizedUses = normalizeUses(step.uses);
52
+ return normalizedUses ? CLOUD_AUTH_ACTIONS.has(normalizedUses) : false;
53
+ });
54
+ }
55
+ function hasAudienceConstraint(jobSteps) {
56
+ return jobSteps.some((step) => {
57
+ const withRecord = step.with;
58
+ if (!withRecord) {
59
+ return false;
60
+ }
61
+ return Object.entries(withRecord).some(([key, value]) => {
62
+ if (!AUDIENCE_KEYS.has(key.trim().toLowerCase())) {
63
+ return false;
64
+ }
65
+ return value.trim().length > 0;
66
+ });
67
+ });
68
+ }
69
+ function hasActorConstraint(condition) {
70
+ const normalized = condition.toLowerCase();
71
+ return (normalized.includes("github.actor") ||
72
+ normalized.includes("github.triggering_actor") ||
73
+ normalized.includes("github.event.pull_request.user.login"));
74
+ }
75
+ function hasRepositoryOrRefConstraint(condition) {
76
+ const normalized = condition.toLowerCase();
77
+ return (normalized.includes("github.repository") ||
78
+ normalized.includes("github.event.pull_request.head.repo.full_name") ||
79
+ normalized.includes("github.event.pull_request.head.repo.fork") ||
80
+ normalized.includes("github.ref") ||
81
+ normalized.includes("github.base_ref") ||
82
+ normalized.includes("github.event.pull_request.base.ref"));
83
+ }
84
+ function hasStrictTrustChecks(condition) {
85
+ if (!condition) {
86
+ return false;
87
+ }
88
+ return hasActorConstraint(condition) && hasRepositoryOrRefConstraint(condition);
89
+ }
90
+ export function detectWorkflowOidcUntrustedContext(input) {
91
+ if (!isGitHubWorkflowPath(input.filePath)) {
92
+ return [];
93
+ }
94
+ const facts = extractWorkflowFacts(input.parsed);
95
+ if (!facts) {
96
+ return [];
97
+ }
98
+ const reachableJobIds = collectUntrustedReachableJobIds(facts);
99
+ if (reachableJobIds.size === 0) {
100
+ return [];
101
+ }
102
+ const findings = [];
103
+ const workflowHasIdTokenWrite = hasIdTokenWrite(facts.workflowPermissions);
104
+ facts.jobs.forEach((job, jobIndex) => {
105
+ if (!reachableJobIds.has(job.id)) {
106
+ return;
107
+ }
108
+ const jobHasIdTokenWrite = workflowHasIdTokenWrite || hasIdTokenWrite(job.permissions);
109
+ if (!jobHasIdTokenWrite) {
110
+ return;
111
+ }
112
+ const strictTrustChecks = hasStrictTrustChecks(job.if);
113
+ const cloudAuthDetected = hasCloudOidcAuthStep(job.steps);
114
+ const hasAudience = hasAudienceConstraint(job.steps);
115
+ if (strictTrustChecks && hasAudience) {
116
+ return;
117
+ }
118
+ const evidence = buildFindingEvidence({
119
+ textContent: input.textContent,
120
+ searchTerms: [
121
+ "id-token: write",
122
+ "permissions",
123
+ "audience",
124
+ "github.actor",
125
+ "github.repository",
126
+ ],
127
+ fallbackValue: `${job.id} enables id-token write in untrusted trigger context`,
128
+ });
129
+ findings.push({
130
+ rule_id: "workflow-oidc-untrusted-context",
131
+ finding_id: `WORKFLOW_OIDC_UNTRUSTED_CONTEXT-${input.filePath}-${jobIndex}`,
132
+ severity: hasWritePermission(job.permissions) || hasWritePermission(facts.workflowPermissions)
133
+ ? "CRITICAL"
134
+ : "HIGH",
135
+ category: "CI_PERMISSIONS",
136
+ layer: "L2",
137
+ file_path: input.filePath,
138
+ location: { field: `jobs.${job.id}.permissions.id-token` },
139
+ description: "Workflow enables OIDC token minting in an untrusted trigger context without strict trust boundaries",
140
+ affected_tools: ["github-actions"],
141
+ cve: null,
142
+ owasp: ["ASI02"],
143
+ cwe: "CWE-284",
144
+ confidence: "HIGH",
145
+ fixable: false,
146
+ remediation_actions: [
147
+ "Restrict id-token: write to trusted branches or trusted workflow_call boundaries",
148
+ "Require explicit actor and repository/ref checks on untrusted triggers",
149
+ cloudAuthDetected
150
+ ? "Configure explicit audience constraints for cloud authentication actions"
151
+ : "Add audience constraints and scoped trust conditions before minting OIDC tokens",
152
+ ],
153
+ metadata: {
154
+ risk_tags: [
155
+ strictTrustChecks ? "strict-trust-checks" : "missing-strict-trust-checks",
156
+ hasAudience ? "audience-constrained" : "missing-audience-constraint",
157
+ cloudAuthDetected ? "cloud-auth-step" : "generic-oidc",
158
+ ],
159
+ origin: "workflow-audit",
160
+ },
161
+ evidence: evidence?.evidence ?? null,
162
+ suppressed: false,
163
+ });
164
+ });
165
+ return findings;
166
+ }
@@ -0,0 +1,7 @@
1
+ import type { Finding } from "../../types/finding.js";
2
+ export interface WorkflowPrTargetCheckoutHeadInput {
3
+ filePath: string;
4
+ parsed: unknown;
5
+ textContent: string;
6
+ }
7
+ export declare function detectWorkflowPrTargetCheckoutHead(input: WorkflowPrTargetCheckoutHeadInput): Finding[];
@@ -0,0 +1,99 @@
1
+ import { buildFindingEvidence } from "../evidence.js";
2
+ import { collectUntrustedReachableJobIds } from "../workflow/analysis.js";
3
+ import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
4
+ function hasWritePermission(value) {
5
+ if (typeof value === "string") {
6
+ return value.trim().toLowerCase() === "write-all";
7
+ }
8
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
9
+ return false;
10
+ }
11
+ return Object.values(value).some((permission) => {
12
+ if (typeof permission !== "string") {
13
+ return false;
14
+ }
15
+ return permission.trim().toLowerCase() === "write";
16
+ });
17
+ }
18
+ function isCheckoutStep(uses) {
19
+ if (!uses) {
20
+ return false;
21
+ }
22
+ return /^actions\/checkout(?:@.+)?$/iu.test(uses.trim());
23
+ }
24
+ function isRiskyCheckoutRef(ref) {
25
+ if (!ref) {
26
+ return false;
27
+ }
28
+ const normalized = ref.toLowerCase();
29
+ return (normalized.includes("github.event.pull_request.head.") || normalized.includes("github.head_ref"));
30
+ }
31
+ function hasInheritedSecrets(secrets) {
32
+ return typeof secrets === "string" && secrets.trim().toLowerCase() === "inherit";
33
+ }
34
+ export function detectWorkflowPrTargetCheckoutHead(input) {
35
+ if (!isGitHubWorkflowPath(input.filePath)) {
36
+ return [];
37
+ }
38
+ const facts = extractWorkflowFacts(input.parsed);
39
+ if (!facts) {
40
+ return [];
41
+ }
42
+ const hasPullRequestTarget = facts.triggers.some((trigger) => trigger.trim().toLowerCase() === "pull_request_target");
43
+ if (!hasPullRequestTarget) {
44
+ return [];
45
+ }
46
+ const reachableJobIds = collectUntrustedReachableJobIds(facts);
47
+ if (reachableJobIds.size === 0) {
48
+ return [];
49
+ }
50
+ const findings = [];
51
+ const workflowPrivileged = hasWritePermission(facts.workflowPermissions);
52
+ facts.jobs.forEach((job, jobIndex) => {
53
+ if (!reachableJobIds.has(job.id)) {
54
+ return;
55
+ }
56
+ const jobPrivileged = workflowPrivileged || hasWritePermission(job.permissions) || hasInheritedSecrets(job.secrets);
57
+ job.steps.forEach((step, stepIndex) => {
58
+ if (!isCheckoutStep(step.uses)) {
59
+ return;
60
+ }
61
+ if (!isRiskyCheckoutRef(step.with?.ref)) {
62
+ return;
63
+ }
64
+ const evidence = buildFindingEvidence({
65
+ textContent: input.textContent,
66
+ searchTerms: [
67
+ "pull_request_target",
68
+ "actions/checkout",
69
+ step.with?.ref ?? "github.event.pull_request.head",
70
+ ],
71
+ fallbackValue: "pull_request_target workflow checks out untrusted PR head ref",
72
+ });
73
+ findings.push({
74
+ rule_id: "workflow-pr-target-checkout-head",
75
+ finding_id: `WORKFLOW_PR_TARGET_CHECKOUT_HEAD-${input.filePath}-${jobIndex}-${stepIndex}`,
76
+ severity: jobPrivileged ? "CRITICAL" : "HIGH",
77
+ category: "CI_TRIGGER",
78
+ layer: "L2",
79
+ file_path: input.filePath,
80
+ location: { field: `jobs.${job.id}.steps[${stepIndex}].with.ref` },
81
+ description: "pull_request_target job checks out pull request head ref, enabling untrusted code execution in privileged context",
82
+ affected_tools: ["github-actions"],
83
+ cve: null,
84
+ owasp: ["ASI02"],
85
+ cwe: "CWE-284",
86
+ confidence: "HIGH",
87
+ fixable: false,
88
+ remediation_actions: [
89
+ "Avoid checking out pull request head refs in pull_request_target workflows",
90
+ "Use pull_request for untrusted code validation and keep privileged operations isolated",
91
+ "Enforce least-privilege token scopes and avoid inherited secrets for untrusted paths",
92
+ ],
93
+ evidence: evidence?.evidence ?? null,
94
+ suppressed: false,
95
+ });
96
+ });
97
+ });
98
+ return findings;
99
+ }
@@ -0,0 +1,8 @@
1
+ import type { Finding } from "../../types/finding.js";
2
+ export interface WorkflowSecretExfiltrationInput {
3
+ filePath: string;
4
+ parsed: unknown;
5
+ textContent: string;
6
+ trustedApiDomains: string[];
7
+ }
8
+ export declare function detectWorkflowSecretExfiltration(input: WorkflowSecretExfiltrationInput): Finding[];
@@ -0,0 +1,97 @@
1
+ import { buildFindingEvidence } from "../evidence.js";
2
+ import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
3
+ const OUTBOUND_COMMAND_PATTERN = /\b(curl|wget|invoke-webrequest|httpie)\b/iu;
4
+ const SECRET_REFERENCE_PATTERN = /\$\{\{\s*secrets\.([a-zA-Z0-9_]+)\s*\}\}/giu;
5
+ const URL_PATTERN = /https?:\/\/[^\s"')]+/giu;
6
+ function extractSecretReferences(value) {
7
+ const references = new Set();
8
+ for (const match of value.matchAll(SECRET_REFERENCE_PATTERN)) {
9
+ const key = match[1]?.trim();
10
+ if (!key) {
11
+ continue;
12
+ }
13
+ references.add(key);
14
+ }
15
+ return Array.from(references);
16
+ }
17
+ function extractUrls(value) {
18
+ return Array.from(value.matchAll(URL_PATTERN), (match) => match[0] ?? "").filter(Boolean);
19
+ }
20
+ function isTrustedHost(hostname, trustedApiDomains) {
21
+ const normalizedHost = hostname.toLowerCase();
22
+ return trustedApiDomains.some((domain) => {
23
+ const normalizedDomain = domain.toLowerCase();
24
+ return normalizedHost === normalizedDomain || normalizedHost.endsWith(`.${normalizedDomain}`);
25
+ });
26
+ }
27
+ function hasOnlyTrustedUrls(run, trustedApiDomains) {
28
+ const urls = extractUrls(run);
29
+ if (urls.length === 0) {
30
+ return false;
31
+ }
32
+ return urls.every((url) => {
33
+ try {
34
+ const parsed = new URL(url);
35
+ return isTrustedHost(parsed.hostname, trustedApiDomains);
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ });
41
+ }
42
+ export function detectWorkflowSecretExfiltration(input) {
43
+ if (!isGitHubWorkflowPath(input.filePath)) {
44
+ return [];
45
+ }
46
+ const facts = extractWorkflowFacts(input.parsed);
47
+ if (!facts) {
48
+ return [];
49
+ }
50
+ const findings = [];
51
+ facts.jobs.forEach((job, jobIndex) => {
52
+ job.steps.forEach((step, stepIndex) => {
53
+ if (!step.run || !OUTBOUND_COMMAND_PATTERN.test(step.run)) {
54
+ return;
55
+ }
56
+ const referencedSecrets = extractSecretReferences(step.run);
57
+ if (referencedSecrets.length === 0) {
58
+ return;
59
+ }
60
+ if (hasOnlyTrustedUrls(step.run, input.trustedApiDomains)) {
61
+ return;
62
+ }
63
+ const evidence = buildFindingEvidence({
64
+ textContent: input.textContent,
65
+ searchTerms: ["secrets.", "curl", "wget", "http://", "https://"],
66
+ fallbackValue: `${job.id} step references secrets in outbound network command`,
67
+ });
68
+ findings.push({
69
+ rule_id: "workflow-secret-exfiltration",
70
+ finding_id: `WORKFLOW_SECRET_EXFILTRATION-${input.filePath}-${jobIndex}-${stepIndex}`,
71
+ severity: "CRITICAL",
72
+ category: "CI_PERMISSIONS",
73
+ layer: "L2",
74
+ file_path: input.filePath,
75
+ location: { field: `jobs.${job.id}.steps[${stepIndex}].run` },
76
+ description: "Workflow step sends secret context through outbound network command",
77
+ affected_tools: ["github-actions"],
78
+ cve: null,
79
+ owasp: ["ASI02"],
80
+ cwe: "CWE-200",
81
+ confidence: "HIGH",
82
+ fixable: false,
83
+ remediation_actions: [
84
+ "Avoid sending secrets through outbound shell commands",
85
+ "Use trusted first-party actions with scoped credentials instead of ad-hoc exfil-prone scripts",
86
+ "Restrict outbound domains and sanitize command arguments in privileged workflows",
87
+ ],
88
+ metadata: {
89
+ referenced_secrets: referencedSecrets,
90
+ },
91
+ evidence: evidence?.evidence ?? null,
92
+ suppressed: false,
93
+ });
94
+ });
95
+ });
96
+ return findings;
97
+ }
@@ -41,6 +41,14 @@ import { detectDependabotExecution } from "./detectors/dependabot-execution.js";
41
41
  import { detectWorkflowHardcodedContainerCredentials } from "./detectors/workflow-hardcoded-container-credentials.js";
42
42
  import { detectWorkflowUnredactedSecrets } from "./detectors/workflow-unredacted-secrets.js";
43
43
  import { detectWorkflowBotConditions } from "./detectors/workflow-bot-conditions.js";
44
+ import { detectWorkflowPrTargetCheckoutHead } from "./detectors/workflow-pr-target-checkout-head.js";
45
+ import { detectWorkflowArtifactTrustChain } from "./detectors/workflow-artifact-trust-chain.js";
46
+ import { detectWorkflowCallBoundary } from "./detectors/workflow-call-boundary.js";
47
+ import { detectWorkflowSecretExfiltration } from "./detectors/workflow-secret-exfiltration.js";
48
+ import { detectWorkflowOidcUntrustedContext } from "./detectors/workflow-oidc-untrusted-context.js";
49
+ import { detectWorkflowDynamicMatrixInjection } from "./detectors/workflow-dynamic-matrix-injection.js";
50
+ import { detectDependabotAutoMerge } from "./detectors/dependabot-auto-merge.js";
51
+ import { detectWorkflowLocalActionMutation } from "./detectors/workflow-local-action-mutation.js";
44
52
  import { filterRegisteredAudits } from "./audits/registry.js";
45
53
  import { FINDING_CATEGORIES } from "../types/finding.js";
46
54
  import { buildFindingEvidence } from "./evidence.js";
@@ -536,6 +544,87 @@ function buildFileAudits() {
536
544
  })
537
545
  : [],
538
546
  },
547
+ {
548
+ id: "workflow-pr-target-checkout-head",
549
+ run: ({ file, input }) => input.config.workflowAuditsEnabled
550
+ ? detectWorkflowPrTargetCheckoutHead({
551
+ filePath: file.filePath,
552
+ parsed: file.parsed,
553
+ textContent: file.textContent,
554
+ })
555
+ : [],
556
+ },
557
+ {
558
+ id: "workflow-artifact-trust-chain",
559
+ run: ({ file, input }) => input.config.workflowAuditsEnabled
560
+ ? detectWorkflowArtifactTrustChain({
561
+ filePath: file.filePath,
562
+ parsed: file.parsed,
563
+ textContent: file.textContent,
564
+ })
565
+ : [],
566
+ },
567
+ {
568
+ id: "workflow-call-boundary",
569
+ run: ({ file, input }) => input.config.workflowAuditsEnabled
570
+ ? detectWorkflowCallBoundary({
571
+ filePath: file.filePath,
572
+ parsed: file.parsed,
573
+ textContent: file.textContent,
574
+ })
575
+ : [],
576
+ },
577
+ {
578
+ id: "workflow-secret-exfiltration",
579
+ run: ({ file, input }) => input.config.workflowAuditsEnabled
580
+ ? detectWorkflowSecretExfiltration({
581
+ filePath: file.filePath,
582
+ parsed: file.parsed,
583
+ textContent: file.textContent,
584
+ trustedApiDomains: input.config.trustedApiDomains,
585
+ })
586
+ : [],
587
+ },
588
+ {
589
+ id: "workflow-oidc-untrusted-context",
590
+ run: ({ file, input }) => input.config.workflowAuditsEnabled
591
+ ? detectWorkflowOidcUntrustedContext({
592
+ filePath: file.filePath,
593
+ parsed: file.parsed,
594
+ textContent: file.textContent,
595
+ })
596
+ : [],
597
+ },
598
+ {
599
+ id: "workflow-dynamic-matrix-injection",
600
+ run: ({ file, input }) => input.config.workflowAuditsEnabled
601
+ ? detectWorkflowDynamicMatrixInjection({
602
+ filePath: file.filePath,
603
+ parsed: file.parsed,
604
+ textContent: file.textContent,
605
+ })
606
+ : [],
607
+ },
608
+ {
609
+ id: "dependabot-auto-merge",
610
+ run: ({ file, input }) => input.config.workflowAuditsEnabled
611
+ ? detectDependabotAutoMerge({
612
+ filePath: file.filePath,
613
+ parsed: file.parsed,
614
+ textContent: file.textContent,
615
+ })
616
+ : [],
617
+ },
618
+ {
619
+ id: "workflow-local-action-mutation",
620
+ run: ({ file, input }) => input.config.workflowAuditsEnabled
621
+ ? detectWorkflowLocalActionMutation({
622
+ filePath: file.filePath,
623
+ parsed: file.parsed,
624
+ textContent: file.textContent,
625
+ })
626
+ : [],
627
+ },
539
628
  {
540
629
  id: "hardcoded-container-credentials",
541
630
  run: ({ file, input }) => input.config.workflowAuditsEnabled
@@ -0,0 +1,24 @@
1
+ import type { WorkflowFacts } from "./types.js";
2
+ export interface WorkflowArtifactTransferEdge {
3
+ artifactName: string;
4
+ producerJobId: string;
5
+ producerStepIndex: number;
6
+ consumerJobId: string;
7
+ consumerStepIndex: number;
8
+ consumerDownloadsAll: boolean;
9
+ }
10
+ export interface WorkflowCallBoundaryContext {
11
+ hasWorkflowCall: boolean;
12
+ declaredInputKeys: string[];
13
+ requiredInputKeys: string[];
14
+ declaredSecretKeys: string[];
15
+ requiredSecretKeys: string[];
16
+ jobsWithInheritedSecrets: string[];
17
+ jobsCallingReusableWorkflow: string[];
18
+ }
19
+ export declare function buildWorkflowNeedsGraph(facts: WorkflowFacts): Map<string, string[]>;
20
+ export declare function collectTransitiveDependencies(facts: WorkflowFacts, seedJobIds: Iterable<string>): Set<string>;
21
+ export declare function collectTransitiveDependents(facts: WorkflowFacts, seedJobIds: Iterable<string>): Set<string>;
22
+ export declare function collectArtifactTransferEdges(facts: WorkflowFacts): WorkflowArtifactTransferEdge[];
23
+ export declare function collectUntrustedReachableJobIds(facts: WorkflowFacts): Set<string>;
24
+ export declare function extractWorkflowCallBoundaryContext(parsed: unknown, facts: WorkflowFacts): WorkflowCallBoundaryContext;