Skip to main content

verifyos_cli/report/
agent_pack.rs

1use crate::report::data::{AgentFinding, AgentPack, ReportData, ReportItem};
2use crate::rules::core::{RuleCategory, RuleStatus, Severity};
3use std::collections::HashSet;
4
5pub fn build_agent_pack(report: &ReportData) -> AgentPack {
6    let findings: Vec<AgentFinding> = report
7        .results
8        .iter()
9        .filter(|item| matches!(item.status, RuleStatus::Fail | RuleStatus::Error))
10        .map(|item| AgentFinding {
11            rule_id: item.rule_id.clone(),
12            rule_name: item.rule_name.clone(),
13            severity: item.severity,
14            category: item.category,
15            priority: agent_priority(item.severity).to_string(),
16            message: item
17                .message
18                .clone()
19                .unwrap_or_else(|| item.rule_name.clone()),
20            evidence: item.evidence.clone(),
21            recommendation: item.recommendation.clone(),
22            suggested_fix_scope: suggested_fix_scope(item),
23            target_files: target_files(item),
24            patch_hint: patch_hint(item),
25            why_it_fails_review: why_it_fails_review(item),
26        })
27        .collect();
28
29    AgentPack {
30        generated_at_unix: report.generated_at_unix,
31        total_findings: findings.len(),
32        findings,
33    }
34}
35
36pub fn apply_agent_pack_baseline(pack: &mut AgentPack, baseline: &ReportData) {
37    let baseline_keys: HashSet<String> = baseline
38        .results
39        .iter()
40        .filter(|item| matches!(item.status, RuleStatus::Fail | RuleStatus::Error))
41        .map(agent_pack_baseline_key_from_report)
42        .collect();
43
44    pack.findings.retain(|finding| {
45        let key = agent_pack_baseline_key_from_finding(finding);
46        !baseline_keys.contains(&key)
47    });
48    pack.total_findings = pack.findings.len();
49}
50
51pub fn render_agent_pack_markdown(pack: &AgentPack) -> String {
52    let mut out = String::new();
53    out.push_str("# verifyOS Agent Fix Pack\n\n");
54    out.push_str(&format!("- Generated at: `{}`\n", pack.generated_at_unix));
55    out.push_str(&format!("- Total findings: `{}`\n\n", pack.total_findings));
56
57    if pack.findings.is_empty() {
58        out.push_str("## Findings\n\n- No failing findings.\n");
59        return out;
60    }
61
62    let mut findings = pack.findings.clone();
63    findings.sort_by(|a, b| {
64        a.suggested_fix_scope
65            .cmp(&b.suggested_fix_scope)
66            .then_with(|| a.rule_id.cmp(&b.rule_id))
67    });
68
69    out.push_str("## Findings by Fix Scope\n\n");
70
71    let mut current_scope: Option<&str> = None;
72    for finding in &findings {
73        let scope = finding.suggested_fix_scope.as_str();
74        if current_scope != Some(scope) {
75            if current_scope.is_some() {
76                out.push('\n');
77            }
78            out.push_str(&format!("### {}\n\n", scope));
79            current_scope = Some(scope);
80        }
81
82        out.push_str(&format!(
83            "- **{}** (`{}`)\n",
84            finding.rule_name, finding.rule_id
85        ));
86        out.push_str(&format!("  - Priority: `{}`\n", finding.priority));
87        out.push_str(&format!("  - Severity: `{:?}`\n", finding.severity));
88        out.push_str(&format!("  - Category: `{:?}`\n", finding.category));
89        out.push_str(&format!("  - Message: {}\n", finding.message));
90        if let Some(evidence) = &finding.evidence {
91            out.push_str(&format!("  - Evidence: {}\n", evidence));
92        }
93        if !finding.target_files.is_empty() {
94            out.push_str(&format!(
95                "  - Target files: {}\n",
96                finding.target_files.join(", ")
97            ));
98        }
99        out.push_str(&format!(
100            "  - Why it fails review: {}\n",
101            finding.why_it_fails_review
102        ));
103        out.push_str(&format!("  - Patch hint: {}\n", finding.patch_hint));
104        out.push_str(&format!("  - Recommendation: {}\n", finding.recommendation));
105    }
106
107    out
108}
109
110fn agent_pack_baseline_key_from_report(item: &ReportItem) -> String {
111    format!(
112        "{}|{}",
113        item.rule_id,
114        item.message.clone().unwrap_or_default().trim()
115    )
116}
117
118fn agent_pack_baseline_key_from_finding(item: &AgentFinding) -> String {
119    format!("{}|{}", item.rule_id, item.message.trim())
120}
121
122fn agent_priority(severity: Severity) -> &'static str {
123    match severity {
124        Severity::Error => "high",
125        Severity::Warning => "medium",
126        Severity::Info => "low",
127    }
128}
129
130fn suggested_fix_scope(item: &ReportItem) -> String {
131    match item.category {
132        RuleCategory::Privacy | RuleCategory::Permissions | RuleCategory::Metadata => {
133            "Info.plist".to_string()
134        }
135        RuleCategory::Entitlements | RuleCategory::Signing => "entitlements".to_string(),
136        RuleCategory::Bundling => "bundle-resources".to_string(),
137        RuleCategory::Ats => "ats-config".to_string(),
138        RuleCategory::ThirdParty => "dependencies".to_string(),
139        RuleCategory::Other => "app-bundle".to_string(),
140    }
141}
142
143fn target_files(item: &ReportItem) -> Vec<String> {
144    match item.rule_id.as_str() {
145        "RULE_USAGE_DESCRIPTIONS"
146        | "RULE_USAGE_DESCRIPTIONS_VALUE"
147        | "RULE_CAMERA_USAGE_DESCRIPTION"
148        | "RULE_LSAPPLICATIONQUERIESSCHEMES"
149        | "RULE_UIREQUIREDDEVICECAPABILITIES"
150        | "RULE_INFO_PLIST_VERSIONING" => vec!["Info.plist".to_string()],
151        "RULE_PRIVACY_MANIFEST" | "RULE_PRIVACY_SDK_CROSSCHECK" => {
152            vec!["PrivacyInfo.xcprivacy".to_string()]
153        }
154        "RULE_ATS_AUDIT" => vec!["Info.plist (NSAppTransportSecurity)".to_string()],
155        "RULE_BUNDLE_RESOURCE_LEAKAGE" => vec!["App bundle resources".to_string()],
156        "RULE_ENTITLEMENTS_MISMATCH"
157        | "RULE_ENTITLEMENTS_PROVISIONING_MISMATCH"
158        | "RULE_EXTENSION_ENTITLEMENTS"
159        | "RULE_DEBUG_ENTITLEMENTS" => vec![
160            "App entitlements plist".to_string(),
161            "embedded.mobileprovision".to_string(),
162        ],
163        "RULE_EMBEDDED_SIGNING_CONSISTENCY" => vec![
164            "Main app executable signature".to_string(),
165            "Embedded frameworks/extensions".to_string(),
166        ],
167        "RULE_PRIVATE_API" => vec!["Linked SDKs or app binary".to_string()],
168        _ => match item.category {
169            RuleCategory::Privacy | RuleCategory::Permissions | RuleCategory::Metadata => {
170                vec!["Info.plist".to_string()]
171            }
172            RuleCategory::Entitlements | RuleCategory::Signing => {
173                vec!["App signing and entitlements".to_string()]
174            }
175            RuleCategory::Bundling => vec!["App bundle resources".to_string()],
176            RuleCategory::Ats => vec!["Info.plist (NSAppTransportSecurity)".to_string()],
177            RuleCategory::ThirdParty => vec!["Embedded SDKs or dependencies".to_string()],
178            RuleCategory::Other => vec!["App bundle".to_string()],
179        },
180    }
181}
182
183fn patch_hint(item: &ReportItem) -> String {
184    match item.rule_id.as_str() {
185        "RULE_USAGE_DESCRIPTIONS"
186        | "RULE_USAGE_DESCRIPTIONS_VALUE"
187        | "RULE_CAMERA_USAGE_DESCRIPTION" => {
188            "Update Info.plist with the required NS*UsageDescription keys and give each key a user-facing reason that matches the in-app behavior.".to_string()
189        }
190        "RULE_LSAPPLICATIONQUERIESSCHEMES" => {
191            "Trim LSApplicationQueriesSchemes to only the schemes the app really probes, remove duplicates, and avoid private or overly broad schemes.".to_string()
192        }
193        "RULE_UIREQUIREDDEVICECAPABILITIES" => {
194            "Align UIRequiredDeviceCapabilities with real binary usage so review devices are not excluded by mistake and unsupported hardware is not declared.".to_string()
195        }
196        "RULE_INFO_PLIST_VERSIONING" => {
197            "Set a valid CFBundleShortVersionString and increment CFBundleVersion before the next submission.".to_string()
198        }
199        "RULE_PRIVACY_MANIFEST" => {
200            "Add PrivacyInfo.xcprivacy to the shipped bundle and declare the accessed APIs and collected data used by the app or bundled SDKs.".to_string()
201        }
202        "RULE_PRIVACY_SDK_CROSSCHECK" => {
203            "Review bundled SDKs and extend PrivacyInfo.xcprivacy so their accessed APIs and collected data are explicitly declared.".to_string()
204        }
205        "RULE_ATS_AUDIT" => {
206            "Narrow NSAppTransportSecurity exceptions, remove arbitrary loads when possible, and scope domain exceptions to the smallest set that works.".to_string()
207        }
208        "RULE_BUNDLE_RESOURCE_LEAKAGE" => {
209            "Remove secrets, certificates, provisioning artifacts, debug leftovers, and environment files from the packaged app bundle before archiving.".to_string()
210        }
211        "RULE_ENTITLEMENTS_MISMATCH" | "RULE_ENTITLEMENTS_PROVISIONING_MISMATCH" => {
212            "Make the exported entitlements match the provisioning profile and enabled capabilities for APNs, keychain groups, and iCloud.".to_string()
213        }
214        "RULE_EXTENSION_ENTITLEMENTS" => {
215            "Make each extension entitlement set a valid subset of the host app and add the extension-specific capabilities it actually needs.".to_string()
216        }
217        "RULE_DEBUG_ENTITLEMENTS" => {
218            "Strip debug-only entitlements like get-task-allow from release builds and regenerate the final signed archive.".to_string()
219        }
220        "RULE_EMBEDDED_SIGNING_CONSISTENCY" => {
221            "Re-sign embedded frameworks, dylibs, and extensions with the same Team ID and release identity as the host app.".to_string()
222        }
223        "RULE_PRIVATE_API" => {
224            "Remove or replace private API references in the app binary or third-party SDKs, then rebuild so the shipped binary no longer exposes them.".to_string()
225        }
226        _ => format!(
227            "Patch the {} scope first, then re-run voc to confirm the finding disappears.",
228            suggested_fix_scope(item)
229        ),
230    }
231}
232
233fn why_it_fails_review(item: &ReportItem) -> String {
234    match item.rule_id.as_str() {
235        "RULE_USAGE_DESCRIPTIONS"
236        | "RULE_USAGE_DESCRIPTIONS_VALUE"
237        | "RULE_CAMERA_USAGE_DESCRIPTION" => {
238            "App Review rejects binaries that touch protected APIs without clear, user-facing usage descriptions in Info.plist.".to_string()
239        }
240        "RULE_LSAPPLICATIONQUERIESSCHEMES" => {
241            "Overreaching canOpenURL allowlists look like app enumeration and often trigger manual review questions or rejection.".to_string()
242        }
243        "RULE_UIREQUIREDDEVICECAPABILITIES" => {
244            "Incorrect device capability declarations can exclude valid review devices or misrepresent the hardware the app actually requires.".to_string()
245        }
246        "RULE_INFO_PLIST_VERSIONING" => {
247            "Invalid or non-incrementing version metadata blocks submission and confuses App Store release processing.".to_string()
248        }
249        "RULE_PRIVACY_MANIFEST" | "RULE_PRIVACY_SDK_CROSSCHECK" => {
250            "Apple now expects accurate privacy manifests for apps and bundled SDKs, and missing declarations can block review.".to_string()
251        }
252        "RULE_ATS_AUDIT" => {
253            "Broad ATS exceptions weaken transport security and are a common reason App Review asks teams to justify or remove insecure settings.".to_string()
254        }
255        "RULE_BUNDLE_RESOURCE_LEAKAGE" => {
256            "Shipping secrets, certificates, or provisioning artifacts in the final bundle is treated as a serious distribution and security issue.".to_string()
257        }
258        "RULE_ENTITLEMENTS_MISMATCH"
259        | "RULE_ENTITLEMENTS_PROVISIONING_MISMATCH"
260        | "RULE_EXTENSION_ENTITLEMENTS"
261        | "RULE_DEBUG_ENTITLEMENTS" => {
262            "Entitlements that do not match the signed capabilities or release profile frequently cause validation failures or manual rejection.".to_string()
263        }
264        "RULE_EMBEDDED_SIGNING_CONSISTENCY" => {
265            "Embedded code signed with a different identity or Team ID can fail notarization-style checks during App Store validation.".to_string()
266        }
267        "RULE_PRIVATE_API" => {
268            "Private API usage is one of the clearest App Store rejection reasons because it relies on unsupported system behavior.".to_string()
269        }
270        _ => format!(
271            "This finding maps to the {} scope and signals metadata, signing, or bundle state that App Review may treat as invalid or risky.",
272            suggested_fix_scope(item)
273        ),
274    }
275}