|
10 | 10 | from pathlib import Path |
11 | 11 | from typing import Any |
12 | 12 |
|
| 13 | +PARTITION_KEYS = { |
| 14 | + "system", |
| 15 | + "product", |
| 16 | + "system_ext", |
| 17 | + "vendor", |
| 18 | + "odm", |
| 19 | + "mi_ext", |
| 20 | + "vendor_dlkm", |
| 21 | + "vendor_boot", |
| 22 | + "boot", |
| 23 | +} |
| 24 | + |
| 25 | +CRITICAL_PATH_MARKERS = ( |
| 26 | + "/etc/selinux/", |
| 27 | + "/sepolicy/", |
| 28 | + "build.prop", |
| 29 | + "file_contexts", |
| 30 | + "fs_config", |
| 31 | + "/etc/init/", |
| 32 | + "init.rc", |
| 33 | + "/framework/", |
| 34 | + "/priv-app/", |
| 35 | +) |
| 36 | + |
13 | 37 |
|
14 | 38 | def _sha256(path: Path) -> str: |
15 | 39 | digest = hashlib.sha256() |
@@ -126,6 +150,102 @@ def _diff_map(before: dict[str, Any], after: dict[str, Any]) -> dict[str, list[s |
126 | 150 | } |
127 | 151 |
|
128 | 152 |
|
| 153 | +def _partition_for_path(path: str) -> str: |
| 154 | + first = path.split("/", 1)[0] |
| 155 | + if first in PARTITION_KEYS: |
| 156 | + return first |
| 157 | + return "_root" |
| 158 | + |
| 159 | + |
| 160 | +def _group_paths_by_partition(paths: list[str]) -> dict[str, list[str]]: |
| 161 | + grouped: dict[str, list[str]] = {} |
| 162 | + for path in paths: |
| 163 | + partition = _partition_for_path(path) |
| 164 | + grouped.setdefault(partition, []).append(path) |
| 165 | + for partition, entries in grouped.items(): |
| 166 | + grouped[partition] = sorted(entries) |
| 167 | + return dict(sorted(grouped.items(), key=lambda item: item[0])) |
| 168 | + |
| 169 | + |
| 170 | +def _collect_critical_path_changes( |
| 171 | + file_changes: dict[str, list[str]], apk_changes: list[dict[str, Any]] |
| 172 | +) -> list[str]: |
| 173 | + candidates = file_changes["added"] + file_changes["removed"] + file_changes["modified"] |
| 174 | + candidates.extend(change["path"] for change in apk_changes) |
| 175 | + critical = [] |
| 176 | + for path in candidates: |
| 177 | + if any(marker in path for marker in CRITICAL_PATH_MARKERS): |
| 178 | + critical.append(path) |
| 179 | + return sorted(set(critical)) |
| 180 | + |
| 181 | + |
| 182 | +def _build_risk_flags( |
| 183 | + file_changes: dict[str, list[str]], |
| 184 | + prop_changes: list[dict[str, Any]], |
| 185 | + apk_changes: list[dict[str, Any]], |
| 186 | + critical_paths: list[str], |
| 187 | +) -> list[dict[str, Any]]: |
| 188 | + flags: list[dict[str, Any]] = [] |
| 189 | + |
| 190 | + if critical_paths: |
| 191 | + flags.append( |
| 192 | + { |
| 193 | + "code": "HIGH_IMPACT_PATH_CHANGED", |
| 194 | + "message": "Critical system/security paths changed.", |
| 195 | + "paths": critical_paths, |
| 196 | + } |
| 197 | + ) |
| 198 | + |
| 199 | + identity_prop_paths = sorted( |
| 200 | + { |
| 201 | + item["path"] |
| 202 | + for item in prop_changes |
| 203 | + if item["key"].startswith("ro.product.") |
| 204 | + or item["key"].startswith("ro.build.fingerprint") |
| 205 | + } |
| 206 | + ) |
| 207 | + if identity_prop_paths: |
| 208 | + flags.append( |
| 209 | + { |
| 210 | + "code": "IDENTITY_PROP_CHANGED", |
| 211 | + "message": "Device identity properties changed.", |
| 212 | + "paths": identity_prop_paths, |
| 213 | + } |
| 214 | + ) |
| 215 | + |
| 216 | + priv_app_paths = sorted( |
| 217 | + { |
| 218 | + change["path"] |
| 219 | + for change in apk_changes |
| 220 | + if "/priv-app/" in change["path"] or change["path"].startswith("priv-app/") |
| 221 | + } |
| 222 | + ) |
| 223 | + if priv_app_paths: |
| 224 | + flags.append( |
| 225 | + { |
| 226 | + "code": "PRIV_APP_CHANGED", |
| 227 | + "message": "Privileged APK content changed.", |
| 228 | + "paths": priv_app_paths, |
| 229 | + } |
| 230 | + ) |
| 231 | + |
| 232 | + init_related = sorted( |
| 233 | + path |
| 234 | + for path in (file_changes["added"] + file_changes["removed"] + file_changes["modified"]) |
| 235 | + if "/etc/init/" in path or path.endswith("init.rc") |
| 236 | + ) |
| 237 | + if init_related: |
| 238 | + flags.append( |
| 239 | + { |
| 240 | + "code": "INIT_SCRIPT_CHANGED", |
| 241 | + "message": "Init scripts changed and may affect boot.", |
| 242 | + "paths": init_related, |
| 243 | + } |
| 244 | + ) |
| 245 | + |
| 246 | + return flags |
| 247 | + |
| 248 | + |
129 | 249 | def generate_diff_report(before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any]: |
130 | 250 | """Generate a structured artifact diff report.""" |
131 | 251 | file_scope = _diff_map(before.get("files", {}), after.get("files", {})) |
@@ -169,25 +289,39 @@ def generate_diff_report(before: dict[str, Any], after: dict[str, Any]) -> dict[ |
169 | 289 | } |
170 | 290 | ) |
171 | 291 |
|
| 292 | + file_changes = { |
| 293 | + "added": file_scope["added"], |
| 294 | + "removed": file_scope["removed"], |
| 295 | + "modified": modified_files, |
| 296 | + } |
| 297 | + critical_paths = _collect_critical_path_changes(file_changes, apk_changes) |
| 298 | + risk_flags = _build_risk_flags(file_changes, prop_changes, apk_changes, critical_paths) |
| 299 | + |
172 | 300 | return { |
173 | 301 | "summary": { |
174 | 302 | "files_added": len(file_scope["added"]), |
175 | 303 | "files_removed": len(file_scope["removed"]), |
176 | 304 | "files_modified": len(modified_files), |
177 | 305 | "prop_changes": len(prop_changes), |
178 | 306 | "apk_changes": len(apk_changes), |
| 307 | + "risk_flags": len(risk_flags), |
179 | 308 | }, |
180 | | - "files": { |
181 | | - "added": file_scope["added"], |
182 | | - "removed": file_scope["removed"], |
183 | | - "modified": modified_files, |
184 | | - }, |
| 309 | + "files": file_changes, |
185 | 310 | "build_props": { |
186 | 311 | "added_files": prop_scope["added"], |
187 | 312 | "removed_files": prop_scope["removed"], |
188 | 313 | "changes": prop_changes, |
189 | 314 | }, |
190 | 315 | "apks": apk_changes, |
| 316 | + "partition_groups": { |
| 317 | + "added": _group_paths_by_partition(file_scope["added"]), |
| 318 | + "removed": _group_paths_by_partition(file_scope["removed"]), |
| 319 | + "modified": _group_paths_by_partition(modified_files), |
| 320 | + }, |
| 321 | + "highlights": { |
| 322 | + "critical_path_changes": critical_paths, |
| 323 | + "risk_flags": risk_flags, |
| 324 | + }, |
191 | 325 | } |
192 | 326 |
|
193 | 327 |
|
|
0 commit comments