| import json
|
| import logging
|
| from typing import Optional
|
| from ..models.service import AIBOMService
|
| from ..models.scoring import calculate_completeness_score
|
| from ..models.scoring import calculate_completeness_score
|
| from ..config import OUTPUT_DIR, TEMPLATES_DIR
|
| from ..utils.formatter import export_aibom
|
| import os
|
| import shutil
|
|
|
| logger = logging.getLogger(__name__)
|
|
|
| class CLIController:
|
| def __init__(self):
|
| self.service = AIBOMService()
|
|
|
| def _validate_spdx_schema_version(self, aibom_data: dict, spec_version: str):
|
| """
|
| TODO: Implement SPDX schema validation.
|
| """
|
| pass
|
|
|
| def generate(self, model_id: str, output_file: Optional[str] = None, include_inference: bool = False,
|
| enable_summarization: bool = False, verbose: bool = False,
|
| name: Optional[str] = None, version: Optional[str] = None, manufacturer: Optional[str] = None):
|
| if verbose:
|
| logging.getLogger().setLevel(logging.INFO)
|
|
|
| print(f"Generating AIBOM for {model_id}...")
|
|
|
| versions_to_generate = ["1.6", "1.7"]
|
| reports = []
|
| generated_aiboms = {}
|
|
|
| print(f" - Generating AIBOM model data...")
|
| try:
|
| primary_aibom = self.service.generate_aibom(
|
| model_id,
|
| include_inference=include_inference,
|
| enable_summarization=enable_summarization,
|
| metadata_overrides={
|
| "name": name,
|
| "version": version,
|
| "manufacturer": manufacturer
|
| }
|
| )
|
| primary_report = self.service.get_enhancement_report()
|
|
|
|
|
| json_1_6 = export_aibom(primary_aibom, bom_type="cyclonedx", spec_version="1.6")
|
| json_1_7 = export_aibom(primary_aibom, bom_type="cyclonedx", spec_version="1.7")
|
|
|
|
|
| normalized_id = self.service._normalise_model_id(model_id)
|
| os.makedirs("sboms", exist_ok=True)
|
|
|
| output_file_1_6 = output_file
|
| if not output_file_1_6:
|
| output_file_1_6 = os.path.join("sboms", f"{normalized_id.replace('/', '_')}_ai_sbom_1_6.json")
|
|
|
| base, ext = os.path.splitext(output_file_1_6)
|
| output_file_1_7 = f"{base.replace('_1_6', '')}_1_7{ext}" if '_1_6' in base else f"{base}_1_7{ext}"
|
|
|
| with open(output_file_1_6, 'w') as f:
|
| f.write(json_1_6)
|
| with open(output_file_1_7, 'w') as f:
|
| f.write(json_1_7)
|
|
|
|
|
| validation_data = primary_report.get("final_score", {}).get("validation", {})
|
| is_valid = validation_data.get("valid", True)
|
| validation_errors = [i["message"] for i in validation_data.get("issues", [])]
|
|
|
| if "schema_validation" not in primary_report:
|
| primary_report["schema_validation"] = {}
|
| primary_report["schema_validation"]["valid"] = is_valid
|
| primary_report["schema_validation"]["errors"] = validation_errors
|
| primary_report["schema_validation"]["error_count"] = len(validation_errors)
|
|
|
| reports = [
|
| {"spec_version": "1.6", "output_file": output_file_1_6, "schema_validation": primary_report["schema_validation"]},
|
| {"spec_version": "1.7", "output_file": output_file_1_7, "schema_validation": primary_report["schema_validation"]}
|
| ]
|
| output_file_primary = output_file_1_6
|
|
|
| except Exception as e:
|
| logger.error(f"Failed to generate SBOM: {e}", exc_info=True)
|
| print(f" โ Failed to generate SBOM: {e}")
|
| reports = []
|
|
|
| if reports:
|
| if output_file_primary:
|
| try:
|
| from jinja2 import Environment, FileSystemLoader, select_autoescape
|
| from ..config import TEMPLATES_DIR
|
|
|
| env = Environment(
|
| loader=FileSystemLoader(TEMPLATES_DIR),
|
| autoescape=select_autoescape(['html', 'xml'])
|
| )
|
| template = env.get_template("result.html")
|
|
|
| completeness_score = primary_report.get("final_score")
|
| if not completeness_score:
|
| completeness_score = calculate_completeness_score(primary_aibom)
|
|
|
|
|
| components_json = json.dumps(primary_aibom.get("components", []), indent=2)
|
|
|
| context = {
|
| "request": None,
|
| "filename": os.path.basename(output_file_primary),
|
| "download_url": "#",
|
| "aibom": primary_aibom,
|
| "components_json": components_json,
|
| "aibom_cdx_json_1_6": json_1_6,
|
| "aibom_cdx_json_1_7": json_1_7,
|
| "raw_aibom": primary_aibom,
|
| "model_id": self.service._normalise_model_id(model_id),
|
| "sbom_count": 0,
|
| "completeness_score": completeness_score,
|
| "enhancement_report": primary_report or {},
|
| "result_file": "#",
|
| "static_root": "static"
|
| }
|
|
|
| html_content = template.render(context)
|
| html_output_file = output_file_primary.replace("_1_6.json", ".html").replace(".json", ".html")
|
| with open(html_output_file, "w") as f:
|
| f.write(html_content)
|
|
|
| print(f"\n๐ HTML Report:\n {html_output_file}")
|
|
|
|
|
| try:
|
|
|
|
|
| output_dir = os.path.dirname(html_output_file)
|
|
|
|
|
| current_dir = os.path.dirname(os.path.abspath(__file__))
|
| src_dir = os.path.dirname(current_dir)
|
| static_src = os.path.join(src_dir, "static")
|
| static_dst = os.path.join(output_dir, "static")
|
|
|
| if os.path.exists(static_src):
|
| if os.path.exists(static_dst):
|
| shutil.rmtree(static_dst)
|
| shutil.copytree(static_src, static_dst)
|
|
|
| else:
|
| logger.warning(f"Static source directory not found: {static_src}")
|
|
|
| except Exception as e:
|
| logger.warning(f"Failed to copy static assets: {e}")
|
|
|
|
|
| if "components" in primary_aibom and primary_aibom["components"]:
|
| description = primary_aibom["components"][0].get("description", "No description available")
|
| if len(description) > 256:
|
| description = description[:253] + "..."
|
| print(f"\n๐ Model Description:\n {description}")
|
|
|
|
|
| if "components" in primary_aibom and primary_aibom["components"]:
|
| comp = primary_aibom["components"][0]
|
| if "licenses" in comp:
|
| license_list = []
|
| for l in comp["licenses"]:
|
| lic = l.get("license", {})
|
| val = lic.get("id") or lic.get("name")
|
| if val:
|
| license_list.append(val)
|
| if license_list:
|
| print(f"\nโ๏ธ License:\n {', '.join(license_list)}")
|
|
|
| except Exception as e:
|
| logger.warning(f"Failed to generate HTML report: {e}")
|
|
|
|
|
| for r in reports:
|
| spec = r.get("spec_version", "1.6")
|
| print(f"\nโ
Successfully generated CycloneDX {spec} SBOM:")
|
| print(f" {r.get('output_file')}")
|
|
|
| if not r["schema_validation"]["valid"]:
|
| print(f"โ ๏ธ Schema Validation Errors ({spec}):")
|
| for err in r["schema_validation"]["errors"]:
|
| print(f" - {err}")
|
| else:
|
| print(f" - Schema Validation ({spec}): โ
Valid")
|
|
|
|
|
| if primary_report and "final_score" in primary_report:
|
| score = primary_report["final_score"]
|
| t_score = score.get('total_score', 0)
|
| formatted_t_score = int(t_score) if isinstance(t_score, (int, float)) and t_score == int(t_score) else t_score
|
| print(f"\n๐ Completeness Score: {formatted_t_score}/100")
|
|
|
| if "completeness_profile" in score:
|
| profile = score["completeness_profile"]
|
| print(f" Profile: {profile.get('name')} - {profile.get('description')}")
|
|
|
| if "section_scores" in score:
|
| print("\n๐ Section Breakdown:")
|
|
|
| for section, s_score in score["section_scores"].items():
|
| max_s = score.get("max_scores", {}).get(section, "?")
|
| formatted_s_score = int(s_score) if isinstance(s_score, (int, float)) and s_score == int(s_score) else s_score
|
| print(f" - {section.replace('_', ' ').title()}: {formatted_s_score}/{max_s}")
|
|
|
| else:
|
| print("\nโ Failed to generate any SBOMs.")
|
|
|