1use std::collections::{BTreeMap, BTreeSet};
2
3use anyhow::Result;
4use serde_json::Value;
5
6use crate::{
7 ids::document_id,
8 model::{ArtifactDoc, EdgeDoc, WarningDoc},
9};
10
11fn edge(
12 repo: &str,
13 edge_type: &str,
14 from: &ArtifactDoc,
15 to: &ArtifactDoc,
16 reason: &str,
17 confidence: f32,
18) -> EdgeDoc {
19 EdgeDoc {
20 id: document_id(
21 repo,
22 "edge",
23 from.source_path.as_deref(),
24 from.line_start,
25 Some(&format!("{edge_type}:{}:{}", from.id, to.id)),
26 ),
27 repo: repo.to_owned(),
28 kind: "edge".to_owned(),
29 edge_type: edge_type.to_owned(),
30 from_id: from.id.clone(),
31 from_kind: from.kind.clone(),
32 from_name: from.name.clone(),
33 to_id: to.id.clone(),
34 to_kind: to.kind.clone(),
35 to_name: to.name.clone(),
36 confidence,
37 reason: reason.to_owned(),
38 source_path: from.source_path.clone(),
39 line_start: from.line_start,
40 risk_level: from.risk_level.clone(),
41 updated_at: chrono::Utc::now().to_rfc3339(),
42 }
43}
44
45fn warning(
46 repo: &str,
47 warning_type: &str,
48 message: String,
49 related: Option<&ArtifactDoc>,
50 risk_level: &str,
51) -> WarningDoc {
52 WarningDoc {
53 id: document_id(
54 repo,
55 "warning",
56 related.and_then(|item| item.source_path.as_deref()),
57 related.and_then(|item| item.line_start),
58 Some(warning_type),
59 ),
60 repo: repo.to_owned(),
61 kind: "warning".to_owned(),
62 warning_type: warning_type.to_owned(),
63 severity: if risk_level == "critical" {
64 "error"
65 } else {
66 "warning"
67 }
68 .to_owned(),
69 message,
70 source_path: related.and_then(|item| item.source_path.clone()),
71 line_start: related.and_then(|item| item.line_start),
72 related_id: related.map(|item| item.id.clone()),
73 risk_level: risk_level.to_owned(),
74 remediation: None,
75 updated_at: chrono::Utc::now().to_rfc3339(),
76 }
77}
78
79pub fn link_all(
80 artifacts: &mut [ArtifactDoc],
81 warnings: &mut Vec<WarningDoc>,
82) -> Result<Vec<EdgeDoc>> {
83 let mut edges = Vec::new();
84 let mut by_name: BTreeMap<String, Vec<usize>> = BTreeMap::new();
85 let mut by_invoke: BTreeMap<String, usize> = BTreeMap::new();
86 let mut by_kind: BTreeMap<String, Vec<usize>> = BTreeMap::new();
87
88 for (index, artifact) in artifacts.iter().enumerate() {
89 if let Some(name) = &artifact.name {
90 by_name.entry(name.clone()).or_default().push(index);
91 }
92 if let Some(invoke_key) = artifact.data.get("invoke_key").and_then(Value::as_str) {
93 by_invoke.insert(invoke_key.to_owned(), index);
94 }
95 by_kind
96 .entry(artifact.kind.clone())
97 .or_default()
98 .push(index);
99 }
100
101 let mut test_links: BTreeMap<usize, BTreeSet<String>> = BTreeMap::new();
102
103 for artifact in artifacts.iter() {
104 if artifact.kind == "frontend_hook_use" {
105 if let Some(name) = artifact.data.get("hook_def_name").and_then(Value::as_str) {
106 if let Some(target_index) =
107 by_kind.get("frontend_hook_def").and_then(|candidates| {
108 candidates
109 .iter()
110 .copied()
111 .find(|candidate| artifacts[*candidate].name.as_deref() == Some(name))
112 })
113 {
114 edges.push(edge(
115 &artifact.repo,
116 "uses_hook",
117 artifact,
118 &artifacts[target_index],
119 "hook callsite name matches hook definition",
120 0.95,
121 ));
122 }
123 }
124 }
125
126 if artifact.kind == "tauri_invoke" {
127 if let Some(invoke_key) = artifact.data.get("invoke_key").and_then(Value::as_str) {
128 if let Some(target_index) = by_invoke
129 .get(invoke_key)
130 .copied()
131 .filter(|candidate| artifacts[*candidate].kind == "tauri_plugin_command")
132 .or_else(|| {
133 let name = artifact.data.get("command_name").and_then(Value::as_str)?;
134 by_kind.get("tauri_command").and_then(|candidates| {
135 candidates.iter().copied().find(|candidate| {
136 artifacts[*candidate].name.as_deref() == Some(name)
137 })
138 })
139 })
140 {
141 let edge_type = if artifacts[target_index].kind == "tauri_plugin_command" {
142 "invokes_plugin_command"
143 } else {
144 "invokes"
145 };
146 edges.push(edge(
147 &artifact.repo,
148 edge_type,
149 artifact,
150 &artifacts[target_index],
151 "invoke key matches command registration",
152 0.98,
153 ));
154 }
155 }
156 }
157
158 if artifact.kind == "tauri_plugin_binding" {
159 if let Some(invoke_key) = artifact.data.get("invoke_key").and_then(Value::as_str) {
160 if let Some(target_index) = by_invoke
161 .get(invoke_key)
162 .copied()
163 .filter(|candidate| artifacts[*candidate].kind == "tauri_plugin_command")
164 {
165 edges.push(edge(
166 &artifact.repo,
167 "invokes_plugin_command",
168 artifact,
169 &artifacts[target_index],
170 "plugin binding invoke key matches plugin command",
171 0.99,
172 ));
173 }
174 }
175 }
176
177 if artifact.kind == "tauri_capability" {
178 let capability_permissions = artifact
179 .data
180 .get("permissions")
181 .and_then(Value::as_array)
182 .cloned()
183 .unwrap_or_default();
184 for permission in capability_permissions.iter().filter_map(Value::as_str) {
185 if let Some(target_index) = by_kind.get("tauri_permission").and_then(|candidates| {
186 candidates.iter().copied().find(|candidate| {
187 artifacts[*candidate]
188 .name
189 .as_deref()
190 .map(|name| {
191 name == permission || name.ends_with(&format!(":{permission}"))
192 })
193 .unwrap_or(false)
194 })
195 }) {
196 edges.push(edge(
197 &artifact.repo,
198 "capability_grants_permission",
199 artifact,
200 &artifacts[target_index],
201 "capability permissions include permission identifier",
202 0.9,
203 ));
204 }
205 }
206 }
207
208 if artifact.kind == "frontend_test" {
209 let imports = artifact
210 .data
211 .get("imports")
212 .and_then(Value::as_array)
213 .cloned()
214 .unwrap_or_default();
215 let mocked_commands = artifact
216 .data
217 .get("mocked_commands")
218 .and_then(Value::as_array)
219 .cloned()
220 .unwrap_or_default();
221 for import in imports.iter().filter_map(Value::as_str) {
222 if let Some(candidates) = by_name.get(import) {
223 for target_index in candidates {
224 test_links
225 .entry(*target_index)
226 .or_default()
227 .insert(artifact.source_path.clone().unwrap_or_default());
228 edges.push(edge(
229 &artifact.repo,
230 "tested_by",
231 &artifacts[*target_index],
232 artifact,
233 "frontend test imports symbol",
234 0.9,
235 ));
236 }
237 }
238 }
239 for command in mocked_commands.iter().filter_map(Value::as_str) {
240 if let Some(target_index) = by_invoke.get(command).copied() {
241 test_links
242 .entry(target_index)
243 .or_default()
244 .insert(artifact.source_path.clone().unwrap_or_default());
245 edges.push(edge(
246 &artifact.repo,
247 "mocked_by",
248 &artifacts[target_index],
249 artifact,
250 "frontend test mocks invoke key",
251 0.95,
252 ));
253 }
254 }
255 }
256
257 if artifact.kind == "rust_test" {
258 let targets = artifact
259 .data
260 .get("targets")
261 .and_then(Value::as_array)
262 .cloned()
263 .unwrap_or_default();
264 for target in targets.iter().filter_map(Value::as_str) {
265 if let Some(candidates) = by_name.get(target) {
266 for target_index in candidates {
267 test_links
268 .entry(*target_index)
269 .or_default()
270 .insert(artifact.source_path.clone().unwrap_or_default());
271 edges.push(edge(
272 &artifact.repo,
273 "tested_by",
274 &artifacts[*target_index],
275 artifact,
276 "rust test target name matches artifact name",
277 0.85,
278 ));
279 }
280 }
281 }
282 }
283 }
284
285 for (index, related_tests) in test_links {
286 let artifact = &mut artifacts[index];
287 artifact.related_tests = related_tests.into_iter().collect();
288 artifact.has_related_tests = !artifact.related_tests.is_empty();
289 }
290
291 let warning_exists = |warning_type: &str, related_id: &str, warnings: &[WarningDoc]| {
292 warnings.iter().any(|item| {
293 item.warning_type == warning_type && item.related_id.as_deref() == Some(related_id)
294 })
295 };
296
297 for artifact in artifacts.iter() {
298 if (artifact.kind == "tauri_command" || artifact.kind == "tauri_plugin_command")
299 && artifact.risk_level != "low"
300 && artifact.related_tests.is_empty()
301 {
302 warnings.push(warning(
303 &artifact.repo,
304 "missing_related_test",
305 format!(
306 "{} is high-risk and has no related tests",
307 artifact.name.clone().unwrap_or_default()
308 ),
309 Some(artifact),
310 &artifact.risk_level,
311 ));
312 }
313
314 if artifact.kind == "tauri_plugin_command" {
315 let plugin_name = artifact.data.get("plugin_name").and_then(Value::as_str);
316 let command_name = artifact.name.as_deref().unwrap_or_default();
317 let has_permission = artifacts.iter().any(|candidate| {
318 candidate.kind == "tauri_permission"
319 && candidate.data.get("plugin_name").and_then(Value::as_str) == plugin_name
320 && candidate
321 .data
322 .get("commands_allow")
323 .and_then(Value::as_array)
324 .map(|items| items.iter().any(|item| item.as_str() == Some(command_name)))
325 .unwrap_or(false)
326 });
327 if !has_permission
328 && !warning_exists(
329 "plugin_command_without_permission_evidence",
330 &artifact.id,
331 warnings,
332 )
333 {
334 warnings.push(warning(
335 &artifact.repo,
336 "plugin_command_without_permission_evidence",
337 format!(
338 "{} has no permission evidence",
339 artifact.name.clone().unwrap_or_default()
340 ),
341 Some(artifact),
342 &artifact.risk_level,
343 ));
344 }
345 }
346
347 if artifact.kind == "tauri_command"
348 && !warning_exists(
349 "command_without_permission_evidence",
350 &artifact.id,
351 warnings,
352 )
353 {
354 warnings.push(warning(
355 &artifact.repo,
356 "command_without_permission_evidence",
357 format!(
358 "{} has no permission evidence",
359 artifact.name.clone().unwrap_or_default()
360 ),
361 Some(artifact),
362 &artifact.risk_level,
363 ));
364 }
365 }
366
367 Ok(edges)
368}