1use std::{
2 collections::BTreeSet,
3 fs,
4 io::{BufWriter, Write},
5 path::Path,
6};
7
8use anyhow::{Context, Result};
9use serde::Serialize;
10use serde_json::{json, Value};
11
12use crate::{
13 model::{ArtifactDoc, EdgeDoc, ScanSummary, WarningDoc},
14 scan::ScanBundle,
15};
16
17pub fn write_scan_bundle(output_dir: &Path, bundle: &ScanBundle) -> Result<()> {
18 fs::create_dir_all(output_dir)
19 .with_context(|| format!("failed to create {}", output_dir.display()))?;
20 write_ndjson(output_dir.join("artifacts.ndjson"), &bundle.artifacts)?;
21 write_ndjson(output_dir.join("edges.ndjson"), &bundle.edges)?;
22 write_ndjson(output_dir.join("warnings.ndjson"), &bundle.warnings)?;
23 fs::write(
24 output_dir.join("summary.json"),
25 serde_json::to_vec_pretty(&bundle.summary)?,
26 )?;
27 fs::write(
28 output_dir.join("project-info.json"),
29 serde_json::to_vec_pretty(&bundle.project_info)?,
30 )?;
31 fs::write(
32 output_dir.join("meili-settings.json"),
33 serde_json::to_vec_pretty(&default_meili_settings())?,
34 )?;
35 Ok(())
36}
37
38pub fn write_ndjson<T: Serialize>(path: impl AsRef<Path>, docs: &[T]) -> Result<()> {
39 let file = fs::File::create(path.as_ref())
40 .with_context(|| format!("failed to write {}", path.as_ref().display()))?;
41 let mut writer = BufWriter::new(file);
42 for doc in docs {
43 serde_json::to_writer(&mut writer, doc)?;
44 writer.write_all(b"\n")?;
45 }
46 writer.flush()?;
47 Ok(())
48}
49
50pub fn build_summary(
51 repo: &str,
52 artifacts: &[ArtifactDoc],
53 edges: &[EdgeDoc],
54 warnings: &[WarningDoc],
55) -> ScanSummary {
56 let artifact_kinds = artifacts
57 .iter()
58 .map(|item| item.kind.clone())
59 .collect::<BTreeSet<_>>()
60 .into_iter()
61 .collect();
62 let warning_types = warnings
63 .iter()
64 .map(|item| item.warning_type.clone())
65 .collect::<BTreeSet<_>>()
66 .into_iter()
67 .collect();
68
69 ScanSummary {
70 repo: repo.to_owned(),
71 artifact_count: artifacts.len(),
72 edge_count: edges.len(),
73 warning_count: warnings.len(),
74 artifact_kinds,
75 warning_types,
76 scanned_at: chrono::Utc::now().to_rfc3339(),
77 }
78}
79
80pub fn default_meili_settings() -> Value {
81 json!({
82 "searchableAttributes": [
83 "name",
84 "normalized_path",
85 "http_method",
86 "invoke_key",
87 "command_name",
88 "plugin_name",
89 "plugin_export",
90 "hook_name",
91 "hook_kind",
92 "event_name",
93 "channel_name",
94 "rust_fqn",
95 "component",
96 "display_name",
97 "signature",
98 "source_path",
99 "bundle_path",
100 "nearest_symbol",
101 "permissions",
102 "effective_capabilities",
103 "target_rust_commands",
104 "called_by_frontend",
105 "related_symbols",
106 "related_php_symbols",
107 "related_tests",
108 "primary_component",
109 "primary_wrapper",
110 "primary_transport",
111 "source_paths",
112 "risk_reasons",
113 "tags",
114 "comments",
115 "package_name"
116 ],
117 "filterableAttributes": [
118 "repo",
119 "kind",
120 "side",
121 "language",
122 "source_path",
123 "package_name",
124 "risk_level",
125 "contains_phi",
126 "has_related_tests",
127 "normalized_path",
128 "http_method",
129 "command_name",
130 "invoke_key",
131 "plugin_name",
132 "plugin_export",
133 "hook_name",
134 "hook_kind",
135 "component",
136 "event_name",
137 "channel_name",
138 "window_label",
139 "webview_label",
140 "capability_id",
141 "permission_id",
142 "merged_capabilities",
143 "remote_capability",
144 "from_id",
145 "to_id",
146 "from_kind",
147 "to_kind",
148 "edge_type",
149 "warning_type",
150 "severity"
151 ],
152 "sortableAttributes": ["confidence", "updated_at"],
153 "rankingRules": [
154 "words",
155 "typo",
156 "proximity",
157 "attribute",
158 "sort",
159 "exactness"
160 ]
161 })
162}