(self, host: Optional[str] = None)
| 129 | return {"title": "Unmapped finding (informational)", "controls": ["Unmapped (informational)"]} |
| 130 | |
| 131 | def build_cis_report(self, host: Optional[str] = None) -> Dict[str, Any]: |
| 132 | lynis = self._collect_lynis(host) |
| 133 | controls: Dict[str, Dict[str, Any]] = {} |
| 134 | |
| 135 | def bucket(entry: Dict[str, str], rec_host: str, kind: str): |
| 136 | code = (entry.get("code") or "").strip() |
| 137 | mapping = self._map_lynis_code(code) |
| 138 | for control in mapping["controls"]: |
| 139 | slot = controls.setdefault(control, { |
| 140 | "control": control, |
| 141 | "title": mapping["title"], |
| 142 | "warnings": 0, |
| 143 | "suggestions": 0, |
| 144 | "findings": [], |
| 145 | }) |
| 146 | if kind == "warning": |
| 147 | slot["warnings"] += 1 |
| 148 | else: |
| 149 | slot["suggestions"] += 1 |
| 150 | slot["findings"].append({ |
| 151 | "host": rec_host, |
| 152 | "code": code or "—", |
| 153 | "kind": kind, |
| 154 | "message": entry.get("message", ""), |
| 155 | "remediation": entry.get("remediation", ""), |
| 156 | "severity": "high" if kind == "warning" else "medium", |
| 157 | }) |
| 158 | |
| 159 | for rec in lynis: |
| 160 | for warning in rec["warnings"]: |
| 161 | bucket(warning, rec["host"], "warning") |
| 162 | for suggestion in rec["suggestions"]: |
| 163 | bucket(suggestion, rec["host"], "suggestion") |
| 164 | |
| 165 | control_rows = [] |
| 166 | for slot in controls.values(): |
| 167 | if slot["warnings"] > 0: |
| 168 | slot["status"] = "attention" |
| 169 | elif slot["suggestions"] > 0: |
| 170 | slot["status"] = "review" |
| 171 | else: |
| 172 | slot["status"] = "ok" |
| 173 | control_rows.append(slot) |
| 174 | control_rows.sort(key=lambda s: (s["status"] != "attention", s["control"])) |
| 175 | |
| 176 | indices = [float(r["hardening_index"]) for r in lynis |
| 177 | if str(r.get("hardening_index") or "").replace(".", "", 1).isdigit()] |
| 178 | summary = { |
| 179 | "hosts_assessed": len(lynis), |
| 180 | "controls_flagged": sum(1 for r in control_rows if r["status"] == "attention"), |
| 181 | "controls_review": sum(1 for r in control_rows if r["status"] == "review"), |
| 182 | "total_warnings": sum(r["warnings"] for r in control_rows), |
| 183 | "total_suggestions": sum(r["suggestions"] for r in control_rows), |
| 184 | "avg_hardening_index": round(sum(indices) / len(indices), 1) if indices else None, |
| 185 | "hosts": [{"host": r["host"], "hardening_index": r["hardening_index"], "scan_date": r["scan_date"]} |
| 186 | for r in lynis], |
| 187 | } |
| 188 | return { |
no test coverage detected