@clear-capabilities/agentic-security-scanner 0.162.0 → 0.163.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 (153) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bin/agentic-security.js +87 -0
  3. package/dist/1340.index.js +1871 -0
  4. package/dist/3477.index.js +727 -0
  5. package/dist/3499.index.js +23 -3
  6. package/dist/4113.index.js +1 -1
  7. package/dist/5014.index.js +833 -0
  8. package/dist/5756.index.js +2 -2
  9. package/dist/6257.index.js +1 -1
  10. package/dist/6907.index.js +248 -0
  11. package/dist/6994.index.js +1 -1
  12. package/dist/7033.index.js +33 -5
  13. package/dist/7439.index.js +1822 -0
  14. package/dist/7838.index.js +1 -1
  15. package/dist/9560.index.js +2318 -23
  16. package/dist/957.index.js +1 -1
  17. package/dist/9999.index.js +43 -7
  18. package/dist/agentic-security.mjs +12 -12
  19. package/dist/agentic-security.mjs.sha256 +1 -1
  20. package/package.json +33 -6
  21. package/src/capabilities/CLAUDE.md +114 -0
  22. package/src/capabilities/address.js +73 -0
  23. package/src/capabilities/attack-coverage.js +93 -0
  24. package/src/capabilities/decide.js +256 -0
  25. package/src/capabilities/delegate.js +69 -0
  26. package/src/capabilities/egress.js +20 -0
  27. package/src/capabilities/hook-advice.js +50 -0
  28. package/src/capabilities/index.js +32 -0
  29. package/src/capabilities/manifest.js +344 -0
  30. package/src/capabilities/outbound.js +189 -0
  31. package/src/capabilities/paths.js +57 -0
  32. package/src/capabilities/probes.js +211 -0
  33. package/src/capabilities/proxy.js +182 -0
  34. package/src/capabilities/reasons.js +67 -0
  35. package/src/capabilities/receipts.js +356 -0
  36. package/src/capabilities/records.js +58 -0
  37. package/src/capabilities/recovery.js +367 -0
  38. package/src/capabilities/report.js +81 -0
  39. package/src/capabilities/runner.js +236 -0
  40. package/src/capabilities/secrets.js +26 -0
  41. package/src/capabilities/tool-gate.js +119 -0
  42. package/src/capabilities/tool-registry.js +60 -0
  43. package/src/dataflow/CLAUDE.md +29 -0
  44. package/src/dataflow/catalog.js +21 -0
  45. package/src/dataflow/engine.js +20 -0
  46. package/src/dataflow/guard-dominance.js +352 -0
  47. package/src/discovery/CLAUDE.md +9 -0
  48. package/src/discovery/index.js +15 -1
  49. package/src/discovery/lenses.js +18 -0
  50. package/src/engine.js +80 -15
  51. package/src/integrations/index.js +5 -2
  52. package/src/ir/CLAUDE.md +14 -0
  53. package/src/ir/implicit-handler-params.js +74 -0
  54. package/src/ir/parser-cs.js +20 -0
  55. package/src/ir/parser-go.js +30 -13
  56. package/src/ir/parser-java.js +11 -0
  57. package/src/language/nix-eval-isolation.js +33 -5
  58. package/src/lineage/CLAUDE.md +31 -0
  59. package/src/lineage/deployment/adapter-cloud.js +202 -0
  60. package/src/lineage/deployment/adapter-compose.js +90 -0
  61. package/src/lineage/deployment/adapter-kit.js +142 -0
  62. package/src/lineage/deployment/adapter-kubernetes.js +247 -0
  63. package/src/lineage/deployment/attack-paths.js +443 -0
  64. package/src/lineage/deployment/boundaries-run.js +223 -0
  65. package/src/lineage/deployment/boundary-graph.js +567 -0
  66. package/src/lineage/deployment/cross-repo.js +253 -0
  67. package/src/lineage/deployment/drift.js +383 -0
  68. package/src/lineage/deployment/ingest.js +221 -0
  69. package/src/lineage/deployment/projection.js +244 -0
  70. package/src/lineage/deployment/trace-correlation.js +541 -0
  71. package/src/lineage/source-registry.js +19 -0
  72. package/src/mcp/CLAUDE.md +5 -2
  73. package/src/mcp/invariant-tools.js +64 -0
  74. package/src/mcp/portfolio-tools.js +79 -0
  75. package/src/mcp/redact.js +15 -0
  76. package/src/mcp/server.js +24 -3
  77. package/src/mcp/tools.js +13 -1
  78. package/src/posture/CLAUDE.md +332 -1
  79. package/src/posture/accuracy-scorecard.js +95 -4
  80. package/src/posture/assurance/rollback.js +214 -0
  81. package/src/posture/assurance/rollout.js +200 -0
  82. package/src/posture/business-logic.js +26 -4
  83. package/src/posture/evaluation/bakeoff.js +148 -0
  84. package/src/posture/evaluation/custody.js +200 -0
  85. package/src/posture/evaluation/deployment-ablation.js +416 -0
  86. package/src/posture/evaluation/dev-cases.js +65 -0
  87. package/src/posture/evaluation/economics.js +85 -0
  88. package/src/posture/evaluation/gates.js +161 -0
  89. package/src/posture/evaluation/grouping.js +101 -0
  90. package/src/posture/evaluation/interval.js +132 -0
  91. package/src/posture/evaluation/invariant-ablation.js +325 -0
  92. package/src/posture/evaluation/labels.js +200 -0
  93. package/src/posture/evaluation/protocol.js +308 -0
  94. package/src/posture/evaluation/release-gate.js +209 -0
  95. package/src/posture/evaluation/report.js +158 -0
  96. package/src/posture/evaluation/runner.js +273 -0
  97. package/src/posture/evaluation/scan-child.js +80 -0
  98. package/src/posture/evaluation/score.js +174 -0
  99. package/src/posture/evaluation/synthetic.js +90 -0
  100. package/src/posture/evaluation/why-missed.js +219 -0
  101. package/src/posture/fleet.js +11 -2
  102. package/src/posture/invariants/cli.js +95 -0
  103. package/src/posture/invariants/coverage.js +164 -0
  104. package/src/posture/invariants/export.js +178 -0
  105. package/src/posture/invariants/expressions.js +46 -0
  106. package/src/posture/invariants/lifecycle.js +289 -0
  107. package/src/posture/invariants/project-input.js +55 -0
  108. package/src/posture/invariants/repair.js +340 -0
  109. package/src/posture/invariants/run.js +45 -0
  110. package/src/posture/invariants/scenarios.js +311 -0
  111. package/src/posture/invariants/schema.js +167 -0
  112. package/src/posture/invariants/shrink.js +407 -0
  113. package/src/posture/invariants/state-assertions.js +165 -0
  114. package/src/posture/logic-claims.js +12 -0
  115. package/src/posture/model-routing.js +40 -0
  116. package/src/posture/oracles/adapters.js +172 -1
  117. package/src/posture/oracles/conformance.js +3 -2
  118. package/src/posture/oracles/oracle.js +23 -3
  119. package/src/posture/oracles/registry.js +1 -1
  120. package/src/posture/portfolio/backend.js +279 -0
  121. package/src/posture/portfolio/bundle.js +314 -0
  122. package/src/posture/portfolio/cli.js +142 -0
  123. package/src/posture/portfolio/manifest.js +263 -0
  124. package/src/posture/portfolio/progress.js +267 -0
  125. package/src/posture/portfolio/resume.js +130 -0
  126. package/src/posture/portfolio/retention.js +207 -0
  127. package/src/posture/portfolio/scheduler.js +488 -0
  128. package/src/posture/portfolio/signing.js +155 -0
  129. package/src/posture/portfolio/wording.js +59 -0
  130. package/src/posture/portfolio/work-units.js +405 -0
  131. package/src/posture/replay/replay.js +1 -0
  132. package/src/posture/routing/adapter.js +140 -0
  133. package/src/posture/routing/calibration.js +278 -0
  134. package/src/posture/routing/canary.js +141 -0
  135. package/src/posture/routing/control.js +35 -0
  136. package/src/posture/routing/decide.js +211 -0
  137. package/src/posture/routing/drift.js +137 -0
  138. package/src/posture/routing/economics.js +185 -0
  139. package/src/posture/routing/feedback.js +207 -0
  140. package/src/posture/routing/index.js +31 -0
  141. package/src/posture/routing/outcomes.js +281 -0
  142. package/src/posture/routing/promotion.js +291 -0
  143. package/src/posture/routing/receipts.js +59 -0
  144. package/src/posture/routing/report.js +142 -0
  145. package/src/posture/routing/shadow.js +53 -0
  146. package/src/posture/specification-mining.js +26 -0
  147. package/src/posture/verification/patch-negative.js +16 -4
  148. package/src/posture/verification/patch-promotion.js +10 -2
  149. package/src/report/index.js +51 -1
  150. package/src/sandbox/CLAUDE.md +10 -0
  151. package/src/sandbox/backend-userspace.js +91 -13
  152. package/dist/5613.index.js +0 -500
  153. package/dist/6239.index.js +0 -469
@@ -0,0 +1,247 @@
1
+ // adapter-kubernetes.js: Kubernetes manifests, Ingress, Gateway API HTTPRoute,
2
+ // RBAC and NetworkPolicy into deployment boundary nodes and edges (X-302).
3
+ //
4
+ // Reads only the fields that name a topology relation. Container environment
5
+ // variables, Secret objects, ConfigMap data and image references are never
6
+ // read, so no credential or payload can reach the graph through this adapter.
7
+ //
8
+ // Two passes: the first indexes every object in the supplied files, the second
9
+ // emits nodes and edges, so a Service can be matched to a workload declared
10
+ // later in the same bundle. A reference to something not in the supplied
11
+ // files (a ServiceAccount, a Role, a backend Service, a selector with no
12
+ // matching workload) becomes an unresolved node and a typed gap.
13
+
14
+ import { adapterContext, parseYamlDocuments, isObject, asArray, str, selectorMatches } from './adapter-kit.js';
15
+
16
+ export const KUBERNETES_PARSER = 'kubernetes-manifest';
17
+ export const KUBERNETES_PARSER_VERSION = '1';
18
+
19
+ const WORKLOAD_KINDS = new Set(['Deployment', 'StatefulSet', 'DaemonSet', 'ReplicaSet', 'Job', 'CronJob', 'Pod']);
20
+ const SKIPPED_KINDS = new Set(['Secret', 'ConfigMap']);
21
+
22
+ function ns(o) { return str(o?.metadata?.namespace) ?? 'default'; }
23
+ function nm(o) { return str(o?.metadata?.name); }
24
+
25
+ function podTemplate(o) {
26
+ if (o.kind === 'Pod') return { spec: o.spec, labels: o.metadata?.labels };
27
+ if (o.kind === 'CronJob') return { spec: o.spec?.jobTemplate?.spec?.template?.spec, labels: o.spec?.jobTemplate?.spec?.template?.metadata?.labels };
28
+ return { spec: o.spec?.template?.spec, labels: o.spec?.template?.metadata?.labels };
29
+ }
30
+
31
+ /**
32
+ * Does a NetworkPolicy podSelector pick this workload? An empty selector picks
33
+ * every workload in the namespace; matchExpressions are not evaluated, so a
34
+ * selector that uses them selects nothing (and the caller reports a gap).
35
+ */
36
+ function podSelectorPicks(sel, labels) {
37
+ if (!isObject(sel)) return false;
38
+ const keys = Object.keys(sel);
39
+ if (keys.length === 0) return true;
40
+ if (keys.includes('matchExpressions')) return false;
41
+ const ml = sel.matchLabels;
42
+ if (!isObject(ml) || Object.keys(ml).length === 0) return keys.every(k => k === 'matchLabels');
43
+ return selectorMatches(ml, labels);
44
+ }
45
+
46
+ function tenantOf(...labelMaps) {
47
+ for (const m of labelMaps) if (isObject(m) && typeof m.tenant === 'string' && /^[A-Za-z0-9._-]{1,64}$/.test(m.tenant)) return m.tenant;
48
+ return null;
49
+ }
50
+
51
+ /** @returns {{nodes: object[], edges: object[], gaps: object[]}} */
52
+ export function parseKubernetes(text, fileCtx) {
53
+ const kit = adapterContext({ ...fileCtx, parser: KUBERNETES_PARSER, parserVersion: KUBERNETES_PARSER_VERSION });
54
+ const parsed = parseYamlDocuments(text, fileCtx.file);
55
+ if (parsed.gap) { kit.gaps.push(parsed.gap); return kit.result(); }
56
+
57
+ const objects = [];
58
+ for (const d of parsed.docs) {
59
+ if (!isObject(d) || !str(d.kind) || !str(d.apiVersion)) {
60
+ kit.gap('unsupported-format', fileCtx.file, 'a YAML document without apiVersion and kind is not a Kubernetes object and is skipped');
61
+ continue;
62
+ }
63
+ if (SKIPPED_KINDS.has(d.kind)) continue; // never read secret-bearing kinds
64
+ if (!nm(d)) { kit.gap('malformed-input', `${d.kind}`, `a ${d.kind} without metadata.name is skipped`); continue; }
65
+ objects.push(d);
66
+ }
67
+
68
+ // ---- pass 1: index
69
+ const workloads = [];
70
+ const serviceAccounts = new Set();
71
+ const services = new Map();
72
+ const roles = new Map();
73
+ for (const o of objects) {
74
+ if (WORKLOAD_KINDS.has(o.kind)) {
75
+ const t = podTemplate(o);
76
+ workloads.push({ obj: o, namespace: ns(o), name: nm(o), labels: isObject(t.labels) ? t.labels : {}, spec: isObject(t.spec) ? t.spec : {} });
77
+ } else if (o.kind === 'ServiceAccount') serviceAccounts.add(`${ns(o)}/${nm(o)}`);
78
+ else if (o.kind === 'Service') services.set(`${ns(o)}/${nm(o)}`, o);
79
+ else if (o.kind === 'Role') roles.set(`Role:${ns(o)}/${nm(o)}`, o);
80
+ else if (o.kind === 'ClusterRole') roles.set(`ClusterRole:${nm(o)}`, o);
81
+ }
82
+
83
+ // NetworkPolicy: which workloads are isolated, and which peers may reach them.
84
+ const policies = objects.filter(o => o.kind === 'NetworkPolicy');
85
+ const isolation = new Map(); // workload key -> 'deny-all' | 'restricted'
86
+ const wkey = (w) => `${w.namespace}/${w.name}`;
87
+ for (const p of policies) {
88
+ const types = asArray(p.spec?.policyTypes);
89
+ const selects = workloads.filter(w => w.namespace === ns(p) && podSelectorPicks(p.spec?.podSelector, w.labels));
90
+ if (selects.length === 0) { kit.gap('unresolved-identity', `NetworkPolicy ${ns(p)}/${nm(p)}`, 'the policy selects no workload in the supplied manifests'); continue; }
91
+ const hasIngress = types.includes('Ingress') || (types.length === 0 && Array.isArray(p.spec?.ingress));
92
+ for (const w of selects) {
93
+ if (!hasIngress) continue;
94
+ const rules = asArray(p.spec?.ingress);
95
+ const prior = isolation.get(wkey(w));
96
+ isolation.set(wkey(w), rules.length === 0 ? (prior ?? 'deny-all') : 'restricted');
97
+ }
98
+ }
99
+
100
+ // ---- pass 2: emit
101
+ const wNodes = new Map();
102
+ for (const w of workloads) {
103
+ const tenant = tenantOf(w.labels, w.obj.metadata?.labels);
104
+ const attrs = { workloadKind: w.obj.kind, namespace: w.namespace };
105
+ const iso = isolation.get(wkey(w));
106
+ if (iso) attrs.ingressPolicy = iso;
107
+ if (w.spec.hostNetwork === true) attrs.hostNetwork = true;
108
+ const node = kit.node('service', wkey(w), { tenant, trustZone: 'internal', attrs });
109
+ wNodes.set(wkey(w), node);
110
+ const saName = str(w.spec.serviceAccountName) ?? 'default';
111
+ const saKey = `${w.namespace}/${saName}`;
112
+ let ident;
113
+ if (serviceAccounts.has(saKey) || saName === 'default') {
114
+ ident = kit.node('identity', `sa/${saKey}`, { trustZone: 'internal', attrs: saName === 'default' && !serviceAccounts.has(saKey) ? { implicit: true } : {} });
115
+ } else {
116
+ ident = kit.unresolved('identity', `sa/${saKey}`, 'ServiceAccount is not in the supplied manifests', { trustZone: 'unknown' });
117
+ kit.gap('unresolved-identity', wkey(w), `serviceAccountName '${saName}' is not declared in the supplied manifests`);
118
+ }
119
+ kit.edge('assumes', node, ident, { confidence: 'high' });
120
+ }
121
+ for (const key of serviceAccounts) kit.node('identity', `sa/${key}`, { trustZone: 'internal' });
122
+
123
+ // RBAC
124
+ for (const b of objects.filter(o => o.kind === 'RoleBinding' || o.kind === 'ClusterRoleBinding')) {
125
+ const refKind = b.roleRef?.kind, refName = str(b.roleRef?.name);
126
+ if (!refName || (refKind !== 'Role' && refKind !== 'ClusterRole')) { kit.gap('malformed-input', `${b.kind} ${nm(b)}`, 'roleRef is missing or has an unsupported kind'); continue; }
127
+ const cluster = b.kind === 'ClusterRoleBinding';
128
+ const roleKey = refKind === 'ClusterRole' ? `ClusterRole:${refName}` : `Role:${ns(b)}/${refName}`;
129
+ const role = roles.get(roleKey);
130
+ const scope = cluster ? 'cluster' : `ns:${ns(b)}`;
131
+ for (const s of asArray(b.subjects)) {
132
+ let ident;
133
+ if (s?.kind === 'ServiceAccount' && str(s.name)) ident = kit.node('identity', `sa/${str(s.namespace) ?? ns(b)}/${s.name}`, { trustZone: 'internal' });
134
+ else if ((s?.kind === 'User' || s?.kind === 'Group') && str(s.name)) ident = kit.node('identity', `${s.kind.toLowerCase()}/${s.name}`, { trustZone: 'internal' });
135
+ else { kit.gap('malformed-input', `${b.kind} ${nm(b)}`, 'a subject without kind and name is skipped'); continue; }
136
+ if (!role) {
137
+ const target = kit.unresolved('resource', `k8s/${scope}/role:${refName}`, `${refKind} '${refName}' is not in the supplied manifests`, { trustZone: 'unknown' });
138
+ kit.gap('unresolved-identity', `${b.kind} ${nm(b)}`, `${refKind} '${refName}' is not in the supplied manifests, so the permissions it grants are unknown`);
139
+ kit.edge('grants', ident, target, { effect: 'allow', confidence: 'low', discriminator: `role:${refName}` });
140
+ continue;
141
+ }
142
+ for (const rule of asArray(role.rules)) {
143
+ const verbs = asArray(rule?.verbs).filter(v => typeof v === 'string').sort();
144
+ for (const res of asArray(rule?.resources).filter(r => typeof r === 'string')) {
145
+ const target = kit.node('resource', `k8s/${scope}/${res}`, { trustZone: 'internal', attrs: res === '*' ? { wildcard: true } : {} });
146
+ kit.edge('grants', ident, target, { effect: 'allow', confidence: 'high', discriminator: verbs.join(',') || 'none', attrs: { verbs: verbs.join(',') || 'none' } });
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ // Services and what they route to
153
+ const svcNodes = new Map();
154
+ const svcNode = (nsName, name) => {
155
+ const key = `${nsName}/${name}`;
156
+ if (svcNodes.has(key)) return svcNodes.get(key);
157
+ const obj = services.get(key);
158
+ let node;
159
+ if (obj) {
160
+ const type = str(obj.spec?.type) ?? 'ClusterIP';
161
+ const internalLb = obj.metadata?.annotations && Object.entries(obj.metadata.annotations).some(([k, v]) => /internal/i.test(k) && String(v) === 'true');
162
+ const zone = type === 'LoadBalancer' ? (internalLb ? 'internal' : 'public') : type === 'NodePort' ? 'edge' : 'internal';
163
+ node = kit.node('route', `svc/${key}`, { tenant: tenantOf(obj.metadata?.labels), trustZone: zone, attrs: { serviceType: type, namespace: nsName } });
164
+ if (type === 'ExternalName' && str(obj.spec?.externalName)) {
165
+ const ext = kit.unresolved('resource', `external/${obj.spec.externalName}`, 'ExternalName target is outside the supplied manifests', { trustZone: 'public' });
166
+ kit.edge('routes-to', node, ext, { confidence: 'low' });
167
+ kit.gap('unresolved-identity', `Service ${key}`, `ExternalName '${obj.spec.externalName}' is not resolved`);
168
+ } else {
169
+ const sel = obj.spec?.selector;
170
+ const matched = workloads.filter(w => w.namespace === nsName && selectorMatches(sel, w.labels));
171
+ for (const w of matched) kit.edge('routes-to', node, wNodes.get(wkey(w)), { confidence: 'medium' });
172
+ if (matched.length === 0) {
173
+ const miss = kit.unresolved('service', `${key}#selector`, 'the Service selector matches no workload in the supplied manifests', { trustZone: 'unknown' });
174
+ kit.edge('routes-to', node, miss, { confidence: 'low' });
175
+ kit.gap('unresolved-identity', `Service ${key}`, 'the selector matches no workload in the supplied manifests');
176
+ }
177
+ }
178
+ } else {
179
+ node = kit.unresolved('route', `svc/${key}`, 'the Service is not in the supplied manifests', { trustZone: 'unknown' });
180
+ kit.gap('unresolved-identity', `Service ${key}`, 'a route targets a Service that is not in the supplied manifests');
181
+ }
182
+ svcNodes.set(key, node);
183
+ return node;
184
+ };
185
+ for (const key of services.keys()) { const [n, ...rest] = key.split('/'); svcNode(n, rest.join('/')); }
186
+
187
+ // Ingress
188
+ for (const ing of objects.filter(o => o.kind === 'Ingress')) {
189
+ const cls = String(ing.spec?.ingressClassName ?? ing.metadata?.annotations?.['kubernetes.io/ingress.class'] ?? '');
190
+ const zone = /internal/i.test(cls) ? 'internal' : 'public';
191
+ const tls = asArray(ing.spec?.tls).length > 0;
192
+ const rules = asArray(ing.spec?.rules);
193
+ const targets = [];
194
+ for (const r of rules) {
195
+ for (const p of asArray(r?.http?.paths)) targets.push({ host: str(r.host) ?? '*', path: str(p.path) ?? '/', backend: str(p.backend?.service?.name) });
196
+ }
197
+ const def = str(ing.spec?.defaultBackend?.service?.name);
198
+ if (def) targets.push({ host: '*', path: '/', backend: def });
199
+ if (targets.length === 0) kit.gap('malformed-input', `Ingress ${ns(ing)}/${nm(ing)}`, 'the Ingress has no routable rule');
200
+ for (const t of targets) {
201
+ const route = kit.node('route', `ingress/${ns(ing)}/${nm(ing)}:${t.host}${t.path}`, { tenant: tenantOf(ing.metadata?.labels), trustZone: zone, attrs: { host: t.host, path: t.path, tls } });
202
+ if (!t.backend) { kit.gap('malformed-input', `Ingress ${ns(ing)}/${nm(ing)}`, 'a rule without a backend service is skipped'); continue; }
203
+ kit.edge('routes-to', route, svcNode(ns(ing), t.backend), { confidence: 'high' });
204
+ }
205
+ }
206
+
207
+ // Gateway API HTTPRoute
208
+ for (const hr of objects.filter(o => o.kind === 'HTTPRoute')) {
209
+ const hosts = asArray(hr.spec?.hostnames).filter(h => typeof h === 'string');
210
+ const gateways = asArray(hr.spec?.parentRefs).map(p => str(p?.name)).filter(Boolean).sort().join(',');
211
+ for (const rule of asArray(hr.spec?.rules)) {
212
+ const paths = asArray(rule?.matches).map(m => str(m?.path?.value) ?? '/');
213
+ const pathList = paths.length ? paths : ['/'];
214
+ for (const host of hosts.length ? hosts : ['*']) for (const path of pathList) {
215
+ const route = kit.node('route', `httproute/${ns(hr)}/${nm(hr)}:${host}${path}`, { tenant: tenantOf(hr.metadata?.labels), trustZone: 'public', attrs: { host, path, gateways: gateways || 'none' } });
216
+ for (const ref of asArray(rule?.backendRefs)) {
217
+ const b = str(ref?.name);
218
+ if (b) kit.edge('routes-to', route, svcNode(str(ref.namespace) ?? ns(hr), b), { confidence: 'medium' });
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ // NetworkPolicy allows
225
+ for (const p of policies) {
226
+ const selected = workloads.filter(w => w.namespace === ns(p) && podSelectorPicks(p.spec?.podSelector, w.labels));
227
+ for (const rule of asArray(p.spec?.ingress)) {
228
+ for (const from of asArray(rule?.from)) {
229
+ const sources = [];
230
+ if (isObject(from.podSelector)) {
231
+ const pods = workloads.filter(w => (from.namespaceSelector ? true : w.namespace === ns(p)) && podSelectorPicks(from.podSelector, w.labels) && Object.keys(from.podSelector).length > 0);
232
+ for (const w of pods) sources.push(wNodes.get(wkey(w)));
233
+ if (pods.length === 0) kit.gap('unresolved-identity', `NetworkPolicy ${ns(p)}/${nm(p)}`, 'a peer selector matches no workload in the supplied manifests');
234
+ } else if (isObject(from.ipBlock) && str(from.ipBlock.cidr)) {
235
+ const cidr = from.ipBlock.cidr;
236
+ sources.push(kit.node('route', `cidr/${cidr}`, { trustZone: /^0\.0\.0\.0\/0$|^::\/0$/.test(cidr) ? 'public' : 'edge', attrs: { cidr } }));
237
+ } else if (isObject(from.namespaceSelector)) {
238
+ kit.gap('unresolved-identity', `NetworkPolicy ${ns(p)}/${nm(p)}`, 'a namespace-only peer selector cannot be resolved to workloads from the supplied manifests');
239
+ }
240
+ for (const src of sources) for (const w of selected) {
241
+ if (src && src !== wNodes.get(wkey(w))) kit.edge('network-allows', src, wNodes.get(wkey(w)), { confidence: 'medium', discriminator: `policy:${nm(p)}` });
242
+ }
243
+ }
244
+ }
245
+ }
246
+ return kit.result();
247
+ }