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 "path_aliases",
86 "http_method",
87 "invoke_key",
88 "command_name",
89 "plugin_name",
90 "plugin_export",
91 "hook_name",
92 "hook_kind",
93 "event_name",
94 "channel_name",
95 "rust_fqn",
96 "component",
97 "display_name",
98 "signature",
99 "source_path",
100 "bundle_path",
101 "nearest_symbol",
102 "permissions",
103 "effective_capabilities",
104 "target_rust_commands",
105 "called_by_frontend",
106 "related_symbols",
107 "related_php_symbols",
108 "related_tests",
109 "primary_component",
110 "primary_wrapper",
111 "primary_transport",
112 "source_paths",
113 "risk_reasons",
114 "tags",
115 "comments",
116 "package_name"
117 ],
118 "filterableAttributes": [
119 "repo",
120 "kind",
121 "side",
122 "language",
123 "source_path",
124 "package_name",
125 "risk_level",
126 "contains_phi",
127 "has_related_tests",
128 "normalized_path",
129 "path_aliases",
130 "http_method",
131 "command_name",
132 "invoke_key",
133 "plugin_name",
134 "plugin_export",
135 "hook_name",
136 "hook_kind",
137 "component",
138 "event_name",
139 "channel_name",
140 "window_label",
141 "webview_label",
142 "capability_id",
143 "permission_id",
144 "merged_capabilities",
145 "remote_capability",
146 "from_id",
147 "to_id",
148 "from_kind",
149 "to_kind",
150 "edge_type",
151 "warning_type",
152 "severity"
153 ],
154 "sortableAttributes": ["confidence", "updated_at"],
155 "rankingRules": [
156 "words",
157 "typo",
158 "proximity",
159 "attribute",
160 "sort",
161 "exactness"
162 ]
163 })
164}