Spaces:
Running
Running
Initial commit after cleanup
Browse files- Dockerfile +39 -0
- README.md +6 -9
- WELCOME_README.md +39 -0
- docs/AI_SBOM_API_doc.md +296 -0
- entrypoint.sh +38 -0
- requirements.txt +15 -0
- setup.py +16 -0
- src/aibom-generator/__init__.py +8 -0
- src/aibom-generator/aibom_score_report.py +68 -0
- src/aibom-generator/aibom_security.py +256 -0
- src/aibom-generator/api.py +1204 -0
- src/aibom-generator/auth.py +23 -0
- src/aibom-generator/captcha.py +55 -0
- src/aibom-generator/cleanup_utils.py +74 -0
- src/aibom-generator/cli.py +193 -0
- src/aibom-generator/generator.py +611 -0
- src/aibom-generator/improved_score_renderer.py +55 -0
- src/aibom-generator/rate_limiting.py +114 -0
- src/aibom-generator/utils.py +1307 -0
- templates/error.html +216 -0
- templates/improved_scoring_template.html +262 -0
- templates/index.html +270 -0
- templates/result.html +1275 -0
Dockerfile
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10-slim
|
2 |
+
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
# This ensures models are cached properly and don’t re-download every time
|
6 |
+
RUN mkdir /.cache && chmod 777 /.cache
|
7 |
+
ENV TRANSFORMERS_CACHE=/.cache
|
8 |
+
ENV HF_HOME=/.cache
|
9 |
+
# Optional: Set default values for API configuration
|
10 |
+
# ENV AIBOM_USE_INFERENCE=true
|
11 |
+
# ENV AIBOM_CACHE_DIR=/.cache
|
12 |
+
|
13 |
+
# Install dependencies
|
14 |
+
COPY requirements.txt .
|
15 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
16 |
+
|
17 |
+
# Copy all application files (including setup.py)
|
18 |
+
COPY . /app
|
19 |
+
|
20 |
+
# Safety check to ensure correct directory naming
|
21 |
+
RUN if [ -d "/app/src/aibom-generator" ] && [ ! -d "/app/src/aibom_generator" ]; then \
|
22 |
+
mv /app/src/aibom-generator /app/src/aibom_generator; \
|
23 |
+
echo "Renamed directory to match Python import conventions"; \
|
24 |
+
fi
|
25 |
+
|
26 |
+
# Creates a directory called "output" inside application directory, sets permissions so that the application can write files to this directory
|
27 |
+
# RUN mkdir -p /app/output && chmod 777 /app/output
|
28 |
+
|
29 |
+
# Install the package in development mode
|
30 |
+
RUN pip install -e .
|
31 |
+
|
32 |
+
# Set environment variables
|
33 |
+
ENV PYTHONPATH=/app
|
34 |
+
|
35 |
+
# Create entrypoint script
|
36 |
+
RUN chmod +x /app/entrypoint.sh
|
37 |
+
|
38 |
+
# Command to run the application
|
39 |
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
README.md
CHANGED
@@ -1,13 +1,10 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
colorFrom: purple
|
5 |
-
colorTo:
|
6 |
-
sdk:
|
7 |
-
sdk_version: 5.32.0
|
8 |
-
app_file: app.py
|
9 |
pinned: false
|
10 |
license: mit
|
11 |
-
|
12 |
-
|
13 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
---
|
2 |
+
title: Aetheris AI - AI SBOM Generator
|
3 |
+
emoji: 🚀
|
4 |
colorFrom: purple
|
5 |
+
colorTo: blue
|
6 |
+
sdk: docker
|
|
|
|
|
7 |
pinned: false
|
8 |
license: mit
|
9 |
+
short_description: AI SBOM (AIBOM) Generation
|
10 |
+
---
|
|
WELCOME_README.md
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 🤖 AI SBOM Generator
|
2 |
+
|
3 |
+
This is the official Hugging Face Space repository for the **AI SBOM Generator** — an open-source tool for generating AI Software Bills of Materials (AI SBOMs) in [CycloneDX](https://cyclonedx.org) format.
|
4 |
+
Official GitHub reporitory is here: [github.com/aetheris-ai/aibom-generator]](https://github.com/aetheris-ai/aibom-generator/)
|
5 |
+
|
6 |
+
🚀 **Try the tool live:**
|
7 |
+
👉 [huggingface.co/spaces/aetheris-ai/aibom-generator](https://huggingface.co/spaces/aetheris-ai/aibom-generator)
|
8 |
+
|
9 |
+
---
|
10 |
+
|
11 |
+
## 📦 What It Does
|
12 |
+
|
13 |
+
- Extracts metadata from models hosted on Hugging Face 🤗
|
14 |
+
- Generates an AI SBOM in JSON format based on CycloneDX 1.6
|
15 |
+
- Assesses metadata completeness and provides improvement tips
|
16 |
+
- Supports model cards, training data, evaluation, and usage metadata
|
17 |
+
|
18 |
+
---
|
19 |
+
|
20 |
+
## 🛠 Features
|
21 |
+
|
22 |
+
- Human-readable SBOM view
|
23 |
+
- JSON download
|
24 |
+
- Completeness scoring and recommendations
|
25 |
+
- AI-assisted enhancements (optional)
|
26 |
+
|
27 |
+
---
|
28 |
+
|
29 |
+
## 🐞 Found a Bug or Have an Improvement Rerquest?
|
30 |
+
|
31 |
+
Please help us improve!
|
32 |
+
|
33 |
+
➡ [Log an issue on GitHub](https://github.com/aetheris-ai/aibom-generator/issues)
|
34 |
+
|
35 |
+
---
|
36 |
+
|
37 |
+
## 📄 License
|
38 |
+
|
39 |
+
This project is open-source and available under the [MIT License](LICENSE).
|
docs/AI_SBOM_API_doc.md
ADDED
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# AI SBOM Generator API Documentation
|
2 |
+
|
3 |
+
## Overview
|
4 |
+
|
5 |
+
The AI SBOM Generator API provides a comprehensive solution for generating CycloneDX-compliant AI Bill of Materials (AI SBOM) for Hugging Face models. This document outlines the available API endpoints, their functionality, and how to interact with them using cURL commands.
|
6 |
+
|
7 |
+
## Base URL
|
8 |
+
|
9 |
+
When deployed on Hugging Face Spaces, the base URL will be:
|
10 |
+
```
|
11 |
+
https://aetheris-ai-aibom-generator.hf.space
|
12 |
+
```
|
13 |
+
|
14 |
+
Replace this with your actual deployment URL.
|
15 |
+
|
16 |
+
## API Endpoints
|
17 |
+
|
18 |
+
### Status Endpoint
|
19 |
+
|
20 |
+
**Purpose**: Check if the API is operational and get version information.
|
21 |
+
|
22 |
+
**Endpoint**: `/status`
|
23 |
+
|
24 |
+
**Method**: GET
|
25 |
+
|
26 |
+
**cURL Example**:
|
27 |
+
```bash
|
28 |
+
curl -X GET "https://aetheris-ai-aibom-generator.hf.space/status"
|
29 |
+
```
|
30 |
+
|
31 |
+
**Expected Response**:
|
32 |
+
```json
|
33 |
+
{
|
34 |
+
"status": "operational",
|
35 |
+
"version": "1.0.0",
|
36 |
+
"generator_version": "1.0.0"
|
37 |
+
}
|
38 |
+
```
|
39 |
+
|
40 |
+
### Generate AI SBOM Endpoint
|
41 |
+
|
42 |
+
**Purpose**: Generate an AI SBOM for a specified Hugging Face model.
|
43 |
+
|
44 |
+
**Endpoint**: `/api/generate`
|
45 |
+
|
46 |
+
**Method**: POST
|
47 |
+
|
48 |
+
**Parameters**:
|
49 |
+
- `model_id` (required): The Hugging Face model ID (e.g., 'meta-llama/Llama-2-7b-chat-hf')
|
50 |
+
- `include_inference` (optional): Whether to use AI inference to enhance the AI SBOM (default: true)
|
51 |
+
- `use_best_practices` (optional): Whether to use industry best practices for scoring (default: true)
|
52 |
+
- `hf_token` (optional): Hugging Face API token for accessing private models
|
53 |
+
|
54 |
+
**cURL Example**:
|
55 |
+
```bash
|
56 |
+
curl -X POST "https://aetheris-ai-aibom-generator.hf.space/api/generate" \
|
57 |
+
-H "Content-Type: application/json" \
|
58 |
+
-d '{
|
59 |
+
"model_id": "meta-llama/Llama-2-7b-chat-hf",
|
60 |
+
"include_inference": true,
|
61 |
+
"use_best_practices": true
|
62 |
+
}'
|
63 |
+
```
|
64 |
+
|
65 |
+
**Expected Response**: JSON containing the generated AI SBOM, model ID, timestamp, and download URL.
|
66 |
+
```json
|
67 |
+
{
|
68 |
+
"aibom": {
|
69 |
+
"bomFormat": "CycloneDX",
|
70 |
+
"specVersion": "1.6",
|
71 |
+
"serialNumber": "urn:uuid:...",
|
72 |
+
"version": 1,
|
73 |
+
"metadata": { ... },
|
74 |
+
"components": [ ... ],
|
75 |
+
"dependencies": [ ... ]
|
76 |
+
},
|
77 |
+
"model_id": "meta-llama/Llama-2-7b-chat-hf",
|
78 |
+
"generated_at": "2025-04-24T20:30:00Z",
|
79 |
+
"request_id": "...",
|
80 |
+
"download_url": "/output/meta-llama_Llama-2-7b-chat-hf_....json"
|
81 |
+
}
|
82 |
+
```
|
83 |
+
|
84 |
+
### Generate AI SBOM with Enhancement Report
|
85 |
+
|
86 |
+
**Purpose**: Generate an AI SBOM with a detailed enhancement report.
|
87 |
+
|
88 |
+
**Endpoint**: `/api/generate-with-report`
|
89 |
+
|
90 |
+
**Method**: POST
|
91 |
+
|
92 |
+
**Parameters**: Same as `/api/generate`
|
93 |
+
|
94 |
+
**cURL Example**:
|
95 |
+
```bash
|
96 |
+
curl -X POST "https://aetheris-ai-aibom-generator.hf.space/api/generate-with-report" \
|
97 |
+
-H "Content-Type: application/json" \
|
98 |
+
-d '{
|
99 |
+
"model_id": "meta-llama/Llama-2-7b-chat-hf",
|
100 |
+
"include_inference": true,
|
101 |
+
"use_best_practices": true
|
102 |
+
}'
|
103 |
+
```
|
104 |
+
|
105 |
+
**Expected Response**: JSON containing the generated AI SBOM, model ID, timestamp, download URL, and enhancement report.
|
106 |
+
```json
|
107 |
+
{
|
108 |
+
"aibom": { ... },
|
109 |
+
"model_id": "meta-llama/Llama-2-7b-chat-hf",
|
110 |
+
"generated_at": "2025-04-24T20:30:00Z",
|
111 |
+
"request_id": "...",
|
112 |
+
"download_url": "/output/meta-llama_Llama-2-7b-chat-hf_....json",
|
113 |
+
"enhancement_report": {
|
114 |
+
"ai_enhanced": true,
|
115 |
+
"ai_model": "BERT-base-uncased",
|
116 |
+
"original_score": {
|
117 |
+
"total_score": 65.5,
|
118 |
+
"completeness_score": 65.5
|
119 |
+
},
|
120 |
+
"final_score": {
|
121 |
+
"total_score": 85.2,
|
122 |
+
"completeness_score": 85.2
|
123 |
+
},
|
124 |
+
"improvement": 19.7
|
125 |
+
}
|
126 |
+
}
|
127 |
+
```
|
128 |
+
|
129 |
+
### Get Model Score
|
130 |
+
|
131 |
+
**Purpose**: Get the completeness score for a model without generating a full AI SBOM.
|
132 |
+
|
133 |
+
**Endpoint**: `/api/models/{model_id}/score`
|
134 |
+
|
135 |
+
**Method**: GET
|
136 |
+
|
137 |
+
**Parameters**:
|
138 |
+
- `model_id` (path parameter): The Hugging Face model ID
|
139 |
+
- `hf_token` (query parameter, optional): Hugging Face API token for accessing private models
|
140 |
+
- `use_best_practices` (query parameter, optional): Whether to use industry best practices for scoring (default: true)
|
141 |
+
|
142 |
+
**cURL Example**:
|
143 |
+
```bash
|
144 |
+
curl -X GET "https://aetheris-ai-aibom-generator.hf.space/api/models/meta-llama/Llama-2-7b-chat-hf/score?use_best_practices=true"
|
145 |
+
```
|
146 |
+
|
147 |
+
**Expected Response**: JSON containing the completeness score information.
|
148 |
+
```json
|
149 |
+
{
|
150 |
+
"total_score": 85.2,
|
151 |
+
"section_scores": {
|
152 |
+
"required_fields": 20,
|
153 |
+
"metadata": 18.5,
|
154 |
+
"component_basic": 20,
|
155 |
+
"component_model_card": 20.7,
|
156 |
+
"external_references": 6
|
157 |
+
},
|
158 |
+
"max_scores": {
|
159 |
+
"required_fields": 20,
|
160 |
+
"metadata": 20,
|
161 |
+
"component_basic": 20,
|
162 |
+
"component_model_card": 30,
|
163 |
+
"external_references": 10
|
164 |
+
}
|
165 |
+
}
|
166 |
+
```
|
167 |
+
|
168 |
+
### Download Generated AI SBOM
|
169 |
+
|
170 |
+
**Purpose**: Download a previously generated AI SBOM file.
|
171 |
+
|
172 |
+
**Endpoint**: `/download/{filename}`
|
173 |
+
|
174 |
+
**Method**: GET
|
175 |
+
|
176 |
+
**Parameters**:
|
177 |
+
- `filename` (path parameter): The filename of the AI SBOM to download
|
178 |
+
|
179 |
+
**cURL Example**:
|
180 |
+
```bash
|
181 |
+
curl -X GET "https://aetheris-ai-aibom-generator.hf.space/download/{filename}" \
|
182 |
+
-o "downloaded_aibom.json"
|
183 |
+
```
|
184 |
+
|
185 |
+
**Expected Response**: The AI SBOM JSON file will be downloaded to your local machine.
|
186 |
+
|
187 |
+
### Form-Based Generation (Web UI)
|
188 |
+
|
189 |
+
**Purpose**: Generate an AI SBOM using form data (typically used by the web UI).
|
190 |
+
|
191 |
+
**Endpoint**: `/generate`
|
192 |
+
|
193 |
+
**Method**: POST
|
194 |
+
|
195 |
+
**Parameters**:
|
196 |
+
- `model_id` (form field, required): The Hugging Face model ID
|
197 |
+
- `include_inference` (form field, optional): Whether to use AI inference to enhance the AI SBOM
|
198 |
+
- `use_best_practices` (form field, optional): Whether to use industry best practices for scoring
|
199 |
+
|
200 |
+
**cURL Example**:
|
201 |
+
```bash
|
202 |
+
curl -X POST "https://aetheris-ai-aibom-generator.hf.space/generate" \
|
203 |
+
-F "model_id=meta-llama/Llama-2-7b-chat-hf" \
|
204 |
+
-F "include_inference=true" \
|
205 |
+
-F "use_best_practices=true"
|
206 |
+
```
|
207 |
+
|
208 |
+
**Expected Response**: HTML page with the generated AI SBOM results.
|
209 |
+
|
210 |
+
## Web UI
|
211 |
+
|
212 |
+
The API also provides a web user interface for generating AI SBOMs without writing code:
|
213 |
+
|
214 |
+
**URL**: `https://aetheris-ai-aibom-generator.hf.space/`
|
215 |
+
|
216 |
+
The web UI allows you to:
|
217 |
+
1. Enter a Hugging Face model ID
|
218 |
+
2. Configure generation options
|
219 |
+
3. Generate an AI SBOM
|
220 |
+
4. View the results in a human-friendly format
|
221 |
+
5. Download the generated AI SBOM as a JSON file
|
222 |
+
|
223 |
+
## Understanding the Field Checklist
|
224 |
+
|
225 |
+
In the Field Checklist tab of the results page, you'll see a list of fields with check marks (✔/✘) and stars (★). Here's what they mean:
|
226 |
+
|
227 |
+
- **Check marks**:
|
228 |
+
- ✔: Field is present in the AI SBOM
|
229 |
+
- ✘: Field is missing from the AI SBOM
|
230 |
+
|
231 |
+
- **Stars** (importance level):
|
232 |
+
- ★★★ (three stars): Critical fields - Essential for a valid and complete AI SBOM
|
233 |
+
- ★★ (two stars): Important fields - Valuable information that enhances completeness
|
234 |
+
- ★ (one star): Supplementary fields - Additional context and details (optional)
|
235 |
+
|
236 |
+
## Security Features
|
237 |
+
|
238 |
+
The API includes several security features to protect against Denial of Service (DoS) attacks:
|
239 |
+
|
240 |
+
1. **Rate Limiting**: Limits the number of requests a single IP address can make within a specific time window.
|
241 |
+
|
242 |
+
2. **Concurrency Limiting**: Restricts the total number of simultaneous requests being processed to prevent resource exhaustion.
|
243 |
+
|
244 |
+
3. **Request Size Limiting**: Prevents attackers from sending extremely large payloads that could consume memory or processing resources.
|
245 |
+
|
246 |
+
4. **API Key Authentication** (optional): When configured, requires an API key for accessing API endpoints, enabling tracking and control of API usage.
|
247 |
+
|
248 |
+
5. **CAPTCHA Verification** (optional): When configured for the web interface, helps ensure requests come from humans rather than bots.
|
249 |
+
|
250 |
+
## Notes on Using the API
|
251 |
+
|
252 |
+
1. When deployed on Hugging Face Spaces, use the correct URL format as shown in the examples.
|
253 |
+
2. Some endpoints may have rate limiting or require authentication.
|
254 |
+
3. For large responses, consider adding appropriate timeout settings in your requests.
|
255 |
+
4. If you encounter CORS issues, you may need to add appropriate headers.
|
256 |
+
5. For downloading files, specify the output file name in your client code.
|
257 |
+
|
258 |
+
## Error Handling
|
259 |
+
|
260 |
+
The API returns standard HTTP status codes:
|
261 |
+
- 200: Success
|
262 |
+
- 400: Bad Request (invalid parameters)
|
263 |
+
- 404: Not Found (resource not found)
|
264 |
+
- 429: Too Many Requests (rate limit exceeded)
|
265 |
+
- 500: Internal Server Error (server-side error)
|
266 |
+
- 503: Service Unavailable (server at capacity)
|
267 |
+
|
268 |
+
Error responses include a detail message explaining the error:
|
269 |
+
```json
|
270 |
+
{
|
271 |
+
"detail": "Error generating AI SBOM: Model not found"
|
272 |
+
}
|
273 |
+
```
|
274 |
+
|
275 |
+
## Completeness Score
|
276 |
+
|
277 |
+
The completeness score is calculated based on the presence and quality of various fields in the AI SBOM. The score is broken down into sections:
|
278 |
+
|
279 |
+
1. **Required Fields** (20 points): Basic required fields for a valid AI SBOM
|
280 |
+
2. **Metadata** (20 points): Information about the AI SBOM itself
|
281 |
+
3. **Component Basic Info** (20 points): Basic information about the AI model component
|
282 |
+
4. **Model Card** (30 points): Detailed model card information
|
283 |
+
5. **External References** (10 points): Links to external resources
|
284 |
+
|
285 |
+
The total score is a weighted sum of these section scores, with a maximum of 100 points.
|
286 |
+
|
287 |
+
## Enhancement Report
|
288 |
+
|
289 |
+
When AI enhancement is enabled, the API uses an inference model to extract additional information from the model card and other sources. The enhancement report shows:
|
290 |
+
|
291 |
+
1. **Original Score**: The completeness score before enhancement
|
292 |
+
2. **Enhanced Score**: The completeness score after enhancement
|
293 |
+
3. **Improvement**: The point increase from enhancement
|
294 |
+
4. **AI Model Used**: The model used for enhancement
|
295 |
+
|
296 |
+
This helps you understand how much the AI enhancement improved the AI SBOM's completeness.
|
entrypoint.sh
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/bin/bash
|
2 |
+
set -e
|
3 |
+
|
4 |
+
# Default inference URL for internal inference model service
|
5 |
+
DEFAULT_INFERENCE_URL="http://localhost:8000/extract"
|
6 |
+
export AIBOM_INFERENCE_URL=${AIBOM_INFERENCE_URL:-$DEFAULT_INFERENCE_URL}
|
7 |
+
|
8 |
+
echo "Using AIBOM_INFERENCE_URL: $AIBOM_INFERENCE_URL"
|
9 |
+
|
10 |
+
# Check if command-line arguments are provided
|
11 |
+
if [ -n "$1" ]; then
|
12 |
+
case "$1" in
|
13 |
+
server)
|
14 |
+
# Start the API server explicitly (recommended for Hugging Face Spaces)
|
15 |
+
echo "Starting AIBOM Generator API server..."
|
16 |
+
exec uvicorn src.aibom_generator.api:app --host 0.0.0.0 --port ${PORT:-7860}
|
17 |
+
;;
|
18 |
+
worker)
|
19 |
+
# Start the background worker
|
20 |
+
echo "Starting AIBOM Generator background worker..."
|
21 |
+
exec python -m src.aibom_generator.worker
|
22 |
+
;;
|
23 |
+
inference)
|
24 |
+
# Start the inference model server
|
25 |
+
echo "Starting AIBOM Generator inference model server..."
|
26 |
+
exec python -m src.aibom_generator.inference_model --host 0.0.0.0 --port ${PORT:-8000}
|
27 |
+
;;
|
28 |
+
*)
|
29 |
+
# Run as CLI with provided arguments
|
30 |
+
echo "Running AIBOM Generator CLI..."
|
31 |
+
exec python -m src.aibom_generator.cli "$@"
|
32 |
+
;;
|
33 |
+
esac
|
34 |
+
else
|
35 |
+
# Default behavior (if no arguments): start API server (web UI mode)
|
36 |
+
echo "Starting AIBOM Generator API server (web UI)..."
|
37 |
+
exec uvicorn src.aibom_generator.api:app --host 0.0.0.0 --port ${PORT:-7860}
|
38 |
+
fi
|
requirements.txt
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
huggingface_hub>=0.19.0
|
2 |
+
transformers>=4.36.0
|
3 |
+
torch>=2.0.0
|
4 |
+
fastapi>=0.104.0
|
5 |
+
uvicorn>=0.24.0
|
6 |
+
pydantic>=2.4.0
|
7 |
+
requests>=2.31.0
|
8 |
+
python-dotenv>=1.0.0
|
9 |
+
PyYAML>=6.0.1
|
10 |
+
flask>=2.3.0
|
11 |
+
gunicorn>=21.2.0
|
12 |
+
cyclonedx-python-lib>=4.0.0
|
13 |
+
python-multipart
|
14 |
+
jinja2>=3.0.0
|
15 |
+
datasets>=2.0.0
|
setup.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from setuptools import setup, find_packages
|
2 |
+
|
3 |
+
setup(
|
4 |
+
name="aibom_generator",
|
5 |
+
version="1.0.0",
|
6 |
+
packages=find_packages(where="src"),
|
7 |
+
package_dir={"": "src"},
|
8 |
+
install_requires=[
|
9 |
+
"huggingface_hub",
|
10 |
+
"transformers",
|
11 |
+
"cyclonedx-python-lib",
|
12 |
+
"requests",
|
13 |
+
"pyyaml",
|
14 |
+
],
|
15 |
+
python_requires=">=3.8",
|
16 |
+
)
|
src/aibom-generator/__init__.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
AI SBOM Generator for Hugging Face Models.
|
3 |
+
|
4 |
+
This package provides tools to generate AI Software Bills of Materials (AI SBOMs) in CycloneDX format for AI models hosted on the Hugging Face.
|
5 |
+
"""
|
6 |
+
|
7 |
+
__version__ = "1.0.0"
|
8 |
+
|
src/aibom-generator/aibom_score_report.py
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
from typing import Dict
|
3 |
+
|
4 |
+
def humanize(text: str) -> str:
|
5 |
+
return text.replace('_', ' ').title()
|
6 |
+
|
7 |
+
def render_score_html(score_report: Dict[str, any]) -> str:
|
8 |
+
max_scores = score_report.get("max_scores", {
|
9 |
+
"required_fields": 20,
|
10 |
+
"metadata": 20,
|
11 |
+
"component_basic": 20,
|
12 |
+
"component_model_card": 30,
|
13 |
+
"external_references": 10
|
14 |
+
})
|
15 |
+
|
16 |
+
total_max = 100
|
17 |
+
|
18 |
+
html = f"""
|
19 |
+
<html>
|
20 |
+
<head>
|
21 |
+
<title>AIBOM Score Report</title>
|
22 |
+
<style>
|
23 |
+
body {{ font-family: Arial, sans-serif; margin: 20px; }}
|
24 |
+
h2 {{ color: #2c3e50; }}
|
25 |
+
table {{ border-collapse: collapse; width: 60%; margin-bottom: 20px; }}
|
26 |
+
th, td {{ border: 1px solid #ccc; padding: 8px; text-align: left; }}
|
27 |
+
th {{ background-color: #f9f9f9; }}
|
28 |
+
ul {{ list-style: none; padding-left: 0; }}
|
29 |
+
li::before {{ content: "\\2713 "; color: green; margin-right: 6px; }}
|
30 |
+
li.missing::before {{ content: "\\2717 "; color: red; }}
|
31 |
+
details {{ margin-top: 20px; }}
|
32 |
+
pre {{ background-color: #f4f4f4; padding: 10px; border-radius: 4px; }}
|
33 |
+
</style>
|
34 |
+
</head>
|
35 |
+
<body>
|
36 |
+
<h2>AIBOM Completeness Score: <strong>{score_report['total_score']}/{total_max}</strong></h2>
|
37 |
+
<h3>Section Scores</h3>
|
38 |
+
<table>
|
39 |
+
<tr><th>Section</th><th>Score</th></tr>
|
40 |
+
"""
|
41 |
+
for section, score in score_report.get("section_scores", {}).items():
|
42 |
+
max_score = max_scores.get(section, 0)
|
43 |
+
html += f"<tr><td>{humanize(section)}</td><td>{score}/{max_score}</td></tr>"
|
44 |
+
|
45 |
+
html += "</table>"
|
46 |
+
|
47 |
+
if "field_checklist" in score_report:
|
48 |
+
html += "<h3>Field Checklist</h3><ul>"
|
49 |
+
for field, mark in score_report["field_checklist"].items():
|
50 |
+
css_class = "missing" if mark == "✘" else ""
|
51 |
+
html += f"<li class=\"{css_class}\">{field}</li>"
|
52 |
+
html += "</ul>"
|
53 |
+
|
54 |
+
html += f"""
|
55 |
+
<details>
|
56 |
+
<summary>Raw Score Report</summary>
|
57 |
+
<pre>{json.dumps(score_report, indent=2)}</pre>
|
58 |
+
</details>
|
59 |
+
</body>
|
60 |
+
</html>
|
61 |
+
"""
|
62 |
+
return html
|
63 |
+
|
64 |
+
def save_score_report_html(score_report: Dict[str, any], output_path: str):
|
65 |
+
html_content = render_score_html(score_report)
|
66 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
67 |
+
f.write(html_content)
|
68 |
+
print(f"Score report saved to {output_path}")
|
src/aibom-generator/aibom_security.py
ADDED
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Security module for AIBOM generator implementation.
|
3 |
+
|
4 |
+
This module provides security functions that can be integrated
|
5 |
+
into the AIBOM generator to improve input validation, error handling,
|
6 |
+
and protection against common web vulnerabilities.
|
7 |
+
"""
|
8 |
+
|
9 |
+
import re
|
10 |
+
import os
|
11 |
+
import json
|
12 |
+
import logging
|
13 |
+
from typing import Dict, Any, Optional, Union
|
14 |
+
|
15 |
+
# Set up logging
|
16 |
+
logging.basicConfig(
|
17 |
+
level=logging.INFO,
|
18 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
19 |
+
)
|
20 |
+
logger = logging.getLogger(__name__)
|
21 |
+
|
22 |
+
def validate_model_id(model_id: str) -> str:
|
23 |
+
"""
|
24 |
+
Validate model ID to prevent injection attacks.
|
25 |
+
|
26 |
+
Args:
|
27 |
+
model_id: The model ID to validate
|
28 |
+
|
29 |
+
Returns:
|
30 |
+
The validated model ID
|
31 |
+
|
32 |
+
Raises:
|
33 |
+
ValueError: If the model ID contains invalid characters
|
34 |
+
"""
|
35 |
+
# Only allow alphanumeric characters, hyphens, underscores, and forward slashes
|
36 |
+
if not model_id or not isinstance(model_id, str):
|
37 |
+
raise ValueError("Model ID must be a non-empty string")
|
38 |
+
|
39 |
+
if not re.match(r'^[a-zA-Z0-9_\-/]+$', model_id):
|
40 |
+
raise ValueError(f"Invalid model ID format: {model_id}")
|
41 |
+
|
42 |
+
# Prevent path traversal attempts
|
43 |
+
if '..' in model_id:
|
44 |
+
raise ValueError(f"Invalid model ID - contains path traversal sequence: {model_id}")
|
45 |
+
|
46 |
+
return model_id
|
47 |
+
|
48 |
+
def safe_path_join(directory: str, filename: str) -> str:
|
49 |
+
"""
|
50 |
+
Safely join directory and filename to prevent path traversal attacks.
|
51 |
+
|
52 |
+
Args:
|
53 |
+
directory: Base directory
|
54 |
+
filename: Filename to append
|
55 |
+
|
56 |
+
Returns:
|
57 |
+
Safe file path
|
58 |
+
"""
|
59 |
+
# Ensure filename doesn't contain path traversal attempts
|
60 |
+
filename = os.path.basename(filename)
|
61 |
+
return os.path.join(directory, filename)
|
62 |
+
|
63 |
+
def safe_json_parse(json_string: str, default: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
64 |
+
"""
|
65 |
+
Safely parse JSON with error handling.
|
66 |
+
|
67 |
+
Args:
|
68 |
+
json_string: JSON string to parse
|
69 |
+
default: Default value to return if parsing fails
|
70 |
+
|
71 |
+
Returns:
|
72 |
+
Parsed JSON object or default value
|
73 |
+
"""
|
74 |
+
if default is None:
|
75 |
+
default = {}
|
76 |
+
|
77 |
+
try:
|
78 |
+
return json.loads(json_string)
|
79 |
+
except (json.JSONDecodeError, TypeError) as e:
|
80 |
+
logger.error(f"Invalid JSON: {e}")
|
81 |
+
return default
|
82 |
+
|
83 |
+
def sanitize_html_output(text: str) -> str:
|
84 |
+
"""
|
85 |
+
Sanitize text for safe HTML output to prevent XSS attacks.
|
86 |
+
|
87 |
+
Args:
|
88 |
+
text: Text to sanitize
|
89 |
+
|
90 |
+
Returns:
|
91 |
+
Sanitized text
|
92 |
+
"""
|
93 |
+
if not text or not isinstance(text, str):
|
94 |
+
return ""
|
95 |
+
|
96 |
+
# Replace HTML special characters with their entities
|
97 |
+
replacements = {
|
98 |
+
'&': '&',
|
99 |
+
'<': '<',
|
100 |
+
'>': '>',
|
101 |
+
'"': '"',
|
102 |
+
"'": ''',
|
103 |
+
'/': '/',
|
104 |
+
}
|
105 |
+
|
106 |
+
for char, entity in replacements.items():
|
107 |
+
text = text.replace(char, entity)
|
108 |
+
|
109 |
+
return text
|
110 |
+
|
111 |
+
def secure_file_operations(file_path: str, operation: str, content: Optional[str] = None) -> Union[str, bool]:
|
112 |
+
"""
|
113 |
+
Perform secure file operations with proper error handling.
|
114 |
+
|
115 |
+
Args:
|
116 |
+
file_path: Path to the file
|
117 |
+
operation: Operation to perform ('read', 'write', 'append')
|
118 |
+
content: Content to write (for 'write' and 'append' operations)
|
119 |
+
|
120 |
+
Returns:
|
121 |
+
File content for 'read' operation, True for successful 'write'/'append', False otherwise
|
122 |
+
"""
|
123 |
+
try:
|
124 |
+
if operation == 'read':
|
125 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
126 |
+
return f.read()
|
127 |
+
elif operation == 'write' and content is not None:
|
128 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
129 |
+
f.write(content)
|
130 |
+
return True
|
131 |
+
elif operation == 'append' and content is not None:
|
132 |
+
with open(file_path, 'a', encoding='utf-8') as f:
|
133 |
+
f.write(content)
|
134 |
+
return True
|
135 |
+
else:
|
136 |
+
logger.error(f"Invalid file operation: {operation}")
|
137 |
+
return False
|
138 |
+
except Exception as e:
|
139 |
+
logger.error(f"File operation failed: {e}")
|
140 |
+
return "" if operation == 'read' else False
|
141 |
+
|
142 |
+
def validate_url(url: str) -> bool:
|
143 |
+
"""
|
144 |
+
Validate URL format to prevent malicious URL injection.
|
145 |
+
|
146 |
+
Args:
|
147 |
+
url: URL to validate
|
148 |
+
|
149 |
+
Returns:
|
150 |
+
True if URL is valid, False otherwise
|
151 |
+
"""
|
152 |
+
# Basic URL validation
|
153 |
+
url_pattern = re.compile(
|
154 |
+
r'^(https?):\/\/' # http:// or https://
|
155 |
+
r'(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*' # domain segments
|
156 |
+
r'([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])' # last domain segment
|
157 |
+
r'(:\d+)?' # optional port
|
158 |
+
r'(\/[-a-zA-Z0-9%_.~#+]*)*' # path
|
159 |
+
r'(\?[;&a-zA-Z0-9%_.~+=-]*)?' # query string
|
160 |
+
r'(\#[-a-zA-Z0-9%_.~+=/]*)?$' # fragment
|
161 |
+
)
|
162 |
+
|
163 |
+
return bool(url_pattern.match(url))
|
164 |
+
|
165 |
+
def secure_template_rendering(template_content: str, context: Dict[str, Any]) -> str:
|
166 |
+
"""
|
167 |
+
Render templates securely with auto-escaping enabled.
|
168 |
+
|
169 |
+
This is a placeholder function. In a real implementation, you would use
|
170 |
+
a template engine like Jinja2 with auto-escaping enabled.
|
171 |
+
|
172 |
+
Args:
|
173 |
+
template_content: Template content
|
174 |
+
context: Context variables for rendering
|
175 |
+
|
176 |
+
Returns:
|
177 |
+
Rendered template
|
178 |
+
"""
|
179 |
+
try:
|
180 |
+
from jinja2 import Template
|
181 |
+
template = Template(template_content, autoescape=True)
|
182 |
+
return template.render(**context)
|
183 |
+
except ImportError:
|
184 |
+
logger.error("Jinja2 not available, falling back to basic rendering")
|
185 |
+
# Very basic fallback (not recommended for production)
|
186 |
+
result = template_content
|
187 |
+
for key, value in context.items():
|
188 |
+
if isinstance(value, str):
|
189 |
+
placeholder = "{{" + key + "}}"
|
190 |
+
result = result.replace(placeholder, sanitize_html_output(value))
|
191 |
+
return result
|
192 |
+
except Exception as e:
|
193 |
+
logger.error(f"Template rendering failed: {e}")
|
194 |
+
return ""
|
195 |
+
|
196 |
+
def implement_rate_limiting(user_id: str, action: str, limit: int, period: int) -> bool:
|
197 |
+
"""
|
198 |
+
Implement basic rate limiting to prevent abuse.
|
199 |
+
|
200 |
+
This is a placeholder function. In a real implementation, you would use
|
201 |
+
a database or cache to track request counts.
|
202 |
+
|
203 |
+
Args:
|
204 |
+
user_id: Identifier for the user
|
205 |
+
action: Action being performed
|
206 |
+
limit: Maximum number of actions allowed
|
207 |
+
period: Time period in seconds
|
208 |
+
|
209 |
+
Returns:
|
210 |
+
True if action is allowed, False if rate limit exceeded
|
211 |
+
"""
|
212 |
+
# In a real implementation, you would:
|
213 |
+
# 1. Check if user has exceeded limit in the given period
|
214 |
+
# 2. If not, increment counter and allow action
|
215 |
+
# 3. If yes, deny action
|
216 |
+
|
217 |
+
# Placeholder implementation always allows action
|
218 |
+
logger.info(f"Rate limiting check for user {user_id}, action {action}")
|
219 |
+
return True
|
220 |
+
|
221 |
+
# Integration example for the AIBOM generator
|
222 |
+
def secure_aibom_generation(model_id: str, output_file: Optional[str] = None) -> Dict[str, Any]:
|
223 |
+
"""
|
224 |
+
Example of how to integrate security improvements into AIBOM generation.
|
225 |
+
|
226 |
+
Args:
|
227 |
+
model_id: Model ID to generate AIBOM for
|
228 |
+
output_file: Optional output file path
|
229 |
+
|
230 |
+
Returns:
|
231 |
+
Generated AIBOM data
|
232 |
+
"""
|
233 |
+
try:
|
234 |
+
# Validate input
|
235 |
+
validated_model_id = validate_model_id(model_id)
|
236 |
+
|
237 |
+
# Process model ID securely
|
238 |
+
# (This would call your actual AIBOM generation logic)
|
239 |
+
aibom_data = {"message": f"AIBOM for {validated_model_id}"}
|
240 |
+
|
241 |
+
# Handle output file securely if provided
|
242 |
+
if output_file:
|
243 |
+
safe_output_path = safe_path_join(os.path.dirname(output_file), os.path.basename(output_file))
|
244 |
+
secure_file_operations(safe_output_path, 'write', json.dumps(aibom_data, indent=2))
|
245 |
+
|
246 |
+
return aibom_data
|
247 |
+
|
248 |
+
except ValueError as e:
|
249 |
+
# Handle validation errors
|
250 |
+
logger.error(f"Validation error: {e}")
|
251 |
+
return {"error": "Invalid input parameters"}
|
252 |
+
|
253 |
+
except Exception as e:
|
254 |
+
# Handle unexpected errors
|
255 |
+
logger.error(f"AIBOM generation failed: {e}")
|
256 |
+
return {"error": "An internal error occurred"}
|
src/aibom-generator/api.py
ADDED
@@ -0,0 +1,1204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python
|
2 |
+
import os
|
3 |
+
import json
|
4 |
+
import logging
|
5 |
+
import sys
|
6 |
+
from fastapi import FastAPI, HTTPException, Request, Form
|
7 |
+
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
|
8 |
+
from fastapi.staticfiles import StaticFiles
|
9 |
+
from fastapi.templating import Jinja2Templates
|
10 |
+
from pydantic import BaseModel
|
11 |
+
from datetime import datetime
|
12 |
+
from datasets import Dataset, load_dataset, concatenate_datasets
|
13 |
+
from typing import Dict, Optional, Any, List
|
14 |
+
import uuid
|
15 |
+
import re # Import regex module
|
16 |
+
import html # Import html module for escaping
|
17 |
+
from urllib.parse import urlparse
|
18 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
19 |
+
from huggingface_hub import HfApi
|
20 |
+
from huggingface_hub.utils import RepositoryNotFoundError # For specific error handling
|
21 |
+
|
22 |
+
|
23 |
+
# Configure logging
|
24 |
+
logging.basicConfig(level=logging.INFO)
|
25 |
+
logger = logging.getLogger(__name__)
|
26 |
+
|
27 |
+
# Define directories and constants
|
28 |
+
templates_dir = "templates"
|
29 |
+
OUTPUT_DIR = "/tmp/aibom_output"
|
30 |
+
MAX_AGE_DAYS = 7 # Remove files older than 7 days
|
31 |
+
MAX_FILES = 1000 # Keep maximum 1000 files
|
32 |
+
CLEANUP_INTERVAL = 100 # Run cleanup every 100 requests
|
33 |
+
|
34 |
+
# --- Add Counter Configuration (started as of May 3, 2025) ---
|
35 |
+
HF_REPO = "aetheris-ai/aisbom-usage-log" # User needs to create this private repo
|
36 |
+
HF_TOKEN = os.getenv("HF_TOKEN") # User must set this environment variable
|
37 |
+
# --- End Counter Configuration ---
|
38 |
+
|
39 |
+
# Create app
|
40 |
+
app = FastAPI(title="AI SBOM Generator API")
|
41 |
+
|
42 |
+
# Try different import paths
|
43 |
+
try:
|
44 |
+
from src.aibom_generator.rate_limiting import RateLimitMiddleware, ConcurrencyLimitMiddleware, RequestSizeLimitMiddleware
|
45 |
+
logger.info("Successfully imported rate_limiting from src.aibom_generator")
|
46 |
+
except ImportError:
|
47 |
+
try:
|
48 |
+
from .rate_limiting import RateLimitMiddleware, ConcurrencyLimitMiddleware, RequestSizeLimitMiddleware
|
49 |
+
logger.info("Successfully imported rate_limiting with relative import")
|
50 |
+
except ImportError:
|
51 |
+
try:
|
52 |
+
from rate_limiting import RateLimitMiddleware, ConcurrencyLimitMiddleware, RequestSizeLimitMiddleware
|
53 |
+
logger.info("Successfully imported rate_limiting from current directory")
|
54 |
+
except ImportError:
|
55 |
+
logger.error("Could not import rate_limiting, DoS protection disabled")
|
56 |
+
# Define dummy middleware classes that just pass through requests
|
57 |
+
class RateLimitMiddleware(BaseHTTPMiddleware):
|
58 |
+
def __init__(self, app, **kwargs):
|
59 |
+
super().__init__(app)
|
60 |
+
async def dispatch(self, request, call_next):
|
61 |
+
try:
|
62 |
+
return await call_next(request)
|
63 |
+
except Exception as e:
|
64 |
+
logger.error(f"Error in RateLimitMiddleware: {str(e)}")
|
65 |
+
return JSONResponse(
|
66 |
+
status_code=500,
|
67 |
+
content={"detail": f"Internal server error: {str(e)}"}
|
68 |
+
)
|
69 |
+
|
70 |
+
class ConcurrencyLimitMiddleware(BaseHTTPMiddleware):
|
71 |
+
def __init__(self, app, **kwargs):
|
72 |
+
super().__init__(app)
|
73 |
+
async def dispatch(self, request, call_next):
|
74 |
+
try:
|
75 |
+
return await call_next(request)
|
76 |
+
except Exception as e:
|
77 |
+
logger.error(f"Error in ConcurrencyLimitMiddleware: {str(e)}")
|
78 |
+
return JSONResponse(
|
79 |
+
status_code=500,
|
80 |
+
content={"detail": f"Internal server error: {str(e)}"}
|
81 |
+
)
|
82 |
+
|
83 |
+
class RequestSizeLimitMiddleware(BaseHTTPMiddleware):
|
84 |
+
def __init__(self, app, **kwargs):
|
85 |
+
super().__init__(app)
|
86 |
+
async def dispatch(self, request, call_next):
|
87 |
+
try:
|
88 |
+
return await call_next(request)
|
89 |
+
except Exception as e:
|
90 |
+
logger.error(f"Error in RequestSizeLimitMiddleware: {str(e)}")
|
91 |
+
return JSONResponse(
|
92 |
+
status_code=500,
|
93 |
+
content={"detail": f"Internal server error: {str(e)}"}
|
94 |
+
)
|
95 |
+
try:
|
96 |
+
from src.aibom_generator.captcha import verify_recaptcha
|
97 |
+
logger.info("Successfully imported captcha from src.aibom_generator")
|
98 |
+
except ImportError:
|
99 |
+
try:
|
100 |
+
from .captcha import verify_recaptcha
|
101 |
+
logger.info("Successfully imported captcha with relative import")
|
102 |
+
except ImportError:
|
103 |
+
try:
|
104 |
+
from captcha import verify_recaptcha
|
105 |
+
logger.info("Successfully imported captcha from current directory")
|
106 |
+
except ImportError:
|
107 |
+
logger.warning("Could not import captcha module, CAPTCHA verification disabled")
|
108 |
+
# Define a dummy verify_recaptcha function that always succeeds
|
109 |
+
def verify_recaptcha(response_token=None):
|
110 |
+
logger.warning("Using dummy CAPTCHA verification (always succeeds)")
|
111 |
+
return True
|
112 |
+
|
113 |
+
|
114 |
+
|
115 |
+
# Rate limiting middleware
|
116 |
+
app.add_middleware(
|
117 |
+
RateLimitMiddleware,
|
118 |
+
rate_limit_per_minute=10, # Adjust as needed
|
119 |
+
rate_limit_window=60,
|
120 |
+
protected_routes=["/generate", "/api/generate", "/api/generate-with-report"]
|
121 |
+
)
|
122 |
+
|
123 |
+
app.add_middleware(
|
124 |
+
ConcurrencyLimitMiddleware,
|
125 |
+
max_concurrent_requests=5, # Adjust based on server capacity
|
126 |
+
timeout=5.0,
|
127 |
+
protected_routes=["/generate", "/api/generate", "/api/generate-with-report"]
|
128 |
+
)
|
129 |
+
|
130 |
+
|
131 |
+
# Size limiting middleware
|
132 |
+
app.add_middleware(
|
133 |
+
RequestSizeLimitMiddleware,
|
134 |
+
max_content_length=1024*1024 # 1MB
|
135 |
+
)
|
136 |
+
|
137 |
+
|
138 |
+
# Define models
|
139 |
+
class StatusResponse(BaseModel):
|
140 |
+
status: str
|
141 |
+
version: str
|
142 |
+
generator_version: str
|
143 |
+
|
144 |
+
# Initialize templates
|
145 |
+
templates = Jinja2Templates(directory=templates_dir)
|
146 |
+
|
147 |
+
# Ensure output directory exists
|
148 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
149 |
+
|
150 |
+
# Mount output directory as static files
|
151 |
+
app.mount("/output", StaticFiles(directory=OUTPUT_DIR), name="output")
|
152 |
+
|
153 |
+
# Request counter for periodic cleanup
|
154 |
+
request_counter = 0
|
155 |
+
|
156 |
+
# Import cleanup_utils using absolute import
|
157 |
+
try:
|
158 |
+
from src.aibom_generator.cleanup_utils import perform_cleanup
|
159 |
+
logger.info("Successfully imported cleanup_utils")
|
160 |
+
except ImportError:
|
161 |
+
try:
|
162 |
+
from cleanup_utils import perform_cleanup
|
163 |
+
logger.info("Successfully imported cleanup_utils from current directory")
|
164 |
+
except ImportError:
|
165 |
+
logger.error("Could not import cleanup_utils, defining functions inline")
|
166 |
+
# Define cleanup functions inline if import fails
|
167 |
+
def cleanup_old_files(directory, max_age_days=7):
|
168 |
+
"""Remove files older than max_age_days from the specified directory."""
|
169 |
+
if not os.path.exists(directory):
|
170 |
+
logger.warning(f"Directory does not exist: {directory}")
|
171 |
+
return 0
|
172 |
+
|
173 |
+
removed_count = 0
|
174 |
+
now = datetime.now()
|
175 |
+
|
176 |
+
try:
|
177 |
+
for filename in os.listdir(directory):
|
178 |
+
file_path = os.path.join(directory, filename)
|
179 |
+
if os.path.isfile(file_path):
|
180 |
+
file_age = now - datetime.fromtimestamp(os.path.getmtime(file_path))
|
181 |
+
if file_age.days > max_age_days:
|
182 |
+
try:
|
183 |
+
os.remove(file_path)
|
184 |
+
removed_count += 1
|
185 |
+
logger.info(f"Removed old file: {file_path}")
|
186 |
+
except Exception as e:
|
187 |
+
logger.error(f"Error removing file {file_path}: {e}")
|
188 |
+
|
189 |
+
logger.info(f"Cleanup completed: removed {removed_count} files older than {max_age_days} days from {directory}")
|
190 |
+
return removed_count
|
191 |
+
except Exception as e:
|
192 |
+
logger.error(f"Error during cleanup of directory {directory}: {e}")
|
193 |
+
return 0
|
194 |
+
|
195 |
+
def limit_file_count(directory, max_files=1000):
|
196 |
+
"""Ensure no more than max_files are kept in the directory (removes oldest first)."""
|
197 |
+
if not os.path.exists(directory):
|
198 |
+
logger.warning(f"Directory does not exist: {directory}")
|
199 |
+
return 0
|
200 |
+
|
201 |
+
removed_count = 0
|
202 |
+
|
203 |
+
try:
|
204 |
+
files = []
|
205 |
+
for filename in os.listdir(directory):
|
206 |
+
file_path = os.path.join(directory, filename)
|
207 |
+
if os.path.isfile(file_path):
|
208 |
+
files.append((file_path, os.path.getmtime(file_path)))
|
209 |
+
|
210 |
+
# Sort by modification time (oldest first)
|
211 |
+
files.sort(key=lambda x: x[1])
|
212 |
+
|
213 |
+
# Remove oldest files if limit is exceeded
|
214 |
+
files_to_remove = files[:-max_files] if len(files) > max_files else []
|
215 |
+
|
216 |
+
for file_path, _ in files_to_remove:
|
217 |
+
try:
|
218 |
+
os.remove(file_path)
|
219 |
+
removed_count += 1
|
220 |
+
logger.info(f"Removed excess file: {file_path}")
|
221 |
+
except Exception as e:
|
222 |
+
logger.error(f"Error removing file {file_path}: {e}")
|
223 |
+
|
224 |
+
logger.info(f"File count limit enforced: removed {removed_count} oldest files from {directory}, keeping max {max_files}")
|
225 |
+
return removed_count
|
226 |
+
except Exception as e:
|
227 |
+
logger.error(f"Error during file count limiting in directory {directory}: {e}")
|
228 |
+
return 0
|
229 |
+
|
230 |
+
def perform_cleanup(directory, max_age_days=7, max_files=1000):
|
231 |
+
"""Perform both time-based and count-based cleanup."""
|
232 |
+
time_removed = cleanup_old_files(directory, max_age_days)
|
233 |
+
count_removed = limit_file_count(directory, max_files)
|
234 |
+
return time_removed + count_removed
|
235 |
+
|
236 |
+
# Run initial cleanup
|
237 |
+
try:
|
238 |
+
removed = perform_cleanup(OUTPUT_DIR, MAX_AGE_DAYS, MAX_FILES)
|
239 |
+
logger.info(f"Initial cleanup removed {removed} files")
|
240 |
+
except Exception as e:
|
241 |
+
logger.error(f"Error during initial cleanup: {e}")
|
242 |
+
|
243 |
+
# Define middleware
|
244 |
+
@app.middleware("http" )
|
245 |
+
async def cleanup_middleware(request, call_next):
|
246 |
+
"""Middleware to periodically run cleanup."""
|
247 |
+
global request_counter
|
248 |
+
|
249 |
+
# Increment request counter
|
250 |
+
request_counter += 1
|
251 |
+
|
252 |
+
# Run cleanup periodically
|
253 |
+
if request_counter % CLEANUP_INTERVAL == 0:
|
254 |
+
logger.info(f"Running scheduled cleanup after {request_counter} requests")
|
255 |
+
try:
|
256 |
+
removed = perform_cleanup(OUTPUT_DIR, MAX_AGE_DAYS, MAX_FILES)
|
257 |
+
logger.info(f"Scheduled cleanup removed {removed} files")
|
258 |
+
except Exception as e:
|
259 |
+
logger.error(f"Error during scheduled cleanup: {e}")
|
260 |
+
|
261 |
+
# Process the request
|
262 |
+
response = await call_next(request)
|
263 |
+
return response
|
264 |
+
|
265 |
+
|
266 |
+
# --- Model ID Validation and Normalization Helpers ---
|
267 |
+
# Regex for valid Hugging Face ID parts (alphanumeric, -, _, .)
|
268 |
+
# Allows owner/model format
|
269 |
+
HF_ID_REGEX = re.compile(r"^[a-zA-Z0-9\.\-\_]+/[a-zA-Z0-9\.\-\_]+$")
|
270 |
+
|
271 |
+
def is_valid_hf_input(input_str: str) -> bool:
|
272 |
+
"""Checks if the input is a valid Hugging Face model ID or URL."""
|
273 |
+
if not input_str or len(input_str) > 200: # Basic length check
|
274 |
+
return False
|
275 |
+
|
276 |
+
if input_str.startswith(("http://", "https://") ):
|
277 |
+
try:
|
278 |
+
parsed = urlparse(input_str)
|
279 |
+
# Check domain and path structure
|
280 |
+
if parsed.netloc == "huggingface.co":
|
281 |
+
path_parts = parsed.path.strip("/").split("/")
|
282 |
+
# Must have at least owner/model, can have more like /tree/main
|
283 |
+
if len(path_parts) >= 2 and path_parts[0] and path_parts[1]:
|
284 |
+
# Check characters in the relevant parts
|
285 |
+
if re.match(r"^[a-zA-Z0-9\.\-\_]+$", path_parts[0]) and \
|
286 |
+
re.match(r"^[a-zA-Z0-9\.\-\_]+$", path_parts[1]):
|
287 |
+
return True
|
288 |
+
return False # Not a valid HF URL format
|
289 |
+
except Exception:
|
290 |
+
return False # URL parsing failed
|
291 |
+
else:
|
292 |
+
# Assume owner/model format, check with regex
|
293 |
+
return bool(HF_ID_REGEX.match(input_str))
|
294 |
+
|
295 |
+
def _normalise_model_id(raw_id: str) -> str:
|
296 |
+
"""
|
297 |
+
Accept either validated 'owner/model' or a validated full URL like
|
298 |
+
'https://huggingface.co/owner/model'. Return 'owner/model'.
|
299 |
+
Assumes input has already been validated by is_valid_hf_input.
|
300 |
+
"""
|
301 |
+
if raw_id.startswith(("http://", "https://") ):
|
302 |
+
path = urlparse(raw_id).path.lstrip("/")
|
303 |
+
parts = path.split("/")
|
304 |
+
# We know from validation that parts[0] and parts[1] exist
|
305 |
+
return f"{parts[0]}/{parts[1]}"
|
306 |
+
return raw_id # Already in owner/model format
|
307 |
+
|
308 |
+
# --- End Model ID Helpers ---
|
309 |
+
|
310 |
+
|
311 |
+
# --- Add Counter Helper Functions ---
|
312 |
+
def log_sbom_generation(model_id: str):
|
313 |
+
"""Logs a successful SBOM generation event to the Hugging Face dataset."""
|
314 |
+
if not HF_TOKEN:
|
315 |
+
logger.warning("HF_TOKEN not set. Skipping SBOM generation logging.")
|
316 |
+
return
|
317 |
+
|
318 |
+
try:
|
319 |
+
# Normalize model_id before logging
|
320 |
+
normalized_model_id_for_log = _normalise_model_id(model_id) # added to normalize id
|
321 |
+
log_data = {
|
322 |
+
"timestamp": [datetime.utcnow().isoformat()],
|
323 |
+
"event": ["generated"],
|
324 |
+
"model_id": [normalized_model_id_for_log] # use normalized_model_id_for_log
|
325 |
+
}
|
326 |
+
ds_new_log = Dataset.from_dict(log_data)
|
327 |
+
|
328 |
+
# Try to load existing dataset to append
|
329 |
+
try:
|
330 |
+
# Use trust_remote_code=True if required by the dataset/model on HF
|
331 |
+
# Corrected: Removed unnecessary backslashes around 'train'
|
332 |
+
existing_ds = load_dataset(HF_REPO, token=HF_TOKEN, split='train', trust_remote_code=True)
|
333 |
+
# Check if dataset is empty or has different columns (handle initial creation)
|
334 |
+
if len(existing_ds) > 0 and set(existing_ds.column_names) == set(log_data.keys()):
|
335 |
+
ds_to_push = concatenate_datasets([existing_ds, ds_new_log])
|
336 |
+
elif len(existing_ds) == 0:
|
337 |
+
logger.info(f"Dataset {HF_REPO} is empty. Pushing initial data.")
|
338 |
+
ds_to_push = ds_new_log
|
339 |
+
else:
|
340 |
+
logger.warning(f"Dataset {HF_REPO} has unexpected columns {existing_ds.column_names} vs {list(log_data.keys())}. Appending new log anyway, structure might differ.")
|
341 |
+
# Attempt concatenation even if columns differ slightly, HF might handle it
|
342 |
+
# Or consider more robust schema migration/handling if needed
|
343 |
+
ds_to_push = concatenate_datasets([existing_ds, ds_new_log])
|
344 |
+
|
345 |
+
except Exception as load_err:
|
346 |
+
# Handle case where dataset doesn't exist yet or other loading errors
|
347 |
+
# Corrected: Removed unnecessary backslash in doesn't
|
348 |
+
logger.info(f"Could not load existing dataset {HF_REPO} (may not exist yet): {load_err}. Pushing new dataset.")
|
349 |
+
ds_to_push = ds_new_log # ds is already prepared with the new log entry
|
350 |
+
|
351 |
+
# Push the updated or new dataset
|
352 |
+
# Corrected: Removed unnecessary backslash in it's
|
353 |
+
ds_to_push.push_to_hub(HF_REPO, token=HF_TOKEN, private=True) # Ensure it's private
|
354 |
+
logger.info(f"Successfully logged SBOM generation for {normalized_model_id_for_log} to {HF_REPO}") # use normalized model id
|
355 |
+
|
356 |
+
except Exception as e:
|
357 |
+
logger.error(f"Failed to log SBOM generation to {HF_REPO}: {e}")
|
358 |
+
|
359 |
+
def get_sbom_count() -> str:
|
360 |
+
"""Retrieves the total count of generated SBOMs from the Hugging Face dataset."""
|
361 |
+
if not HF_TOKEN:
|
362 |
+
logger.warning("HF_TOKEN not set. Cannot retrieve SBOM count.")
|
363 |
+
return "N/A"
|
364 |
+
try:
|
365 |
+
# Load the dataset - assumes 'train' split exists after first push
|
366 |
+
# Use trust_remote_code=True if required by the dataset/model on HF
|
367 |
+
# Corrected: Removed unnecessary backslashes around 'train'
|
368 |
+
ds = load_dataset(HF_REPO, token=HF_TOKEN, split='train', trust_remote_code=True)
|
369 |
+
count = len(ds)
|
370 |
+
logger.info(f"Retrieved SBOM count: {count} from {HF_REPO}")
|
371 |
+
# Format count for display (e.g., add commas for large numbers)
|
372 |
+
return f"{count:,}"
|
373 |
+
except Exception as e:
|
374 |
+
logger.error(f"Failed to retrieve SBOM count from {HF_REPO}: {e}")
|
375 |
+
# Return "N/A" or similar indicator on error
|
376 |
+
return "N/A"
|
377 |
+
# --- End Counter Helper Functions ---
|
378 |
+
|
379 |
+
@app.on_event("startup")
|
380 |
+
async def startup_event():
|
381 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
382 |
+
logger.info(f"Output directory ready at {OUTPUT_DIR}")
|
383 |
+
logger.info(f"Registered routes: {[route.path for route in app.routes]}")
|
384 |
+
|
385 |
+
@app.get("/", response_class=HTMLResponse)
|
386 |
+
async def root(request: Request):
|
387 |
+
sbom_count = get_sbom_count() # Get count
|
388 |
+
try:
|
389 |
+
return templates.TemplateResponse("index.html", {"request": request, "sbom_count": sbom_count}) # Pass to template
|
390 |
+
except Exception as e:
|
391 |
+
logger.error(f"Error rendering template: {str(e)}")
|
392 |
+
# Attempt to render error page even if main page fails
|
393 |
+
try:
|
394 |
+
return templates.TemplateResponse("error.html", {"request": request, "error": f"Template rendering error: {str(e)}", "sbom_count": sbom_count})
|
395 |
+
except Exception as template_err:
|
396 |
+
# Fallback if error template also fails
|
397 |
+
logger.error(f"Error rendering error template: {template_err}")
|
398 |
+
raise HTTPException(status_code=500, detail=f"Template rendering error: {str(e)}")
|
399 |
+
|
400 |
+
@app.get("/status", response_model=StatusResponse)
|
401 |
+
async def get_status():
|
402 |
+
return StatusResponse(status="operational", version="1.0.0", generator_version="1.0.0")
|
403 |
+
|
404 |
+
# Import utils module for completeness score calculation
|
405 |
+
def import_utils():
|
406 |
+
"""Import utils module with fallback paths."""
|
407 |
+
try:
|
408 |
+
# Try different import paths
|
409 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
410 |
+
|
411 |
+
# Try direct import first
|
412 |
+
try:
|
413 |
+
from utils import calculate_completeness_score
|
414 |
+
logger.info("Imported utils.calculate_completeness_score directly")
|
415 |
+
return calculate_completeness_score
|
416 |
+
except ImportError:
|
417 |
+
pass
|
418 |
+
|
419 |
+
# Try from src
|
420 |
+
try:
|
421 |
+
from src.aibom_generator.utils import calculate_completeness_score
|
422 |
+
logger.info("Imported src.aibom_generator.utils.calculate_completeness_score")
|
423 |
+
return calculate_completeness_score
|
424 |
+
except ImportError:
|
425 |
+
pass
|
426 |
+
|
427 |
+
# Try from aibom_generator
|
428 |
+
try:
|
429 |
+
from aibom_generator.utils import calculate_completeness_score
|
430 |
+
logger.info("Imported aibom_generator.utils.calculate_completeness_score")
|
431 |
+
return calculate_completeness_score
|
432 |
+
except ImportError:
|
433 |
+
pass
|
434 |
+
|
435 |
+
# If all imports fail, use the default implementation
|
436 |
+
logger.warning("Could not import calculate_completeness_score, using default implementation")
|
437 |
+
return None
|
438 |
+
except Exception as e:
|
439 |
+
logger.error(f"Error importing utils: {str(e)}")
|
440 |
+
return None
|
441 |
+
|
442 |
+
# Try to import the calculate_completeness_score function
|
443 |
+
calculate_completeness_score = import_utils()
|
444 |
+
|
445 |
+
# Helper function to create a comprehensive completeness_score with field_checklist
|
446 |
+
def create_comprehensive_completeness_score(aibom=None):
|
447 |
+
"""
|
448 |
+
Create a comprehensive completeness_score object with all required attributes.
|
449 |
+
If aibom is provided and calculate_completeness_score is available, use it to calculate the score.
|
450 |
+
Otherwise, return a default score structure.
|
451 |
+
"""
|
452 |
+
# If we have the calculate_completeness_score function and an AIBOM, use it
|
453 |
+
if calculate_completeness_score and aibom:
|
454 |
+
try:
|
455 |
+
return calculate_completeness_score(aibom, validate=True, use_best_practices=True)
|
456 |
+
except Exception as e:
|
457 |
+
logger.error(f"Error calculating completeness score: {str(e)}")
|
458 |
+
|
459 |
+
# Otherwise, return a default comprehensive structure
|
460 |
+
return {
|
461 |
+
"total_score": 75.5, # Default score for better UI display
|
462 |
+
"section_scores": {
|
463 |
+
"required_fields": 20,
|
464 |
+
"metadata": 15,
|
465 |
+
"component_basic": 18,
|
466 |
+
"component_model_card": 15,
|
467 |
+
"external_references": 7.5
|
468 |
+
},
|
469 |
+
"max_scores": {
|
470 |
+
"required_fields": 20,
|
471 |
+
"metadata": 20,
|
472 |
+
"component_basic": 20,
|
473 |
+
"component_model_card": 30,
|
474 |
+
"external_references": 10
|
475 |
+
},
|
476 |
+
"field_checklist": {
|
477 |
+
# Required fields
|
478 |
+
"bomFormat": "✔ ★★★",
|
479 |
+
"specVersion": "✔ ★★★",
|
480 |
+
"serialNumber": "✔ ★★★",
|
481 |
+
"version": "✔ ★★★",
|
482 |
+
"metadata.timestamp": "✔ ★★",
|
483 |
+
"metadata.tools": "✔ ★★",
|
484 |
+
"metadata.authors": "✔ ★★",
|
485 |
+
"metadata.component": "✔ ★★",
|
486 |
+
|
487 |
+
# Component basic info
|
488 |
+
"component.type": "✔ ★★",
|
489 |
+
"component.name": "✔ ★★★",
|
490 |
+
"component.bom-ref": "✔ ★★",
|
491 |
+
"component.purl": "✔ ★★",
|
492 |
+
"component.description": "✔ ★★",
|
493 |
+
"component.licenses": "✔ ★★",
|
494 |
+
|
495 |
+
# Model card
|
496 |
+
"modelCard.modelParameters": "✔ ★★",
|
497 |
+
"modelCard.quantitativeAnalysis": "✘ ★★",
|
498 |
+
"modelCard.considerations": "✔ ★★",
|
499 |
+
|
500 |
+
# External references
|
501 |
+
"externalReferences": "✔ ★",
|
502 |
+
|
503 |
+
# Additional fields from FIELD_CLASSIFICATION
|
504 |
+
"name": "✔ ★★★",
|
505 |
+
"downloadLocation": "✔ ★★★",
|
506 |
+
"primaryPurpose": "✔ ★★★",
|
507 |
+
"suppliedBy": "✔ ★★★",
|
508 |
+
"energyConsumption": "✘ ★★",
|
509 |
+
"hyperparameter": "✔ ★★",
|
510 |
+
"limitation": "✔ ★★",
|
511 |
+
"safetyRiskAssessment": "✘ ★★",
|
512 |
+
"typeOfModel": "✔ ★★",
|
513 |
+
"modelExplainability": "✘ ★",
|
514 |
+
"standardCompliance": "✘ ★",
|
515 |
+
"domain": "✔ ★",
|
516 |
+
"energyQuantity": "✘ ★",
|
517 |
+
"energyUnit": "✘ ★",
|
518 |
+
"informationAboutTraining": "✔ ★",
|
519 |
+
"informationAboutApplication": "✔ ★",
|
520 |
+
"metric": "✘ ★",
|
521 |
+
"metricDecisionThreshold": "✘ ★",
|
522 |
+
"modelDataPreprocessing": "✘ ★",
|
523 |
+
"autonomyType": "✘ ★",
|
524 |
+
"useSensitivePersonalInformation": "✘ ★"
|
525 |
+
},
|
526 |
+
"field_tiers": {
|
527 |
+
# Required fields
|
528 |
+
"bomFormat": "critical",
|
529 |
+
"specVersion": "critical",
|
530 |
+
"serialNumber": "critical",
|
531 |
+
"version": "critical",
|
532 |
+
"metadata.timestamp": "important",
|
533 |
+
"metadata.tools": "important",
|
534 |
+
"metadata.authors": "important",
|
535 |
+
"metadata.component": "important",
|
536 |
+
|
537 |
+
# Component basic info
|
538 |
+
"component.type": "important",
|
539 |
+
"component.name": "critical",
|
540 |
+
"component.bom-ref": "important",
|
541 |
+
"component.purl": "important",
|
542 |
+
"component.description": "important",
|
543 |
+
"component.licenses": "important",
|
544 |
+
|
545 |
+
# Model card
|
546 |
+
"modelCard.modelParameters": "important",
|
547 |
+
"modelCard.quantitativeAnalysis": "important",
|
548 |
+
"modelCard.considerations": "important",
|
549 |
+
|
550 |
+
# External references
|
551 |
+
"externalReferences": "supplementary",
|
552 |
+
|
553 |
+
# Additional fields from FIELD_CLASSIFICATION
|
554 |
+
"name": "critical",
|
555 |
+
"downloadLocation": "critical",
|
556 |
+
"primaryPurpose": "critical",
|
557 |
+
"suppliedBy": "critical",
|
558 |
+
"energyConsumption": "important",
|
559 |
+
"hyperparameter": "important",
|
560 |
+
"limitation": "important",
|
561 |
+
"safetyRiskAssessment": "important",
|
562 |
+
"typeOfModel": "important",
|
563 |
+
"modelExplainability": "supplementary",
|
564 |
+
"standardCompliance": "supplementary",
|
565 |
+
"domain": "supplementary",
|
566 |
+
"energyQuantity": "supplementary",
|
567 |
+
"energyUnit": "supplementary",
|
568 |
+
"informationAboutTraining": "supplementary",
|
569 |
+
"informationAboutApplication": "supplementary",
|
570 |
+
"metric": "supplementary",
|
571 |
+
"metricDecisionThreshold": "supplementary",
|
572 |
+
"modelDataPreprocessing": "supplementary",
|
573 |
+
"autonomyType": "supplementary",
|
574 |
+
"useSensitivePersonalInformation": "supplementary"
|
575 |
+
},
|
576 |
+
"missing_fields": {
|
577 |
+
"critical": [],
|
578 |
+
"important": ["modelCard.quantitativeAnalysis", "energyConsumption", "safetyRiskAssessment"],
|
579 |
+
"supplementary": ["modelExplainability", "standardCompliance", "energyQuantity", "energyUnit",
|
580 |
+
"metric", "metricDecisionThreshold", "modelDataPreprocessing",
|
581 |
+
"autonomyType", "useSensitivePersonalInformation"]
|
582 |
+
},
|
583 |
+
"completeness_profile": {
|
584 |
+
"name": "standard",
|
585 |
+
"description": "Comprehensive fields for proper documentation",
|
586 |
+
"satisfied": True
|
587 |
+
},
|
588 |
+
"penalty_applied": False,
|
589 |
+
"penalty_reason": None,
|
590 |
+
"recommendations": [
|
591 |
+
{
|
592 |
+
"priority": "medium",
|
593 |
+
"field": "modelCard.quantitativeAnalysis",
|
594 |
+
"message": "Missing important field: modelCard.quantitativeAnalysis",
|
595 |
+
"recommendation": "Add quantitative analysis information to the model card"
|
596 |
+
},
|
597 |
+
{
|
598 |
+
"priority": "medium",
|
599 |
+
"field": "energyConsumption",
|
600 |
+
"message": "Missing important field: energyConsumption - helpful for environmental impact assessment",
|
601 |
+
"recommendation": "Consider documenting energy consumption metrics for better transparency"
|
602 |
+
},
|
603 |
+
{
|
604 |
+
"priority": "medium",
|
605 |
+
"field": "safetyRiskAssessment",
|
606 |
+
"message": "Missing important field: safetyRiskAssessment",
|
607 |
+
"recommendation": "Add safety risk assessment information to improve documentation"
|
608 |
+
}
|
609 |
+
]
|
610 |
+
}
|
611 |
+
|
612 |
+
@app.post("/generate", response_class=HTMLResponse)
|
613 |
+
async def generate_form(
|
614 |
+
request: Request,
|
615 |
+
model_id: str = Form(...),
|
616 |
+
include_inference: bool = Form(False),
|
617 |
+
use_best_practices: bool = Form(True),
|
618 |
+
g_recaptcha_response: Optional[str] = Form(None)
|
619 |
+
):
|
620 |
+
# Debug log all form data
|
621 |
+
form_data = await request.form()
|
622 |
+
logger.info(f"All form data: {dict(form_data)}")
|
623 |
+
|
624 |
+
# Verify CAPTCHA
|
625 |
+
if not verify_recaptcha(g_recaptcha_response):
|
626 |
+
return templates.TemplateResponse(
|
627 |
+
"error.html", {
|
628 |
+
"request": request,
|
629 |
+
"error": "Security verification failed. Please try again.",
|
630 |
+
"sbom_count": get_sbom_count()
|
631 |
+
}
|
632 |
+
)
|
633 |
+
|
634 |
+
sbom_count = get_sbom_count() # Get count early for context
|
635 |
+
|
636 |
+
# --- Input Sanitization ---
|
637 |
+
sanitized_model_id = html.escape(model_id)
|
638 |
+
|
639 |
+
# --- Input Format Validation ---
|
640 |
+
if not is_valid_hf_input(sanitized_model_id):
|
641 |
+
error_message = "Invalid input format. Please provide a valid Hugging Face model ID (e.g., 'owner/model') or a full model URL (e.g., 'https://huggingface.co/owner/model') ."
|
642 |
+
logger.warning(f"Invalid model input format received: {model_id}") # Log original input
|
643 |
+
# Try to display sanitized input in error message
|
644 |
+
return templates.TemplateResponse(
|
645 |
+
"error.html", {"request": request, "error": error_message, "sbom_count": sbom_count, "model_id": sanitized_model_id}
|
646 |
+
)
|
647 |
+
|
648 |
+
# --- Normalize the SANITIZED and VALIDATED model ID ---
|
649 |
+
normalized_model_id = _normalise_model_id(sanitized_model_id)
|
650 |
+
|
651 |
+
# --- Check if the ID corresponds to an actual HF Model ---
|
652 |
+
try:
|
653 |
+
hf_api = HfApi()
|
654 |
+
logger.info(f"Attempting to fetch model info for: {normalized_model_id}")
|
655 |
+
model_info = hf_api.model_info(normalized_model_id)
|
656 |
+
logger.info(f"Successfully fetched model info for: {normalized_model_id}")
|
657 |
+
except RepositoryNotFoundError:
|
658 |
+
error_message = f"Error: The provided ID \"{normalized_model_id}\" could not be found on Hugging Face or does not correspond to a model repository."
|
659 |
+
logger.warning(f"Repository not found for ID: {normalized_model_id}")
|
660 |
+
return templates.TemplateResponse(
|
661 |
+
"error.html", {"request": request, "error": error_message, "sbom_count": sbom_count, "model_id": normalized_model_id}
|
662 |
+
)
|
663 |
+
except Exception as api_err: # Catch other potential API errors
|
664 |
+
error_message = f"Error verifying model ID with Hugging Face API: {str(api_err)}"
|
665 |
+
logger.error(f"HF API error for {normalized_model_id}: {str(api_err)}")
|
666 |
+
return templates.TemplateResponse(
|
667 |
+
"error.html", {"request": request, "error": error_message, "sbom_count": sbom_count, "model_id": normalized_model_id}
|
668 |
+
)
|
669 |
+
# --- End Model Existence Check ---
|
670 |
+
|
671 |
+
|
672 |
+
# --- Main Generation Logic ---
|
673 |
+
try:
|
674 |
+
# Try different import paths for AIBOMGenerator
|
675 |
+
generator = None
|
676 |
+
try:
|
677 |
+
from src.aibom_generator.generator import AIBOMGenerator
|
678 |
+
generator = AIBOMGenerator()
|
679 |
+
except ImportError:
|
680 |
+
try:
|
681 |
+
from aibom_generator.generator import AIBOMGenerator
|
682 |
+
generator = AIBOMGenerator()
|
683 |
+
except ImportError:
|
684 |
+
try:
|
685 |
+
from generator import AIBOMGenerator
|
686 |
+
generator = AIBOMGenerator()
|
687 |
+
except ImportError:
|
688 |
+
logger.error("Could not import AIBOMGenerator from any known location")
|
689 |
+
raise ImportError("Could not import AIBOMGenerator from any known location")
|
690 |
+
|
691 |
+
# Generate AIBOM (pass SANITIZED ID)
|
692 |
+
aibom = generator.generate_aibom(
|
693 |
+
model_id=sanitized_model_id, # Use sanitized ID
|
694 |
+
include_inference=include_inference,
|
695 |
+
use_best_practices=use_best_practices
|
696 |
+
)
|
697 |
+
enhancement_report = generator.get_enhancement_report()
|
698 |
+
|
699 |
+
# Save AIBOM to file, use industry term ai_sbom in file name
|
700 |
+
# Corrected: Removed unnecessary backslashes around '/' and '_'
|
701 |
+
# Save AIBOM to file using normalized ID
|
702 |
+
filename = f"{normalized_model_id.replace('/', '_')}_ai_sbom.json"
|
703 |
+
filepath = os.path.join(OUTPUT_DIR, filename)
|
704 |
+
|
705 |
+
with open(filepath, "w") as f:
|
706 |
+
json.dump(aibom, f, indent=2)
|
707 |
+
|
708 |
+
# --- Log Generation Event ---
|
709 |
+
log_sbom_generation(sanitized_model_id) # Use sanitized ID
|
710 |
+
sbom_count = get_sbom_count() # Refresh count after logging
|
711 |
+
# --- End Log ---
|
712 |
+
|
713 |
+
download_url = f"/output/{filename}"
|
714 |
+
|
715 |
+
# Create download and UI interaction scripts
|
716 |
+
download_script = f"""
|
717 |
+
<script>
|
718 |
+
function downloadJSON() {{
|
719 |
+
const a = document.createElement('a');
|
720 |
+
a.href = '{download_url}';
|
721 |
+
a.download = '{filename}';
|
722 |
+
document.body.appendChild(a);
|
723 |
+
a.click();
|
724 |
+
document.body.removeChild(a);
|
725 |
+
}}
|
726 |
+
|
727 |
+
function switchTab(tabId) {{
|
728 |
+
// Hide all tabs
|
729 |
+
document.querySelectorAll('.tab-content').forEach(tab => {{
|
730 |
+
tab.classList.remove('active');
|
731 |
+
}});
|
732 |
+
|
733 |
+
// Deactivate all tab buttons
|
734 |
+
document.querySelectorAll('.aibom-tab').forEach(button => {{
|
735 |
+
button.classList.remove('active');
|
736 |
+
}});
|
737 |
+
|
738 |
+
// Show the selected tab
|
739 |
+
document.getElementById(tabId).classList.add('active');
|
740 |
+
|
741 |
+
// Activate the clicked button
|
742 |
+
event.currentTarget.classList.add('active');
|
743 |
+
}}
|
744 |
+
|
745 |
+
function toggleCollapsible(element) {{
|
746 |
+
element.classList.toggle('active');
|
747 |
+
var content = element.nextElementSibling;
|
748 |
+
if (content.style.maxHeight) {{
|
749 |
+
content.style.maxHeight = null;
|
750 |
+
content.classList.remove('active');
|
751 |
+
}} else {{
|
752 |
+
content.style.maxHeight = content.scrollHeight + "px";
|
753 |
+
content.classList.add('active');
|
754 |
+
}}
|
755 |
+
}}
|
756 |
+
</script>
|
757 |
+
"""
|
758 |
+
|
759 |
+
# Get completeness score or create a comprehensive one if not available
|
760 |
+
# Use sanitized_model_id
|
761 |
+
completeness_score = None
|
762 |
+
if hasattr(generator, 'get_completeness_score'):
|
763 |
+
try:
|
764 |
+
completeness_score = generator.get_completeness_score(sanitized_model_id)
|
765 |
+
logger.info("Successfully retrieved completeness_score from generator")
|
766 |
+
except Exception as e:
|
767 |
+
logger.error(f"Completeness score error from generator: {str(e)}")
|
768 |
+
|
769 |
+
# If completeness_score is None or doesn't have field_checklist, use comprehensive one
|
770 |
+
if completeness_score is None or not isinstance(completeness_score, dict) or 'field_checklist' not in completeness_score:
|
771 |
+
logger.info("Using comprehensive completeness_score with field_checklist")
|
772 |
+
completeness_score = create_comprehensive_completeness_score(aibom)
|
773 |
+
|
774 |
+
# Ensure enhancement_report has the right structure
|
775 |
+
if enhancement_report is None:
|
776 |
+
enhancement_report = {
|
777 |
+
"ai_enhanced": False,
|
778 |
+
"ai_model": None,
|
779 |
+
"original_score": {"total_score": 0, "completeness_score": 0},
|
780 |
+
"final_score": {"total_score": 0, "completeness_score": 0},
|
781 |
+
"improvement": 0
|
782 |
+
}
|
783 |
+
else:
|
784 |
+
# Ensure original_score has completeness_score
|
785 |
+
if "original_score" not in enhancement_report or enhancement_report["original_score"] is None:
|
786 |
+
enhancement_report["original_score"] = {"total_score": 0, "completeness_score": 0}
|
787 |
+
elif "completeness_score" not in enhancement_report["original_score"]:
|
788 |
+
enhancement_report["original_score"]["completeness_score"] = enhancement_report["original_score"].get("total_score", 0)
|
789 |
+
|
790 |
+
# Ensure final_score has completeness_score
|
791 |
+
if "final_score" not in enhancement_report or enhancement_report["final_score"] is None:
|
792 |
+
enhancement_report["final_score"] = {"total_score": 0, "completeness_score": 0}
|
793 |
+
elif "completeness_score" not in enhancement_report["final_score"]:
|
794 |
+
enhancement_report["final_score"]["completeness_score"] = enhancement_report["final_score"].get("total_score", 0)
|
795 |
+
|
796 |
+
# Add display names and tooltips for score sections
|
797 |
+
display_names = {
|
798 |
+
"required_fields": "Required Fields",
|
799 |
+
"metadata": "Metadata",
|
800 |
+
"component_basic": "Component Basic Info",
|
801 |
+
"component_model_card": "Model Card",
|
802 |
+
"external_references": "External References"
|
803 |
+
}
|
804 |
+
|
805 |
+
tooltips = {
|
806 |
+
"required_fields": "Basic required fields for a valid AIBOM",
|
807 |
+
"metadata": "Information about the AIBOM itself",
|
808 |
+
"component_basic": "Basic information about the AI model component",
|
809 |
+
"component_model_card": "Detailed model card information",
|
810 |
+
"external_references": "Links to external resources"
|
811 |
+
}
|
812 |
+
|
813 |
+
weights = {
|
814 |
+
"required_fields": 20,
|
815 |
+
"metadata": 20,
|
816 |
+
"component_basic": 20,
|
817 |
+
"component_model_card": 30,
|
818 |
+
"external_references": 10
|
819 |
+
}
|
820 |
+
|
821 |
+
# Render the template with all necessary data, with normalized model ID
|
822 |
+
return templates.TemplateResponse(
|
823 |
+
"result.html",
|
824 |
+
{
|
825 |
+
"request": request,
|
826 |
+
"model_id": normalized_model_id,
|
827 |
+
"aibom": aibom,
|
828 |
+
"enhancement_report": enhancement_report,
|
829 |
+
"completeness_score": completeness_score,
|
830 |
+
"download_url": download_url,
|
831 |
+
"download_script": download_script,
|
832 |
+
"display_names": display_names,
|
833 |
+
"tooltips": tooltips,
|
834 |
+
"weights": weights,
|
835 |
+
"sbom_count": sbom_count,
|
836 |
+
"display_names": display_names,
|
837 |
+
"tooltips": tooltips,
|
838 |
+
"weights": weights
|
839 |
+
}
|
840 |
+
)
|
841 |
+
# --- Main Exception Handling ---
|
842 |
+
except Exception as e:
|
843 |
+
logger.error(f"Error generating AI SBOM: {str(e)}")
|
844 |
+
sbom_count = get_sbom_count() # Refresh count just in case
|
845 |
+
# Pass count, added normalized model ID
|
846 |
+
return templates.TemplateResponse(
|
847 |
+
"error.html", {"request": request, "error": str(e), "sbom_count": sbom_count, "model_id": normalized_model_id}
|
848 |
+
)
|
849 |
+
|
850 |
+
@app.get("/download/{filename}")
|
851 |
+
async def download_file(filename: str):
|
852 |
+
"""
|
853 |
+
Download a generated AIBOM file.
|
854 |
+
|
855 |
+
This endpoint serves the generated AIBOM JSON files for download.
|
856 |
+
"""
|
857 |
+
file_path = os.path.join(OUTPUT_DIR, filename)
|
858 |
+
if not os.path.exists(file_path):
|
859 |
+
raise HTTPException(status_code=404, detail="File not found")
|
860 |
+
|
861 |
+
return FileResponse(
|
862 |
+
file_path,
|
863 |
+
media_type="application/json",
|
864 |
+
filename=filename
|
865 |
+
)
|
866 |
+
|
867 |
+
# Request model for JSON API
|
868 |
+
class GenerateRequest(BaseModel):
|
869 |
+
model_id: str
|
870 |
+
include_inference: bool = True
|
871 |
+
use_best_practices: bool = True
|
872 |
+
hf_token: Optional[str] = None
|
873 |
+
|
874 |
+
@app.post("/api/generate")
|
875 |
+
async def api_generate_aibom(request: GenerateRequest):
|
876 |
+
"""
|
877 |
+
Generate an AI SBOM for a specified Hugging Face model.
|
878 |
+
|
879 |
+
This endpoint accepts JSON input and returns JSON output.
|
880 |
+
"""
|
881 |
+
try:
|
882 |
+
# Sanitize and validate input
|
883 |
+
sanitized_model_id = html.escape(request.model_id)
|
884 |
+
if not is_valid_hf_input(sanitized_model_id):
|
885 |
+
raise HTTPException(status_code=400, detail="Invalid model ID format")
|
886 |
+
|
887 |
+
normalized_model_id = _normalise_model_id(sanitized_model_id)
|
888 |
+
|
889 |
+
# Verify model exists
|
890 |
+
try:
|
891 |
+
hf_api = HfApi()
|
892 |
+
model_info = hf_api.model_info(normalized_model_id)
|
893 |
+
except RepositoryNotFoundError:
|
894 |
+
raise HTTPException(status_code=404, detail=f"Model {normalized_model_id} not found on Hugging Face")
|
895 |
+
except Exception as api_err:
|
896 |
+
raise HTTPException(status_code=500, detail=f"Error verifying model: {str(api_err)}")
|
897 |
+
|
898 |
+
# Generate AIBOM
|
899 |
+
try:
|
900 |
+
# Try different import paths for AIBOMGenerator
|
901 |
+
generator = None
|
902 |
+
try:
|
903 |
+
from src.aibom_generator.generator import AIBOMGenerator
|
904 |
+
generator = AIBOMGenerator()
|
905 |
+
except ImportError:
|
906 |
+
try:
|
907 |
+
from aibom_generator.generator import AIBOMGenerator
|
908 |
+
generator = AIBOMGenerator()
|
909 |
+
except ImportError:
|
910 |
+
try:
|
911 |
+
from generator import AIBOMGenerator
|
912 |
+
generator = AIBOMGenerator()
|
913 |
+
except ImportError:
|
914 |
+
raise HTTPException(status_code=500, detail="Could not import AIBOMGenerator")
|
915 |
+
|
916 |
+
aibom = generator.generate_aibom(
|
917 |
+
model_id=sanitized_model_id,
|
918 |
+
include_inference=request.include_inference,
|
919 |
+
use_best_practices=request.use_best_practices
|
920 |
+
)
|
921 |
+
enhancement_report = generator.get_enhancement_report()
|
922 |
+
|
923 |
+
# Save AIBOM to file
|
924 |
+
filename = f"{normalized_model_id.replace('/', '_')}_ai_sbom.json"
|
925 |
+
filepath = os.path.join(OUTPUT_DIR, filename)
|
926 |
+
with open(filepath, "w") as f:
|
927 |
+
json.dump(aibom, f, indent=2)
|
928 |
+
|
929 |
+
# Log generation
|
930 |
+
log_sbom_generation(sanitized_model_id)
|
931 |
+
|
932 |
+
# Return JSON response
|
933 |
+
return {
|
934 |
+
"aibom": aibom,
|
935 |
+
"model_id": normalized_model_id,
|
936 |
+
"generated_at": datetime.utcnow().isoformat() + "Z",
|
937 |
+
"request_id": str(uuid.uuid4()),
|
938 |
+
"download_url": f"/output/{filename}"
|
939 |
+
}
|
940 |
+
except HTTPException:
|
941 |
+
raise
|
942 |
+
except Exception as e:
|
943 |
+
raise HTTPException(status_code=500, detail=f"Error generating AI SBOM: {str(e)}")
|
944 |
+
except HTTPException:
|
945 |
+
raise
|
946 |
+
except Exception as e:
|
947 |
+
raise HTTPException(status_code=500, detail=f"Error generating AI SBOM: {str(e)}")
|
948 |
+
|
949 |
+
@app.post("/api/generate-with-report")
|
950 |
+
async def api_generate_with_report(request: GenerateRequest):
|
951 |
+
"""
|
952 |
+
Generate an AI SBOM with a completeness report.
|
953 |
+
This endpoint accepts JSON input and returns JSON output with completeness score.
|
954 |
+
"""
|
955 |
+
try:
|
956 |
+
# Sanitize and validate input
|
957 |
+
sanitized_model_id = html.escape(request.model_id)
|
958 |
+
if not is_valid_hf_input(sanitized_model_id):
|
959 |
+
raise HTTPException(status_code=400, detail="Invalid model ID format")
|
960 |
+
|
961 |
+
normalized_model_id = _normalise_model_id(sanitized_model_id)
|
962 |
+
|
963 |
+
# Verify model exists
|
964 |
+
try:
|
965 |
+
hf_api = HfApi()
|
966 |
+
model_info = hf_api.model_info(normalized_model_id)
|
967 |
+
except RepositoryNotFoundError:
|
968 |
+
raise HTTPException(status_code=404, detail=f"Model {normalized_model_id} not found on Hugging Face")
|
969 |
+
except Exception as api_err:
|
970 |
+
raise HTTPException(status_code=500, detail=f"Error verifying model: {str(api_err)}")
|
971 |
+
|
972 |
+
# Generate AIBOM
|
973 |
+
try:
|
974 |
+
# Try different import paths for AIBOMGenerator
|
975 |
+
generator = None
|
976 |
+
try:
|
977 |
+
from src.aibom_generator.generator import AIBOMGenerator
|
978 |
+
generator = AIBOMGenerator()
|
979 |
+
except ImportError:
|
980 |
+
try:
|
981 |
+
from aibom_generator.generator import AIBOMGenerator
|
982 |
+
generator = AIBOMGenerator()
|
983 |
+
except ImportError:
|
984 |
+
try:
|
985 |
+
from generator import AIBOMGenerator
|
986 |
+
generator = AIBOMGenerator()
|
987 |
+
except ImportError:
|
988 |
+
raise HTTPException(status_code=500, detail="Could not import AIBOMGenerator")
|
989 |
+
|
990 |
+
aibom = generator.generate_aibom(
|
991 |
+
model_id=sanitized_model_id,
|
992 |
+
include_inference=request.include_inference,
|
993 |
+
use_best_practices=request.use_best_practices
|
994 |
+
)
|
995 |
+
|
996 |
+
# Calculate completeness score
|
997 |
+
completeness_score = calculate_completeness_score(aibom, validate=True, use_best_practices=request.use_best_practices)
|
998 |
+
|
999 |
+
# Round only section_scores that aren't already rounded
|
1000 |
+
for section, score in completeness_score["section_scores"].items():
|
1001 |
+
if isinstance(score, float) and not score.is_integer():
|
1002 |
+
completeness_score["section_scores"][section] = round(score, 1)
|
1003 |
+
|
1004 |
+
# Convert field_checklist to machine-parseable format
|
1005 |
+
if "field_checklist" in completeness_score:
|
1006 |
+
machine_parseable_checklist = {}
|
1007 |
+
for field, value in completeness_score["field_checklist"].items():
|
1008 |
+
# Extract presence (✔/✘) and importance (★★★/★★/★)
|
1009 |
+
present = "present" if "✔" in value else "missing"
|
1010 |
+
|
1011 |
+
# Use field_tiers for importance since it's already machine-parseable
|
1012 |
+
importance = completeness_score["field_tiers"].get(field, "unknown")
|
1013 |
+
|
1014 |
+
# Create structured entry
|
1015 |
+
machine_parseable_checklist[field] = {
|
1016 |
+
"status": present,
|
1017 |
+
"importance": importance
|
1018 |
+
}
|
1019 |
+
|
1020 |
+
# Replace the original field_checklist with the machine-parseable version
|
1021 |
+
completeness_score["field_checklist"] = machine_parseable_checklist
|
1022 |
+
|
1023 |
+
# Remove field_tiers to avoid duplication (now incorporated in field_checklist)
|
1024 |
+
completeness_score.pop("field_tiers", None)
|
1025 |
+
|
1026 |
+
# Save AIBOM to file
|
1027 |
+
filename = f"{normalized_model_id.replace('/', '_')}_ai_sbom.json"
|
1028 |
+
filepath = os.path.join(OUTPUT_DIR, filename)
|
1029 |
+
with open(filepath, "w") as f:
|
1030 |
+
json.dump(aibom, f, indent=2)
|
1031 |
+
|
1032 |
+
# Log generation
|
1033 |
+
log_sbom_generation(sanitized_model_id)
|
1034 |
+
|
1035 |
+
# Return JSON response with improved completeness score
|
1036 |
+
return {
|
1037 |
+
"aibom": aibom,
|
1038 |
+
"model_id": normalized_model_id,
|
1039 |
+
"generated_at": datetime.utcnow().isoformat() + "Z",
|
1040 |
+
"request_id": str(uuid.uuid4()),
|
1041 |
+
"download_url": f"/output/{filename}",
|
1042 |
+
"completeness_score": completeness_score
|
1043 |
+
}
|
1044 |
+
except HTTPException:
|
1045 |
+
raise
|
1046 |
+
except Exception as e:
|
1047 |
+
raise HTTPException(status_code=500, detail=f"Error generating AI SBOM: {str(e)}")
|
1048 |
+
except HTTPException:
|
1049 |
+
raise
|
1050 |
+
except Exception as e:
|
1051 |
+
raise HTTPException(status_code=500, detail=f"Error generating AI SBOM: {str(e)}")
|
1052 |
+
|
1053 |
+
|
1054 |
+
@app.get("/api/models/{model_id:path}/score" )
|
1055 |
+
async def get_model_score(
|
1056 |
+
model_id: str,
|
1057 |
+
hf_token: Optional[str] = None,
|
1058 |
+
use_best_practices: bool = True
|
1059 |
+
):
|
1060 |
+
"""
|
1061 |
+
Get the completeness score for a model without generating a full AIBOM.
|
1062 |
+
"""
|
1063 |
+
try:
|
1064 |
+
# Sanitize and validate input
|
1065 |
+
sanitized_model_id = html.escape(model_id)
|
1066 |
+
if not is_valid_hf_input(sanitized_model_id):
|
1067 |
+
raise HTTPException(status_code=400, detail="Invalid model ID format")
|
1068 |
+
|
1069 |
+
normalized_model_id = _normalise_model_id(sanitized_model_id)
|
1070 |
+
|
1071 |
+
# Verify model exists
|
1072 |
+
try:
|
1073 |
+
hf_api = HfApi(token=hf_token)
|
1074 |
+
model_info = hf_api.model_info(normalized_model_id)
|
1075 |
+
except RepositoryNotFoundError:
|
1076 |
+
raise HTTPException(status_code=404, detail=f"Model {normalized_model_id} not found on Hugging Face")
|
1077 |
+
except Exception as api_err:
|
1078 |
+
raise HTTPException(status_code=500, detail=f"Error verifying model: {str(api_err)}")
|
1079 |
+
|
1080 |
+
# Generate minimal AIBOM for scoring
|
1081 |
+
try:
|
1082 |
+
# Try different import paths for AIBOMGenerator
|
1083 |
+
generator = None
|
1084 |
+
try:
|
1085 |
+
from src.aibom_generator.generator import AIBOMGenerator
|
1086 |
+
generator = AIBOMGenerator(hf_token=hf_token)
|
1087 |
+
except ImportError:
|
1088 |
+
try:
|
1089 |
+
from aibom_generator.generator import AIBOMGenerator
|
1090 |
+
generator = AIBOMGenerator(hf_token=hf_token)
|
1091 |
+
except ImportError:
|
1092 |
+
try:
|
1093 |
+
from generator import AIBOMGenerator
|
1094 |
+
generator = AIBOMGenerator(hf_token=hf_token)
|
1095 |
+
except ImportError:
|
1096 |
+
raise HTTPException(status_code=500, detail="Could not import AIBOMGenerator")
|
1097 |
+
|
1098 |
+
# Generate minimal AIBOM
|
1099 |
+
aibom = generator.generate_aibom(
|
1100 |
+
model_id=sanitized_model_id,
|
1101 |
+
include_inference=False, # No need for inference for just scoring
|
1102 |
+
use_best_practices=use_best_practices
|
1103 |
+
)
|
1104 |
+
|
1105 |
+
# Calculate score
|
1106 |
+
score = calculate_completeness_score(aibom, validate=True, use_best_practices=use_best_practices)
|
1107 |
+
|
1108 |
+
# Log SBOM generation for counting purposes
|
1109 |
+
log_sbom_generation(normalized_model_id)
|
1110 |
+
|
1111 |
+
# Round section scores for better readability
|
1112 |
+
for section, value in score["section_scores"].items():
|
1113 |
+
if isinstance(value, float) and not value.is_integer():
|
1114 |
+
score["section_scores"][section] = round(value, 1)
|
1115 |
+
|
1116 |
+
# Return score information
|
1117 |
+
return {
|
1118 |
+
"total_score": score["total_score"],
|
1119 |
+
"section_scores": score["section_scores"],
|
1120 |
+
"max_scores": score["max_scores"]
|
1121 |
+
}
|
1122 |
+
except Exception as e:
|
1123 |
+
raise HTTPException(status_code=500, detail=f"Error calculating model score: {str(e)}")
|
1124 |
+
except HTTPException:
|
1125 |
+
raise
|
1126 |
+
except Exception as e:
|
1127 |
+
raise HTTPException(status_code=500, detail=f"Error processing request: {str(e)}")
|
1128 |
+
|
1129 |
+
|
1130 |
+
# Batch request model
|
1131 |
+
class BatchRequest(BaseModel):
|
1132 |
+
model_ids: List[str]
|
1133 |
+
include_inference: bool = True
|
1134 |
+
use_best_practices: bool = True
|
1135 |
+
hf_token: Optional[str] = None
|
1136 |
+
|
1137 |
+
# In-memory storage for batch jobs
|
1138 |
+
batch_jobs = {}
|
1139 |
+
|
1140 |
+
@app.post("/api/batch")
|
1141 |
+
async def batch_generate(request: BatchRequest):
|
1142 |
+
"""
|
1143 |
+
Start a batch job to generate AIBOMs for multiple models.
|
1144 |
+
"""
|
1145 |
+
try:
|
1146 |
+
# Validate model IDs
|
1147 |
+
valid_model_ids = []
|
1148 |
+
for model_id in request.model_ids:
|
1149 |
+
sanitized_id = html.escape(model_id)
|
1150 |
+
if is_valid_hf_input(sanitized_id):
|
1151 |
+
valid_model_ids.append(sanitized_id)
|
1152 |
+
else:
|
1153 |
+
logger.warning(f"Skipping invalid model ID: {model_id}")
|
1154 |
+
|
1155 |
+
if not valid_model_ids:
|
1156 |
+
raise HTTPException(status_code=400, detail="No valid model IDs provided")
|
1157 |
+
|
1158 |
+
# Create job ID
|
1159 |
+
job_id = str(uuid.uuid4())
|
1160 |
+
created_at = datetime.utcnow()
|
1161 |
+
|
1162 |
+
# Store job information
|
1163 |
+
batch_jobs[job_id] = {
|
1164 |
+
"job_id": job_id,
|
1165 |
+
"status": "queued",
|
1166 |
+
"model_ids": valid_model_ids,
|
1167 |
+
"created_at": created_at.isoformat() + "Z",
|
1168 |
+
"completed": 0,
|
1169 |
+
"total": len(valid_model_ids),
|
1170 |
+
"results": {}
|
1171 |
+
}
|
1172 |
+
|
1173 |
+
# Would be best to start a background task here but for now marking it as "processing"
|
1174 |
+
batch_jobs[job_id]["status"] = "processing"
|
1175 |
+
|
1176 |
+
return {
|
1177 |
+
"job_id": job_id,
|
1178 |
+
"status": "queued",
|
1179 |
+
"model_ids": valid_model_ids,
|
1180 |
+
"created_at": created_at.isoformat() + "Z"
|
1181 |
+
}
|
1182 |
+
except HTTPException:
|
1183 |
+
raise
|
1184 |
+
except Exception as e:
|
1185 |
+
raise HTTPException(status_code=500, detail=f"Error creating batch job: {str(e)}")
|
1186 |
+
|
1187 |
+
@app.get("/api/batch/{job_id}")
|
1188 |
+
async def get_batch_status(job_id: str):
|
1189 |
+
"""
|
1190 |
+
Check the status of a batch job.
|
1191 |
+
"""
|
1192 |
+
if job_id not in batch_jobs:
|
1193 |
+
raise HTTPException(status_code=404, detail=f"Batch job {job_id} not found")
|
1194 |
+
|
1195 |
+
return batch_jobs[job_id]
|
1196 |
+
|
1197 |
+
|
1198 |
+
# If running directly (for local testing)
|
1199 |
+
if __name__ == "__main__":
|
1200 |
+
import uvicorn
|
1201 |
+
# Ensure HF_TOKEN is set for local testing if needed
|
1202 |
+
if not HF_TOKEN:
|
1203 |
+
print("Warning: HF_TOKEN environment variable not set. SBOM count will show N/A and logging will be skipped.")
|
1204 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
src/aibom-generator/auth.py
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import Security, HTTPException, Depends
|
2 |
+
from fastapi.security.api_key import APIKeyHeader
|
3 |
+
import os
|
4 |
+
import logging
|
5 |
+
|
6 |
+
logger = logging.getLogger(__name__)
|
7 |
+
|
8 |
+
API_KEY_NAME = "X-API-Key"
|
9 |
+
API_KEY = os.environ.get("API_KEY")
|
10 |
+
|
11 |
+
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
12 |
+
|
13 |
+
async def get_api_key(api_key_header: str = Security(api_key_header)):
|
14 |
+
if not API_KEY:
|
15 |
+
# If no API key is set, don't enforce authentication
|
16 |
+
logger.warning("API_KEY environment variable not set. API authentication disabled.")
|
17 |
+
return None
|
18 |
+
|
19 |
+
if api_key_header == API_KEY:
|
20 |
+
return api_key_header
|
21 |
+
|
22 |
+
logger.warning(f"Invalid API key attempt")
|
23 |
+
raise HTTPException(status_code=403, detail="Invalid API Key")
|
src/aibom-generator/captcha.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import requests
|
3 |
+
import logging
|
4 |
+
from typing import Optional
|
5 |
+
|
6 |
+
logger = logging.getLogger(__name__ )
|
7 |
+
|
8 |
+
# Get the secret key from environment variable
|
9 |
+
RECAPTCHA_SECRET_KEY = os.environ.get("RECAPTCHA_SECRET_KEY")
|
10 |
+
|
11 |
+
def verify_recaptcha(response_token: Optional[str]) -> bool:
|
12 |
+
# LOGGING: Log the token start
|
13 |
+
logger.info(f"Starting reCAPTCHA verification with token: {response_token[:10]}..." if response_token else "None")
|
14 |
+
|
15 |
+
# Check if secret key is set
|
16 |
+
secret_key = os.environ.get("RECAPTCHA_SECRET_KEY")
|
17 |
+
if not secret_key:
|
18 |
+
logger.warning("RECAPTCHA_SECRET_KEY not set, bypassing verification")
|
19 |
+
return True
|
20 |
+
else:
|
21 |
+
# LOGGING: Log that secret key is set
|
22 |
+
logger.info("RECAPTCHA_SECRET_KEY is set (not showing for security)")
|
23 |
+
|
24 |
+
# If no token provided, verification fails
|
25 |
+
if not response_token:
|
26 |
+
logger.warning("No reCAPTCHA response token provided")
|
27 |
+
return False
|
28 |
+
|
29 |
+
try:
|
30 |
+
# LOGGING: Log before making request
|
31 |
+
logger.info("Sending verification request to Google reCAPTCHA API")
|
32 |
+
verification_response = requests.post(
|
33 |
+
"https://www.google.com/recaptcha/api/siteverify",
|
34 |
+
data={
|
35 |
+
"secret": secret_key,
|
36 |
+
"response": response_token
|
37 |
+
}
|
38 |
+
)
|
39 |
+
|
40 |
+
result = verification_response.json()
|
41 |
+
# LOGGING: Log the complete result from Google
|
42 |
+
logger.info(f"reCAPTCHA verification result: {result}")
|
43 |
+
|
44 |
+
if result.get("success"):
|
45 |
+
logger.info("reCAPTCHA verification successful")
|
46 |
+
return True
|
47 |
+
else:
|
48 |
+
# LOGGING: Log the specific error codes
|
49 |
+
logger.warning(f"reCAPTCHA verification failed: {result.get('error-codes', [])}")
|
50 |
+
return False
|
51 |
+
except Exception as e:
|
52 |
+
# LOGGING: Log any exceptions
|
53 |
+
logger.error(f"Error verifying reCAPTCHA: {str(e)}")
|
54 |
+
return False
|
55 |
+
|
src/aibom-generator/cleanup_utils.py
ADDED
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import logging
|
3 |
+
from datetime import datetime, timedelta
|
4 |
+
|
5 |
+
logger = logging.getLogger(__name__)
|
6 |
+
|
7 |
+
def cleanup_old_files(directory, max_age_days=7):
|
8 |
+
"""Remove files older than max_age_days from the specified directory."""
|
9 |
+
if not os.path.exists(directory):
|
10 |
+
logger.warning(f"Directory does not exist: {directory}")
|
11 |
+
return 0
|
12 |
+
|
13 |
+
removed_count = 0
|
14 |
+
now = datetime.now()
|
15 |
+
|
16 |
+
try:
|
17 |
+
for filename in os.listdir(directory):
|
18 |
+
file_path = os.path.join(directory, filename)
|
19 |
+
if os.path.isfile(file_path):
|
20 |
+
file_age = now - datetime.fromtimestamp(os.path.getmtime(file_path))
|
21 |
+
if file_age.days > max_age_days:
|
22 |
+
try:
|
23 |
+
os.remove(file_path)
|
24 |
+
removed_count += 1
|
25 |
+
logger.info(f"Removed old file: {file_path}")
|
26 |
+
except Exception as e:
|
27 |
+
logger.error(f"Error removing file {file_path}: {e}")
|
28 |
+
|
29 |
+
logger.info(f"Cleanup completed: removed {removed_count} files older than {max_age_days} days from {directory}")
|
30 |
+
return removed_count
|
31 |
+
except Exception as e:
|
32 |
+
logger.error(f"Error during cleanup of directory {directory}: {e}")
|
33 |
+
return 0
|
34 |
+
|
35 |
+
def limit_file_count(directory, max_files=1000):
|
36 |
+
"""Ensure no more than max_files are kept in the directory (removes oldest first)."""
|
37 |
+
if not os.path.exists(directory):
|
38 |
+
logger.warning(f"Directory does not exist: {directory}")
|
39 |
+
return 0
|
40 |
+
|
41 |
+
removed_count = 0
|
42 |
+
|
43 |
+
try:
|
44 |
+
files = []
|
45 |
+
for filename in os.listdir(directory):
|
46 |
+
file_path = os.path.join(directory, filename)
|
47 |
+
if os.path.isfile(file_path):
|
48 |
+
files.append((file_path, os.path.getmtime(file_path)))
|
49 |
+
|
50 |
+
# Sort by modification time (oldest first)
|
51 |
+
files.sort(key=lambda x: x[1])
|
52 |
+
|
53 |
+
# Remove oldest files if we exceed the limit
|
54 |
+
files_to_remove = files[:-max_files] if len(files) > max_files else []
|
55 |
+
|
56 |
+
for file_path, _ in files_to_remove:
|
57 |
+
try:
|
58 |
+
os.remove(file_path)
|
59 |
+
removed_count += 1
|
60 |
+
logger.info(f"Removed excess file: {file_path}")
|
61 |
+
except Exception as e:
|
62 |
+
logger.error(f"Error removing file {file_path}: {e}")
|
63 |
+
|
64 |
+
logger.info(f"File count limit enforced: removed {removed_count} oldest files from {directory}, keeping max {max_files}")
|
65 |
+
return removed_count
|
66 |
+
except Exception as e:
|
67 |
+
logger.error(f"Error during file count limiting in directory {directory}: {e}")
|
68 |
+
return 0
|
69 |
+
|
70 |
+
def perform_cleanup(directory, max_age_days=7, max_files=1000):
|
71 |
+
"""Perform both time-based and count-based cleanup."""
|
72 |
+
time_removed = cleanup_old_files(directory, max_age_days)
|
73 |
+
count_removed = limit_file_count(directory, max_files)
|
74 |
+
return time_removed + count_removed
|
src/aibom-generator/cli.py
ADDED
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
CLI interface for the AIBOM Generator.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import argparse
|
6 |
+
import json
|
7 |
+
import os
|
8 |
+
import sys
|
9 |
+
from typing import Optional
|
10 |
+
|
11 |
+
from aibom_generator.generator import AIBOMGenerator
|
12 |
+
|
13 |
+
|
14 |
+
def parse_args():
|
15 |
+
"""Parse command line arguments."""
|
16 |
+
parser = argparse.ArgumentParser(
|
17 |
+
description="Generate AI Bills of Materials (AIBOMs) in CycloneDX format for Hugging Face models."
|
18 |
+
)
|
19 |
+
|
20 |
+
parser.add_argument(
|
21 |
+
"model_id",
|
22 |
+
help="Hugging Face model ID (e.g., 'google/bert-base-uncased')"
|
23 |
+
)
|
24 |
+
|
25 |
+
parser.add_argument(
|
26 |
+
"-o", "--output",
|
27 |
+
help="Output file path (default: <model_id>.aibom.json)",
|
28 |
+
default=None
|
29 |
+
)
|
30 |
+
|
31 |
+
parser.add_argument(
|
32 |
+
"--token",
|
33 |
+
help="Hugging Face API token for accessing private models",
|
34 |
+
default=os.environ.get("HF_TOKEN")
|
35 |
+
)
|
36 |
+
|
37 |
+
parser.add_argument(
|
38 |
+
"--inference-url",
|
39 |
+
help="URL of the inference model service for metadata extraction",
|
40 |
+
default=os.environ.get("AIBOM_INFERENCE_URL")
|
41 |
+
)
|
42 |
+
|
43 |
+
parser.add_argument(
|
44 |
+
"--no-inference",
|
45 |
+
help="Disable inference model for metadata extraction",
|
46 |
+
action="store_true"
|
47 |
+
)
|
48 |
+
|
49 |
+
parser.add_argument(
|
50 |
+
"--cache-dir",
|
51 |
+
help="Directory to cache API responses and model cards",
|
52 |
+
default=os.environ.get("AIBOM_CACHE_DIR", ".aibom_cache")
|
53 |
+
)
|
54 |
+
|
55 |
+
parser.add_argument(
|
56 |
+
"--completeness-threshold",
|
57 |
+
help="Minimum completeness score (0-100) required for the AIBOM",
|
58 |
+
type=int,
|
59 |
+
default=0
|
60 |
+
)
|
61 |
+
|
62 |
+
parser.add_argument(
|
63 |
+
"--format",
|
64 |
+
help="Output format (json or yaml)",
|
65 |
+
choices=["json", "yaml"],
|
66 |
+
default="json"
|
67 |
+
)
|
68 |
+
|
69 |
+
parser.add_argument(
|
70 |
+
"--pretty",
|
71 |
+
help="Pretty-print the output",
|
72 |
+
action="store_true"
|
73 |
+
)
|
74 |
+
|
75 |
+
return parser.parse_args()
|
76 |
+
|
77 |
+
|
78 |
+
def main():
|
79 |
+
"""Main entry point for the CLI."""
|
80 |
+
args = parse_args()
|
81 |
+
|
82 |
+
# Determine output file if not specified
|
83 |
+
if not args.output:
|
84 |
+
model_name = args.model_id.replace("/", "_")
|
85 |
+
args.output = f"{model_name}.aibom.json"
|
86 |
+
|
87 |
+
# Create the generator
|
88 |
+
generator = AIBOMGenerator(
|
89 |
+
hf_token=args.token,
|
90 |
+
inference_model_url=args.inference_url,
|
91 |
+
use_inference=not args.no_inference,
|
92 |
+
cache_dir=args.cache_dir
|
93 |
+
)
|
94 |
+
|
95 |
+
try:
|
96 |
+
# Generate the AIBOM
|
97 |
+
aibom = generator.generate_aibom(
|
98 |
+
model_id=args.model_id,
|
99 |
+
output_file=None # We'll handle saving ourselves
|
100 |
+
)
|
101 |
+
|
102 |
+
# Calculate completeness score (placeholder for now)
|
103 |
+
completeness_score = calculate_completeness_score(aibom)
|
104 |
+
|
105 |
+
# Check if it meets the threshold
|
106 |
+
if completeness_score < args.completeness_threshold:
|
107 |
+
print(f"Warning: AIBOM completeness score ({completeness_score}) is below threshold ({args.completeness_threshold})")
|
108 |
+
|
109 |
+
# Save the output
|
110 |
+
save_output(aibom, args.output, args.format, args.pretty)
|
111 |
+
|
112 |
+
print(f"AIBOM generated successfully: {args.output}")
|
113 |
+
print(f"Completeness score: {completeness_score}/100")
|
114 |
+
|
115 |
+
return 0
|
116 |
+
|
117 |
+
except Exception as e:
|
118 |
+
print(f"Error generating AIBOM: {e}", file=sys.stderr)
|
119 |
+
return 1
|
120 |
+
|
121 |
+
|
122 |
+
def calculate_completeness_score(aibom):
|
123 |
+
"""
|
124 |
+
Calculate a completeness score for the AIBOM.
|
125 |
+
|
126 |
+
This is a placeholder implementation that will be replaced with a more
|
127 |
+
sophisticated scoring algorithm based on the field mapping framework.
|
128 |
+
"""
|
129 |
+
# TODO: Implement proper completeness scoring
|
130 |
+
score = 0
|
131 |
+
|
132 |
+
# Check required fields
|
133 |
+
if all(field in aibom for field in ["bomFormat", "specVersion", "serialNumber", "version"]):
|
134 |
+
score += 20
|
135 |
+
|
136 |
+
# Check metadata
|
137 |
+
if "metadata" in aibom:
|
138 |
+
metadata = aibom["metadata"]
|
139 |
+
if "timestamp" in metadata:
|
140 |
+
score += 5
|
141 |
+
if "tools" in metadata and metadata["tools"]:
|
142 |
+
score += 5
|
143 |
+
if "authors" in metadata and metadata["authors"]:
|
144 |
+
score += 5
|
145 |
+
if "component" in metadata:
|
146 |
+
score += 5
|
147 |
+
|
148 |
+
# Check components
|
149 |
+
if "components" in aibom and aibom["components"]:
|
150 |
+
component = aibom["components"][0]
|
151 |
+
if "type" in component and component["type"] == "machine-learning-model":
|
152 |
+
score += 10
|
153 |
+
if "name" in component:
|
154 |
+
score += 5
|
155 |
+
if "bom-ref" in component:
|
156 |
+
score += 5
|
157 |
+
if "licenses" in component:
|
158 |
+
score += 5
|
159 |
+
if "externalReferences" in component:
|
160 |
+
score += 5
|
161 |
+
if "modelCard" in component:
|
162 |
+
model_card = component["modelCard"]
|
163 |
+
if "modelParameters" in model_card:
|
164 |
+
score += 10
|
165 |
+
if "quantitativeAnalysis" in model_card:
|
166 |
+
score += 10
|
167 |
+
if "considerations" in model_card:
|
168 |
+
score += 10
|
169 |
+
|
170 |
+
return score
|
171 |
+
|
172 |
+
|
173 |
+
def save_output(aibom, output_file, format_type, pretty):
|
174 |
+
"""Save the AIBOM to the specified output file."""
|
175 |
+
if format_type == "json":
|
176 |
+
with open(output_file, "w") as f:
|
177 |
+
if pretty:
|
178 |
+
json.dump(aibom, f, indent=2)
|
179 |
+
else:
|
180 |
+
json.dump(aibom, f)
|
181 |
+
else: # yaml
|
182 |
+
try:
|
183 |
+
import yaml
|
184 |
+
with open(output_file, "w") as f:
|
185 |
+
yaml.dump(aibom, f, default_flow_style=False)
|
186 |
+
except ImportError:
|
187 |
+
print("Warning: PyYAML not installed. Falling back to JSON format.")
|
188 |
+
with open(output_file, "w") as f:
|
189 |
+
json.dump(aibom, f, indent=2 if pretty else None)
|
190 |
+
|
191 |
+
|
192 |
+
if __name__ == "__main__":
|
193 |
+
sys.exit(main())
|
src/aibom-generator/generator.py
ADDED
@@ -0,0 +1,611 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import uuid
|
3 |
+
import datetime
|
4 |
+
from typing import Dict, Optional, Any, List
|
5 |
+
|
6 |
+
|
7 |
+
from huggingface_hub import HfApi, ModelCard
|
8 |
+
from urllib.parse import urlparse
|
9 |
+
from .utils import calculate_completeness_score
|
10 |
+
|
11 |
+
|
12 |
+
class AIBOMGenerator:
|
13 |
+
def __init__(
|
14 |
+
self,
|
15 |
+
hf_token: Optional[str] = None,
|
16 |
+
inference_model_url: Optional[str] = None,
|
17 |
+
use_inference: bool = True,
|
18 |
+
cache_dir: Optional[str] = None,
|
19 |
+
use_best_practices: bool = True, # Added parameter for industry-neutral scoring
|
20 |
+
):
|
21 |
+
self.hf_api = HfApi(token=hf_token)
|
22 |
+
self.inference_model_url = inference_model_url
|
23 |
+
self.use_inference = use_inference
|
24 |
+
self.cache_dir = cache_dir
|
25 |
+
self.enhancement_report = None # Store enhancement report as instance variable
|
26 |
+
self.use_best_practices = use_best_practices # Store best practices flag
|
27 |
+
|
28 |
+
def generate_aibom(
|
29 |
+
self,
|
30 |
+
model_id: str,
|
31 |
+
output_file: Optional[str] = None,
|
32 |
+
include_inference: Optional[bool] = None,
|
33 |
+
use_best_practices: Optional[bool] = None, # Added parameter for industry-neutral scoring
|
34 |
+
) -> Dict[str, Any]:
|
35 |
+
try:
|
36 |
+
model_id = self._normalise_model_id(model_id)
|
37 |
+
use_inference = include_inference if include_inference is not None else self.use_inference
|
38 |
+
# Use method parameter if provided, otherwise use instance variable
|
39 |
+
use_best_practices = use_best_practices if use_best_practices is not None else self.use_best_practices
|
40 |
+
|
41 |
+
model_info = self._fetch_model_info(model_id)
|
42 |
+
model_card = self._fetch_model_card(model_id)
|
43 |
+
|
44 |
+
# Store original metadata before any AI enhancement
|
45 |
+
original_metadata = self._extract_structured_metadata(model_id, model_info, model_card)
|
46 |
+
|
47 |
+
# Create initial AIBOM with original metadata
|
48 |
+
original_aibom = self._create_aibom_structure(model_id, original_metadata)
|
49 |
+
|
50 |
+
# Calculate initial score with industry-neutral approach if enabled
|
51 |
+
original_score = calculate_completeness_score(original_aibom, validate=True, use_best_practices=use_best_practices)
|
52 |
+
|
53 |
+
# Final metadata starts with original metadata
|
54 |
+
final_metadata = original_metadata.copy() if original_metadata else {}
|
55 |
+
|
56 |
+
# Apply AI enhancement if requested
|
57 |
+
ai_enhanced = False
|
58 |
+
ai_model_name = None
|
59 |
+
|
60 |
+
if use_inference and self.inference_model_url:
|
61 |
+
try:
|
62 |
+
# Extract additional metadata using AI
|
63 |
+
enhanced_metadata = self._extract_unstructured_metadata(model_card, model_id)
|
64 |
+
|
65 |
+
# If we got enhanced metadata, merge it with original
|
66 |
+
if enhanced_metadata:
|
67 |
+
ai_enhanced = True
|
68 |
+
ai_model_name = "BERT-base-uncased" # Will be replaced with actual model name
|
69 |
+
|
70 |
+
# Merge enhanced metadata with original (enhanced takes precedence)
|
71 |
+
for key, value in enhanced_metadata.items():
|
72 |
+
if value is not None and (key not in final_metadata or not final_metadata[key]):
|
73 |
+
final_metadata[key] = value
|
74 |
+
except Exception as e:
|
75 |
+
print(f"Error during AI enhancement: {e}")
|
76 |
+
# Continue with original metadata if enhancement fails
|
77 |
+
|
78 |
+
# Create final AIBOM with potentially enhanced metadata
|
79 |
+
aibom = self._create_aibom_structure(model_id, final_metadata)
|
80 |
+
|
81 |
+
# Calculate final score with industry-neutral approach if enabled
|
82 |
+
final_score = calculate_completeness_score(aibom, validate=True, use_best_practices=use_best_practices)
|
83 |
+
|
84 |
+
# Ensure metadata.properties exists
|
85 |
+
if "metadata" in aibom and "properties" not in aibom["metadata"]:
|
86 |
+
aibom["metadata"]["properties"] = []
|
87 |
+
|
88 |
+
# Note: Quality score information is no longer added to the AIBOM metadata
|
89 |
+
# This was removed as requested by the user
|
90 |
+
|
91 |
+
if output_file:
|
92 |
+
with open(output_file, 'w') as f:
|
93 |
+
json.dump(aibom, f, indent=2)
|
94 |
+
|
95 |
+
# Create enhancement report for UI display and store as instance variable
|
96 |
+
self.enhancement_report = {
|
97 |
+
"ai_enhanced": ai_enhanced,
|
98 |
+
"ai_model": ai_model_name if ai_enhanced else None,
|
99 |
+
"original_score": original_score,
|
100 |
+
"final_score": final_score,
|
101 |
+
"improvement": round(final_score["total_score"] - original_score["total_score"], 2) if ai_enhanced else 0
|
102 |
+
}
|
103 |
+
|
104 |
+
# Return only the AIBOM to maintain compatibility with existing code
|
105 |
+
return aibom
|
106 |
+
except Exception as e:
|
107 |
+
print(f"Error generating AIBOM: {e}")
|
108 |
+
# Return a minimal valid AIBOM structure in case of error
|
109 |
+
return self._create_minimal_aibom(model_id)
|
110 |
+
|
111 |
+
def _create_minimal_aibom(self, model_id: str) -> Dict[str, Any]:
|
112 |
+
"""Create a minimal valid AIBOM structure in case of errors"""
|
113 |
+
return {
|
114 |
+
"bomFormat": "CycloneDX",
|
115 |
+
"specVersion": "1.6",
|
116 |
+
"serialNumber": f"urn:uuid:{str(uuid.uuid4())}",
|
117 |
+
"version": 1,
|
118 |
+
"metadata": {
|
119 |
+
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
|
120 |
+
"tools": {
|
121 |
+
"components": [{
|
122 |
+
"bom-ref": "pkg:generic/aetheris-ai/[email protected]",
|
123 |
+
"type": "application",
|
124 |
+
"name": "aetheris-aibom-generator",
|
125 |
+
"version": "1.0.0",
|
126 |
+
"manufacturer": {
|
127 |
+
"name": "Aetheris AI"
|
128 |
+
}
|
129 |
+
}]
|
130 |
+
},
|
131 |
+
"component": {
|
132 |
+
"bom-ref": f"pkg:generic/{model_id.replace('/', '%2F')}@1.0",
|
133 |
+
"type": "application",
|
134 |
+
"name": model_id.split("/")[-1],
|
135 |
+
"description": f"AI model {model_id}",
|
136 |
+
"version": "1.0",
|
137 |
+
"purl": f"pkg:generic/{model_id.replace('/', '%2F')}@1.0",
|
138 |
+
"copyright": "NOASSERTION"
|
139 |
+
}
|
140 |
+
},
|
141 |
+
"components": [{
|
142 |
+
"bom-ref": f"pkg:huggingface/{model_id.replace('/', '/')}@1.0",
|
143 |
+
"type": "machine-learning-model",
|
144 |
+
"name": model_id.split("/")[-1],
|
145 |
+
"version": "1.0",
|
146 |
+
"purl": f"pkg:huggingface/{model_id.replace('/', '/')}@1.0"
|
147 |
+
}],
|
148 |
+
"dependencies": [{
|
149 |
+
"ref": f"pkg:generic/{model_id.replace('/', '%2F')}@1.0",
|
150 |
+
"dependsOn": [f"pkg:huggingface/{model_id.replace('/', '/')}@1.0"]
|
151 |
+
}]
|
152 |
+
}
|
153 |
+
|
154 |
+
def get_enhancement_report(self):
|
155 |
+
"""Return the enhancement report from the last generate_aibom call"""
|
156 |
+
return self.enhancement_report
|
157 |
+
|
158 |
+
def _fetch_model_info(self, model_id: str) -> Dict[str, Any]:
|
159 |
+
try:
|
160 |
+
return self.hf_api.model_info(model_id)
|
161 |
+
except Exception as e:
|
162 |
+
print(f"Error fetching model info for {model_id}: {e}")
|
163 |
+
return {}
|
164 |
+
|
165 |
+
# ---- new helper ---------------------------------------------------------
|
166 |
+
@staticmethod
|
167 |
+
def _normalise_model_id(raw_id: str) -> str:
|
168 |
+
"""
|
169 |
+
Accept either 'owner/model' or a full URL like
|
170 |
+
'https://huggingface.co/owner/model'. Return 'owner/model'.
|
171 |
+
"""
|
172 |
+
if raw_id.startswith(("http://", "https://")):
|
173 |
+
path = urlparse(raw_id).path.lstrip("/")
|
174 |
+
# path can contain extra segments (e.g. /commit/...), keep first two
|
175 |
+
parts = path.split("/")
|
176 |
+
if len(parts) >= 2:
|
177 |
+
return "/".join(parts[:2])
|
178 |
+
return path
|
179 |
+
return raw_id
|
180 |
+
# -------------------------------------------------------------------------
|
181 |
+
|
182 |
+
def _fetch_model_card(self, model_id: str) -> Optional[ModelCard]:
|
183 |
+
try:
|
184 |
+
return ModelCard.load(model_id)
|
185 |
+
except Exception as e:
|
186 |
+
print(f"Error fetching model card for {model_id}: {e}")
|
187 |
+
return None
|
188 |
+
|
189 |
+
def _create_aibom_structure(
|
190 |
+
self,
|
191 |
+
model_id: str,
|
192 |
+
metadata: Dict[str, Any],
|
193 |
+
) -> Dict[str, Any]:
|
194 |
+
# Extract owner and model name from model_id
|
195 |
+
parts = model_id.split("/")
|
196 |
+
group = parts[0] if len(parts) > 1 else ""
|
197 |
+
name = parts[1] if len(parts) > 1 else parts[0]
|
198 |
+
|
199 |
+
# Get version from metadata or use default
|
200 |
+
version = metadata.get("commit", "1.0")
|
201 |
+
|
202 |
+
aibom = {
|
203 |
+
"bomFormat": "CycloneDX",
|
204 |
+
"specVersion": "1.6",
|
205 |
+
"serialNumber": f"urn:uuid:{str(uuid.uuid4())}",
|
206 |
+
"version": 1,
|
207 |
+
"metadata": self._create_metadata_section(model_id, metadata),
|
208 |
+
"components": [self._create_component_section(model_id, metadata)],
|
209 |
+
"dependencies": [
|
210 |
+
{
|
211 |
+
"ref": f"pkg:generic/{model_id.replace('/', '%2F')}@{version}",
|
212 |
+
"dependsOn": [f"pkg:huggingface/{model_id.replace('/', '/')}@{version}"]
|
213 |
+
}
|
214 |
+
]
|
215 |
+
}
|
216 |
+
|
217 |
+
# Add downloadLocation if available
|
218 |
+
if metadata and "commit_url" in metadata:
|
219 |
+
# Add external reference for downloadLocation
|
220 |
+
if "externalReferences" not in aibom:
|
221 |
+
aibom["externalReferences"] = []
|
222 |
+
|
223 |
+
aibom["externalReferences"].append({
|
224 |
+
"type": "distribution",
|
225 |
+
"url": f"https://huggingface.co/{model_id}"
|
226 |
+
})
|
227 |
+
|
228 |
+
return aibom
|
229 |
+
|
230 |
+
def _extract_structured_metadata(
|
231 |
+
self,
|
232 |
+
model_id: str,
|
233 |
+
model_info: Dict[str, Any],
|
234 |
+
model_card: Optional[ModelCard],
|
235 |
+
) -> Dict[str, Any]:
|
236 |
+
metadata = {}
|
237 |
+
|
238 |
+
if model_info:
|
239 |
+
try:
|
240 |
+
metadata.update({
|
241 |
+
"name": model_info.modelId.split("/")[-1] if hasattr(model_info, "modelId") else model_id.split("/")[-1],
|
242 |
+
"author": model_info.author if hasattr(model_info, "author") else None,
|
243 |
+
"tags": model_info.tags if hasattr(model_info, "tags") else [],
|
244 |
+
"pipeline_tag": model_info.pipeline_tag if hasattr(model_info, "pipeline_tag") else None,
|
245 |
+
"downloads": model_info.downloads if hasattr(model_info, "downloads") else 0,
|
246 |
+
"last_modified": model_info.lastModified if hasattr(model_info, "lastModified") else None,
|
247 |
+
"commit": model_info.sha[:7] if hasattr(model_info, "sha") and model_info.sha else None,
|
248 |
+
"commit_url": f"https://huggingface.co/{model_id}/commit/{model_info.sha}" if hasattr(model_info, "sha") and model_info.sha else None,
|
249 |
+
})
|
250 |
+
except Exception as e:
|
251 |
+
print(f"Error extracting model info metadata: {e}")
|
252 |
+
|
253 |
+
if model_card and hasattr(model_card, "data") and model_card.data:
|
254 |
+
try:
|
255 |
+
card_data = model_card.data.to_dict() if hasattr(model_card.data, "to_dict") else {}
|
256 |
+
metadata.update({
|
257 |
+
"language": card_data.get("language"),
|
258 |
+
"license": card_data.get("license"),
|
259 |
+
"library_name": card_data.get("library_name"),
|
260 |
+
"base_model": card_data.get("base_model"),
|
261 |
+
"datasets": card_data.get("datasets"),
|
262 |
+
"model_name": card_data.get("model_name"),
|
263 |
+
"tags": card_data.get("tags", metadata.get("tags", [])),
|
264 |
+
"description": card_data.get("model_summary", None)
|
265 |
+
})
|
266 |
+
if hasattr(model_card.data, "eval_results") and model_card.data.eval_results:
|
267 |
+
metadata["eval_results"] = model_card.data.eval_results
|
268 |
+
except Exception as e:
|
269 |
+
print(f"Error extracting model card metadata: {e}")
|
270 |
+
|
271 |
+
metadata["ai:type"] = "Transformer"
|
272 |
+
metadata["ai:task"] = metadata.get("pipeline_tag", "Text Generation")
|
273 |
+
metadata["ai:framework"] = "PyTorch" if "transformers" in metadata.get("library_name", "") else "Unknown"
|
274 |
+
|
275 |
+
# Add fields for industry-neutral scoring (silently aligned with SPDX)
|
276 |
+
metadata["primaryPurpose"] = metadata.get("ai:task", "Text Generation")
|
277 |
+
metadata["suppliedBy"] = metadata.get("author", "Unknown")
|
278 |
+
|
279 |
+
# Add typeOfModel field
|
280 |
+
metadata["typeOfModel"] = metadata.get("ai:type", "Transformer")
|
281 |
+
|
282 |
+
return {k: v for k, v in metadata.items() if v is not None}
|
283 |
+
|
284 |
+
def _extract_unstructured_metadata(self, model_card: Optional[ModelCard], model_id: str) -> Dict[str, Any]:
|
285 |
+
"""
|
286 |
+
Extract additional metadata from model card using BERT model.
|
287 |
+
This is a placeholder implementation that would be replaced with actual BERT inference.
|
288 |
+
|
289 |
+
In a real implementation, this would:
|
290 |
+
1. Extract text from model card
|
291 |
+
2. Use BERT to identify key information
|
292 |
+
3. Structure the extracted information
|
293 |
+
|
294 |
+
For now, we'll simulate this with some basic extraction logic.
|
295 |
+
"""
|
296 |
+
enhanced_metadata = {}
|
297 |
+
|
298 |
+
# In a real implementation, we would use a BERT model here
|
299 |
+
# Since we can't install the required libraries due to space constraints,
|
300 |
+
# we'll simulate the enhancement with a placeholder implementation
|
301 |
+
|
302 |
+
if model_card and hasattr(model_card, "text") and model_card.text:
|
303 |
+
try:
|
304 |
+
card_text = model_card.text
|
305 |
+
|
306 |
+
# Simulate BERT extraction with basic text analysis
|
307 |
+
# In reality, this would be done with NLP models
|
308 |
+
|
309 |
+
# Extract description if missing
|
310 |
+
if card_text and "description" not in enhanced_metadata:
|
311 |
+
# Take first paragraph that's longer than 20 chars as description
|
312 |
+
paragraphs = [p.strip() for p in card_text.split('\n\n')]
|
313 |
+
for p in paragraphs:
|
314 |
+
if len(p) > 20 and not p.startswith('#'):
|
315 |
+
enhanced_metadata["description"] = p
|
316 |
+
break
|
317 |
+
|
318 |
+
# Extract limitations if present
|
319 |
+
if "limitations" not in enhanced_metadata:
|
320 |
+
if "## Limitations" in card_text:
|
321 |
+
limitations_section = card_text.split("## Limitations")[1].split("##")[0].strip()
|
322 |
+
if limitations_section:
|
323 |
+
enhanced_metadata["limitations"] = limitations_section
|
324 |
+
|
325 |
+
# Extract ethical considerations if present
|
326 |
+
if "ethical_considerations" not in enhanced_metadata:
|
327 |
+
for heading in ["## Ethical Considerations", "## Ethics", "## Bias"]:
|
328 |
+
if heading in card_text:
|
329 |
+
section = card_text.split(heading)[1].split("##")[0].strip()
|
330 |
+
if section:
|
331 |
+
enhanced_metadata["ethical_considerations"] = section
|
332 |
+
break
|
333 |
+
|
334 |
+
# Extract risks if present
|
335 |
+
if "risks" not in enhanced_metadata:
|
336 |
+
if "## Risks" in card_text:
|
337 |
+
risks_section = card_text.split("## Risks")[1].split("##")[0].strip()
|
338 |
+
if risks_section:
|
339 |
+
enhanced_metadata["risks"] = risks_section
|
340 |
+
|
341 |
+
# Extract datasets if present
|
342 |
+
if "datasets" not in enhanced_metadata:
|
343 |
+
datasets = []
|
344 |
+
if "## Dataset" in card_text or "## Datasets" in card_text:
|
345 |
+
dataset_section = ""
|
346 |
+
if "## Dataset" in card_text:
|
347 |
+
dataset_section = card_text.split("## Dataset")[1].split("##")[0].strip()
|
348 |
+
elif "## Datasets" in card_text:
|
349 |
+
dataset_section = card_text.split("## Datasets")[1].split("##")[0].strip()
|
350 |
+
|
351 |
+
if dataset_section:
|
352 |
+
# Simple parsing to extract dataset names
|
353 |
+
lines = dataset_section.split("\n")
|
354 |
+
for line in lines:
|
355 |
+
if line.strip() and not line.startswith("#"):
|
356 |
+
datasets.append({
|
357 |
+
"type": "dataset",
|
358 |
+
"name": line.strip().split()[0] if line.strip().split() else "Unknown",
|
359 |
+
"description": line.strip()
|
360 |
+
})
|
361 |
+
|
362 |
+
if datasets:
|
363 |
+
enhanced_metadata["datasets"] = datasets
|
364 |
+
except Exception as e:
|
365 |
+
print(f"Error extracting unstructured metadata: {e}")
|
366 |
+
|
367 |
+
return enhanced_metadata
|
368 |
+
|
369 |
+
def _create_metadata_section(self, model_id: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
370 |
+
timestamp = datetime.datetime.utcnow().isoformat() + "Z"
|
371 |
+
|
372 |
+
# Get version from metadata or use default
|
373 |
+
version = metadata.get("commit", "1.0")
|
374 |
+
|
375 |
+
# Create tools section with components array
|
376 |
+
tools = {
|
377 |
+
"components": [{
|
378 |
+
"bom-ref": "pkg:generic/aetheris-ai/[email protected]",
|
379 |
+
"type": "application",
|
380 |
+
"name": "aetheris-aibom-generator",
|
381 |
+
"version": "1.0",
|
382 |
+
"manufacturer": {
|
383 |
+
"name": "Aetheris AI"
|
384 |
+
}
|
385 |
+
}]
|
386 |
+
}
|
387 |
+
|
388 |
+
# Create authors array
|
389 |
+
authors = []
|
390 |
+
if "author" in metadata and metadata["author"]:
|
391 |
+
authors.append({
|
392 |
+
"name": metadata["author"]
|
393 |
+
})
|
394 |
+
|
395 |
+
# Create component section for metadata
|
396 |
+
component = {
|
397 |
+
"bom-ref": f"pkg:generic/{model_id.replace('/', '%2F')}@{version}",
|
398 |
+
"type": "application",
|
399 |
+
"name": metadata.get("name", model_id.split("/")[-1]),
|
400 |
+
"description": metadata.get("description", f"AI model {model_id}"),
|
401 |
+
"version": version,
|
402 |
+
"purl": f"pkg:generic/{model_id.replace('/', '%2F')}@{version}"
|
403 |
+
}
|
404 |
+
|
405 |
+
# Add authors to component if available
|
406 |
+
if authors:
|
407 |
+
component["authors"] = authors
|
408 |
+
|
409 |
+
# Add publisher and supplier if author is available
|
410 |
+
if "author" in metadata and metadata["author"]:
|
411 |
+
component["publisher"] = metadata["author"]
|
412 |
+
component["supplier"] = {
|
413 |
+
"name": metadata["author"]
|
414 |
+
}
|
415 |
+
component["manufacturer"] = {
|
416 |
+
"name": metadata["author"]
|
417 |
+
}
|
418 |
+
|
419 |
+
# Add copyright
|
420 |
+
component["copyright"] = "NOASSERTION"
|
421 |
+
|
422 |
+
# Create properties array for additional metadata
|
423 |
+
properties = []
|
424 |
+
for key, value in metadata.items():
|
425 |
+
if key not in ["name", "author", "license", "description", "commit"] and value is not None:
|
426 |
+
if isinstance(value, (list, dict)):
|
427 |
+
if not isinstance(value, str):
|
428 |
+
value = json.dumps(value)
|
429 |
+
properties.append({"name": key, "value": str(value)})
|
430 |
+
|
431 |
+
# Assemble metadata section
|
432 |
+
metadata_section = {
|
433 |
+
"timestamp": timestamp,
|
434 |
+
"tools": tools,
|
435 |
+
"component": component
|
436 |
+
}
|
437 |
+
|
438 |
+
if properties:
|
439 |
+
metadata_section["properties"] = properties
|
440 |
+
|
441 |
+
return metadata_section
|
442 |
+
|
443 |
+
def _create_component_section(self, model_id: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
444 |
+
# Extract owner and model name from model_id
|
445 |
+
parts = model_id.split("/")
|
446 |
+
group = parts[0] if len(parts) > 1 else ""
|
447 |
+
name = parts[1] if len(parts) > 1 else parts[0]
|
448 |
+
|
449 |
+
# Get version from metadata or use default
|
450 |
+
version = metadata.get("commit", "1.0")
|
451 |
+
|
452 |
+
# Create PURL with version information if commit is available
|
453 |
+
purl = f"pkg:huggingface/{model_id.replace('/', '/')}"
|
454 |
+
if "commit" in metadata:
|
455 |
+
purl = f"{purl}@{metadata['commit']}"
|
456 |
+
else:
|
457 |
+
purl = f"{purl}@{version}"
|
458 |
+
|
459 |
+
component = {
|
460 |
+
"bom-ref": f"pkg:huggingface/{model_id.replace('/', '/')}@{version}",
|
461 |
+
"type": "machine-learning-model",
|
462 |
+
"group": group,
|
463 |
+
"name": name,
|
464 |
+
"version": version,
|
465 |
+
"purl": purl
|
466 |
+
}
|
467 |
+
|
468 |
+
# Add licenses if available
|
469 |
+
if "license" in metadata:
|
470 |
+
component["licenses"] = [{
|
471 |
+
"license": {
|
472 |
+
"id": metadata["license"],
|
473 |
+
"url": self._get_license_url(metadata["license"])
|
474 |
+
}
|
475 |
+
}]
|
476 |
+
|
477 |
+
# Add description if available
|
478 |
+
if "description" in metadata:
|
479 |
+
component["description"] = metadata["description"]
|
480 |
+
|
481 |
+
# Add external references
|
482 |
+
external_refs = [{
|
483 |
+
"type": "website",
|
484 |
+
"url": f"https://huggingface.co/{model_id}"
|
485 |
+
}]
|
486 |
+
if "commit_url" in metadata:
|
487 |
+
external_refs.append({
|
488 |
+
"type": "vcs",
|
489 |
+
"url": metadata["commit_url"]
|
490 |
+
})
|
491 |
+
component["externalReferences"] = external_refs
|
492 |
+
|
493 |
+
# Add authors, publisher, supplier, manufacturer
|
494 |
+
if "author" in metadata and metadata["author"]:
|
495 |
+
component["authors"] = [{"name": metadata["author"]}]
|
496 |
+
component["publisher"] = metadata["author"]
|
497 |
+
component["supplier"] = {
|
498 |
+
"name": metadata["author"],
|
499 |
+
"url": [f"https://huggingface.co/{metadata['author']}"]
|
500 |
+
}
|
501 |
+
component["manufacturer"] = {
|
502 |
+
"name": metadata["author"],
|
503 |
+
"url": [f"https://huggingface.co/{metadata['author']}"]
|
504 |
+
}
|
505 |
+
|
506 |
+
# Add copyright
|
507 |
+
component["copyright"] = "NOASSERTION"
|
508 |
+
|
509 |
+
# Add model card section
|
510 |
+
component["modelCard"] = self._create_model_card_section(metadata)
|
511 |
+
|
512 |
+
return component
|
513 |
+
|
514 |
+
def _create_model_card_section(self, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
515 |
+
model_card_section = {}
|
516 |
+
|
517 |
+
# Add quantitative analysis section
|
518 |
+
if "eval_results" in metadata:
|
519 |
+
model_card_section["quantitativeAnalysis"] = {
|
520 |
+
"performanceMetrics": metadata["eval_results"],
|
521 |
+
"graphics": {} # Empty graphics object as in the example
|
522 |
+
}
|
523 |
+
else:
|
524 |
+
model_card_section["quantitativeAnalysis"] = {"graphics": {}}
|
525 |
+
|
526 |
+
# Add properties section
|
527 |
+
properties = []
|
528 |
+
for key, value in metadata.items():
|
529 |
+
if key in ["author", "library_name", "license", "downloads", "likes", "tags", "created_at", "last_modified"]:
|
530 |
+
properties.append({"name": key, "value": str(value)})
|
531 |
+
|
532 |
+
if properties:
|
533 |
+
model_card_section["properties"] = properties
|
534 |
+
|
535 |
+
# Create model parameters section
|
536 |
+
model_parameters = {}
|
537 |
+
|
538 |
+
# Add outputs array
|
539 |
+
model_parameters["outputs"] = [{"format": "generated-text"}]
|
540 |
+
|
541 |
+
# Add task
|
542 |
+
model_parameters["task"] = metadata.get("pipeline_tag", "text-generation")
|
543 |
+
|
544 |
+
# Add architecture information
|
545 |
+
model_parameters["architectureFamily"] = "llama" if "llama" in metadata.get("name", "").lower() else "transformer"
|
546 |
+
model_parameters["modelArchitecture"] = f"{metadata.get('name', 'Unknown')}ForCausalLM"
|
547 |
+
|
548 |
+
# Add datasets array with proper structure
|
549 |
+
if "datasets" in metadata:
|
550 |
+
datasets = []
|
551 |
+
if isinstance(metadata["datasets"], list):
|
552 |
+
for dataset in metadata["datasets"]:
|
553 |
+
if isinstance(dataset, str):
|
554 |
+
datasets.append({
|
555 |
+
"type": "dataset",
|
556 |
+
"name": dataset,
|
557 |
+
"description": f"Dataset used for training {metadata.get('name', 'the model')}"
|
558 |
+
})
|
559 |
+
elif isinstance(dataset, dict) and "name" in dataset:
|
560 |
+
# Ensure dataset has the required structure
|
561 |
+
dataset_entry = {
|
562 |
+
"type": dataset.get("type", "dataset"),
|
563 |
+
"name": dataset["name"],
|
564 |
+
"description": dataset.get("description", f"Dataset: {dataset['name']}")
|
565 |
+
}
|
566 |
+
datasets.append(dataset_entry)
|
567 |
+
elif isinstance(metadata["datasets"], str):
|
568 |
+
datasets.append({
|
569 |
+
"type": "dataset",
|
570 |
+
"name": metadata["datasets"],
|
571 |
+
"description": f"Dataset used for training {metadata.get('name', 'the model')}"
|
572 |
+
})
|
573 |
+
|
574 |
+
if datasets:
|
575 |
+
model_parameters["datasets"] = datasets
|
576 |
+
|
577 |
+
# Add inputs array
|
578 |
+
model_parameters["inputs"] = [{"format": "text"}]
|
579 |
+
|
580 |
+
# Add model parameters to model card section
|
581 |
+
model_card_section["modelParameters"] = model_parameters
|
582 |
+
|
583 |
+
# Add considerations section
|
584 |
+
considerations = {}
|
585 |
+
for k in ["limitations", "ethical_considerations", "bias", "risks"]:
|
586 |
+
if k in metadata:
|
587 |
+
considerations[k] = metadata[k]
|
588 |
+
if considerations:
|
589 |
+
model_card_section["considerations"] = considerations
|
590 |
+
|
591 |
+
return model_card_section
|
592 |
+
|
593 |
+
def _get_license_url(self, license_id: str) -> str:
|
594 |
+
"""Get the URL for a license based on its SPDX ID."""
|
595 |
+
license_urls = {
|
596 |
+
"Apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0",
|
597 |
+
"MIT": "https://opensource.org/licenses/MIT",
|
598 |
+
"BSD-3-Clause": "https://opensource.org/licenses/BSD-3-Clause",
|
599 |
+
"GPL-3.0": "https://www.gnu.org/licenses/gpl-3.0.en.html",
|
600 |
+
"CC-BY-4.0": "https://creativecommons.org/licenses/by/4.0/",
|
601 |
+
"CC-BY-SA-4.0": "https://creativecommons.org/licenses/by-sa/4.0/",
|
602 |
+
"CC-BY-NC-4.0": "https://creativecommons.org/licenses/by-nc/4.0/",
|
603 |
+
"CC-BY-ND-4.0": "https://creativecommons.org/licenses/by-nd/4.0/",
|
604 |
+
"CC-BY-NC-SA-4.0": "https://creativecommons.org/licenses/by-nc-sa/4.0/",
|
605 |
+
"CC-BY-NC-ND-4.0": "https://creativecommons.org/licenses/by-nc-nd/4.0/",
|
606 |
+
"LGPL-3.0": "https://www.gnu.org/licenses/lgpl-3.0.en.html",
|
607 |
+
"MPL-2.0": "https://www.mozilla.org/en-US/MPL/2.0/",
|
608 |
+
}
|
609 |
+
|
610 |
+
return license_urls.get(license_id, "https://spdx.org/licenses/")
|
611 |
+
|
src/aibom-generator/improved_score_renderer.py
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
from typing import Dict, Optional, Any
|
3 |
+
from jinja2 import Template
|
4 |
+
|
5 |
+
def render_improved_score_template(model_id: str, aibom: Dict[str, Any], completeness_score: Dict[str, Any], enhancement_report: Optional[Dict[str, Any]] = None) -> str:
|
6 |
+
"""
|
7 |
+
Render the improved scoring HTML template with AIBOM data and enhancement information.
|
8 |
+
|
9 |
+
Args:
|
10 |
+
model_id: The Hugging Face model ID
|
11 |
+
aibom: The generated AIBOM data
|
12 |
+
completeness_score: The completeness score report
|
13 |
+
enhancement_report: Optional enhancement report with AI improvement information
|
14 |
+
|
15 |
+
Returns:
|
16 |
+
Rendered HTML content
|
17 |
+
"""
|
18 |
+
with open('/home/ubuntu/improved_scoring_template.html', 'r') as f:
|
19 |
+
template_str = f.read()
|
20 |
+
|
21 |
+
template = Template(template_str)
|
22 |
+
|
23 |
+
# Convert scores to percentages for progress bars
|
24 |
+
if completeness_score:
|
25 |
+
completeness_score['total_score'] = round(completeness_score.get('total_score', 0))
|
26 |
+
|
27 |
+
if enhancement_report and enhancement_report.get('original_score'):
|
28 |
+
enhancement_report['original_score']['total_score'] = round(enhancement_report['original_score'].get('total_score', 0))
|
29 |
+
|
30 |
+
if enhancement_report and enhancement_report.get('final_score'):
|
31 |
+
enhancement_report['final_score']['total_score'] = round(enhancement_report['final_score'].get('total_score', 0))
|
32 |
+
|
33 |
+
return template.render(
|
34 |
+
model_id=model_id,
|
35 |
+
aibom=aibom,
|
36 |
+
completeness_score=completeness_score,
|
37 |
+
enhancement_report=enhancement_report
|
38 |
+
)
|
39 |
+
|
40 |
+
def save_improved_score_html(model_id: str, aibom: Dict[str, Any], completeness_score: Dict[str, Any],
|
41 |
+
output_path: str, enhancement_report: Optional[Dict[str, Any]] = None):
|
42 |
+
"""
|
43 |
+
Save the improved scoring HTML to a file.
|
44 |
+
|
45 |
+
Args:
|
46 |
+
model_id: The Hugging Face model ID
|
47 |
+
aibom: The generated AIBOM data
|
48 |
+
completeness_score: The completeness score report
|
49 |
+
output_path: Path to save the HTML file
|
50 |
+
enhancement_report: Optional enhancement report with AI improvement information
|
51 |
+
"""
|
52 |
+
html_content = render_improved_score_template(model_id, aibom, completeness_score, enhancement_report)
|
53 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
54 |
+
f.write(html_content)
|
55 |
+
print(f"Improved scoring HTML saved to {output_path}")
|
src/aibom-generator/rate_limiting.py
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import time
|
2 |
+
from collections import defaultdict
|
3 |
+
from fastapi import Request
|
4 |
+
from fastapi.responses import JSONResponse
|
5 |
+
from starlette.middleware.base import BaseHTTPMiddleware
|
6 |
+
import logging
|
7 |
+
import asyncio # Concurrency limiting
|
8 |
+
|
9 |
+
logger = logging.getLogger(__name__)
|
10 |
+
|
11 |
+
class RateLimitMiddleware(BaseHTTPMiddleware):
|
12 |
+
def __init__(
|
13 |
+
self,
|
14 |
+
app,
|
15 |
+
rate_limit_per_minute=10,
|
16 |
+
rate_limit_window=60,
|
17 |
+
protected_routes=["/generate", "/api/generate", "/api/generate-with-report"]
|
18 |
+
):
|
19 |
+
super().__init__(app)
|
20 |
+
self.rate_limit_per_minute = rate_limit_per_minute
|
21 |
+
self.rate_limit_window = rate_limit_window
|
22 |
+
self.protected_routes = protected_routes
|
23 |
+
self.ip_requests = defaultdict(list)
|
24 |
+
logger.info(f"Rate limit middleware initialized: {rate_limit_per_minute} requests per {rate_limit_window}s")
|
25 |
+
|
26 |
+
async def dispatch(self, request: Request, call_next):
|
27 |
+
client_ip = request.client.host
|
28 |
+
current_time = time.time()
|
29 |
+
|
30 |
+
# Only apply rate limiting to protected routes
|
31 |
+
if any(request.url.path.startswith(route) for route in self.protected_routes):
|
32 |
+
# Clean up old requests
|
33 |
+
self.ip_requests[client_ip] = [t for t in self.ip_requests[client_ip]
|
34 |
+
if current_time - t < self.rate_limit_window]
|
35 |
+
|
36 |
+
# Check if rate limit exceeded
|
37 |
+
if len(self.ip_requests[client_ip]) >= self.rate_limit_per_minute:
|
38 |
+
logger.warning(f"Rate limit exceeded for IP {client_ip} on {request.url.path}")
|
39 |
+
return JSONResponse(
|
40 |
+
status_code=429,
|
41 |
+
content={"detail": "Rate limit exceeded. Please try again later."}
|
42 |
+
)
|
43 |
+
|
44 |
+
# Add current request timestamp
|
45 |
+
self.ip_requests[client_ip].append(current_time)
|
46 |
+
|
47 |
+
# Process the request
|
48 |
+
response = await call_next(request)
|
49 |
+
return response
|
50 |
+
|
51 |
+
class ConcurrencyLimitMiddleware(BaseHTTPMiddleware):
|
52 |
+
def __init__(
|
53 |
+
self,
|
54 |
+
app,
|
55 |
+
max_concurrent_requests=5,
|
56 |
+
timeout=5.0,
|
57 |
+
protected_routes=None
|
58 |
+
):
|
59 |
+
super().__init__(app)
|
60 |
+
self.semaphore = asyncio.Semaphore(max_concurrent_requests)
|
61 |
+
self.timeout = timeout
|
62 |
+
self.protected_routes = protected_routes or ["/generate", "/api/generate", "/api/generate-with-report"]
|
63 |
+
logger.info(f"Concurrency limit middleware initialized: {max_concurrent_requests} concurrent requests")
|
64 |
+
|
65 |
+
async def dispatch(self, request, call_next):
|
66 |
+
try:
|
67 |
+
# Only apply to protected routes
|
68 |
+
if any(request.url.path.startswith(route) for route in self.protected_routes):
|
69 |
+
try:
|
70 |
+
# Try to acquire the semaphore
|
71 |
+
acquired = False
|
72 |
+
try:
|
73 |
+
# Use wait_for instead of timeout context manager for compatibility
|
74 |
+
await asyncio.wait_for(self.semaphore.acquire(), timeout=self.timeout)
|
75 |
+
acquired = True
|
76 |
+
return await call_next(request)
|
77 |
+
finally:
|
78 |
+
if acquired:
|
79 |
+
self.semaphore.release()
|
80 |
+
except asyncio.TimeoutError:
|
81 |
+
# Timeout waiting for semaphore
|
82 |
+
logger.warning(f"Concurrency limit reached for {request.url.path}")
|
83 |
+
return JSONResponse(
|
84 |
+
status_code=503,
|
85 |
+
content={"detail": "Server is at capacity. Please try again later."}
|
86 |
+
)
|
87 |
+
else:
|
88 |
+
# For non-protected routes, proceed normally
|
89 |
+
return await call_next(request)
|
90 |
+
except Exception as e:
|
91 |
+
logger.error(f"Error in ConcurrencyLimitMiddleware: {str(e)}")
|
92 |
+
return JSONResponse(
|
93 |
+
status_code=500,
|
94 |
+
content={"detail": f"Internal server error in middleware: {str(e)}"}
|
95 |
+
)
|
96 |
+
|
97 |
+
|
98 |
+
# Protection against large request payloads
|
99 |
+
class RequestSizeLimitMiddleware(BaseHTTPMiddleware):
|
100 |
+
def __init__(self, app, max_content_length=1024*1024): # 1MB default
|
101 |
+
super().__init__(app)
|
102 |
+
self.max_content_length = max_content_length
|
103 |
+
logger.info(f"Request size limit middleware initialized: {max_content_length} bytes")
|
104 |
+
|
105 |
+
async def dispatch(self, request: Request, call_next):
|
106 |
+
content_length = request.headers.get('content-length')
|
107 |
+
if content_length:
|
108 |
+
if int(content_length) > self.max_content_length:
|
109 |
+
logger.warning(f"Request too large: {content_length} bytes")
|
110 |
+
return JSONResponse(
|
111 |
+
status_code=413,
|
112 |
+
content={"detail": "Request too large"}
|
113 |
+
)
|
114 |
+
return await call_next(request)
|
src/aibom-generator/utils.py
ADDED
@@ -0,0 +1,1307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Utility functions for the AI SBOM Generator.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import json
|
6 |
+
import logging
|
7 |
+
import os
|
8 |
+
import re
|
9 |
+
import uuid
|
10 |
+
from typing import Dict, List, Optional, Any, Union, Tuple
|
11 |
+
from enum import Enum
|
12 |
+
|
13 |
+
logger = logging.getLogger(__name__)
|
14 |
+
|
15 |
+
# Validation severity levels
|
16 |
+
class ValidationSeverity(Enum):
|
17 |
+
ERROR = "error"
|
18 |
+
WARNING = "warning"
|
19 |
+
INFO = "info"
|
20 |
+
|
21 |
+
# Field classification based on documentation value (silently aligned with SPDX)
|
22 |
+
FIELD_CLASSIFICATION = {
|
23 |
+
# Critical fields (silently aligned with SPDX mandatory fields)
|
24 |
+
"bomFormat": {"tier": "critical", "weight": 3, "category": "required_fields"},
|
25 |
+
"specVersion": {"tier": "critical", "weight": 3, "category": "required_fields"},
|
26 |
+
"serialNumber": {"tier": "critical", "weight": 3, "category": "required_fields"},
|
27 |
+
"version": {"tier": "critical", "weight": 3, "category": "required_fields"},
|
28 |
+
"name": {"tier": "critical", "weight": 4, "category": "component_basic"},
|
29 |
+
"downloadLocation": {"tier": "critical", "weight": 4, "category": "external_references"},
|
30 |
+
"primaryPurpose": {"tier": "critical", "weight": 3, "category": "metadata"},
|
31 |
+
"suppliedBy": {"tier": "critical", "weight": 4, "category": "metadata"},
|
32 |
+
|
33 |
+
# Important fields (aligned with key SPDX optional fields)
|
34 |
+
"type": {"tier": "important", "weight": 2, "category": "component_basic"},
|
35 |
+
"purl": {"tier": "important", "weight": 4, "category": "component_basic"},
|
36 |
+
"description": {"tier": "important", "weight": 4, "category": "component_basic"},
|
37 |
+
"licenses": {"tier": "important", "weight": 4, "category": "component_basic"},
|
38 |
+
"energyConsumption": {"tier": "important", "weight": 3, "category": "component_model_card"},
|
39 |
+
"hyperparameter": {"tier": "important", "weight": 3, "category": "component_model_card"},
|
40 |
+
"limitation": {"tier": "important", "weight": 3, "category": "component_model_card"},
|
41 |
+
"safetyRiskAssessment": {"tier": "important", "weight": 3, "category": "component_model_card"},
|
42 |
+
"typeOfModel": {"tier": "important", "weight": 3, "category": "component_model_card"},
|
43 |
+
|
44 |
+
# Supplementary fields (aligned with remaining SPDX optional fields)
|
45 |
+
"modelExplainability": {"tier": "supplementary", "weight": 2, "category": "component_model_card"},
|
46 |
+
"standardCompliance": {"tier": "supplementary", "weight": 2, "category": "metadata"},
|
47 |
+
"domain": {"tier": "supplementary", "weight": 2, "category": "metadata"},
|
48 |
+
"energyQuantity": {"tier": "supplementary", "weight": 2, "category": "component_model_card"},
|
49 |
+
"energyUnit": {"tier": "supplementary", "weight": 2, "category": "component_model_card"},
|
50 |
+
"informationAboutTraining": {"tier": "supplementary", "weight": 2, "category": "component_model_card"},
|
51 |
+
"informationAboutApplication": {"tier": "supplementary", "weight": 2, "category": "component_model_card"},
|
52 |
+
"metric": {"tier": "supplementary", "weight": 2, "category": "component_model_card"},
|
53 |
+
"metricDecisionThreshold": {"tier": "supplementary", "weight": 2, "category": "component_model_card"},
|
54 |
+
"modelDataPreprocessing": {"tier": "supplementary", "weight": 2, "category": "component_model_card"},
|
55 |
+
"autonomyType": {"tier": "supplementary", "weight": 1, "category": "metadata"},
|
56 |
+
"useSensitivePersonalInformation": {"tier": "supplementary", "weight": 2, "category": "component_model_card"}
|
57 |
+
}
|
58 |
+
|
59 |
+
# Completeness profiles (silently aligned with SPDX requirements)
|
60 |
+
COMPLETENESS_PROFILES = {
|
61 |
+
"basic": {
|
62 |
+
"description": "Minimal fields required for identification",
|
63 |
+
"required_fields": ["bomFormat", "specVersion", "serialNumber", "version", "name"],
|
64 |
+
"minimum_score": 40
|
65 |
+
},
|
66 |
+
"standard": {
|
67 |
+
"description": "Comprehensive fields for proper documentation",
|
68 |
+
"required_fields": ["bomFormat", "specVersion", "serialNumber", "version", "name",
|
69 |
+
"downloadLocation", "primaryPurpose", "suppliedBy"],
|
70 |
+
"minimum_score": 70
|
71 |
+
},
|
72 |
+
"advanced": {
|
73 |
+
"description": "Extensive documentation for maximum transparency",
|
74 |
+
"required_fields": ["bomFormat", "specVersion", "serialNumber", "version", "name",
|
75 |
+
"downloadLocation", "primaryPurpose", "suppliedBy",
|
76 |
+
"type", "purl", "description", "licenses", "hyperparameter", "limitation",
|
77 |
+
"energyConsumption", "safetyRiskAssessment", "typeOfModel"],
|
78 |
+
"minimum_score": 85
|
79 |
+
}
|
80 |
+
}
|
81 |
+
|
82 |
+
# Validation messages framed as best practices
|
83 |
+
VALIDATION_MESSAGES = {
|
84 |
+
"name": {
|
85 |
+
"missing": "Missing critical field: name - essential for model identification",
|
86 |
+
"recommendation": "Add a descriptive name for the model"
|
87 |
+
},
|
88 |
+
"downloadLocation": {
|
89 |
+
"missing": "Missing critical field: downloadLocation - needed for artifact retrieval",
|
90 |
+
"recommendation": "Add information about where the model can be downloaded"
|
91 |
+
},
|
92 |
+
"primaryPurpose": {
|
93 |
+
"missing": "Missing critical field: primaryPurpose - important for understanding model intent",
|
94 |
+
"recommendation": "Add information about the primary purpose of this model"
|
95 |
+
},
|
96 |
+
"suppliedBy": {
|
97 |
+
"missing": "Missing critical field: suppliedBy - needed for provenance tracking",
|
98 |
+
"recommendation": "Add information about who supplied this model"
|
99 |
+
},
|
100 |
+
"energyConsumption": {
|
101 |
+
"missing": "Missing important field: energyConsumption - helpful for environmental impact assessment",
|
102 |
+
"recommendation": "Consider documenting energy consumption metrics for better transparency"
|
103 |
+
},
|
104 |
+
"hyperparameter": {
|
105 |
+
"missing": "Missing important field: hyperparameter - valuable for reproducibility",
|
106 |
+
"recommendation": "Document key hyperparameters used in training"
|
107 |
+
},
|
108 |
+
"limitation": {
|
109 |
+
"missing": "Missing important field: limitation - important for responsible use",
|
110 |
+
"recommendation": "Document known limitations of the model to guide appropriate usage"
|
111 |
+
}
|
112 |
+
}
|
113 |
+
|
114 |
+
|
115 |
+
def setup_logging(level=logging.INFO):
|
116 |
+
logging.basicConfig(
|
117 |
+
level=level,
|
118 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
119 |
+
datefmt="%Y-%m-%d %H:%M:%S",
|
120 |
+
)
|
121 |
+
|
122 |
+
|
123 |
+
def ensure_directory(directory_path):
|
124 |
+
if not os.path.exists(directory_path):
|
125 |
+
os.makedirs(directory_path)
|
126 |
+
return directory_path
|
127 |
+
|
128 |
+
|
129 |
+
def generate_uuid():
|
130 |
+
return str(uuid.uuid4())
|
131 |
+
|
132 |
+
|
133 |
+
def normalize_license_id(license_text):
|
134 |
+
license_mappings = {
|
135 |
+
"mit": "MIT",
|
136 |
+
"apache": "Apache-2.0",
|
137 |
+
"apache 2": "Apache-2.0",
|
138 |
+
"apache 2.0": "Apache-2.0",
|
139 |
+
"apache-2": "Apache-2.0",
|
140 |
+
"apache-2.0": "Apache-2.0",
|
141 |
+
"gpl": "GPL-3.0-only",
|
142 |
+
"gpl-3": "GPL-3.0-only",
|
143 |
+
"gpl-3.0": "GPL-3.0-only",
|
144 |
+
"gpl3": "GPL-3.0-only",
|
145 |
+
"gpl v3": "GPL-3.0-only",
|
146 |
+
"gpl-2": "GPL-2.0-only",
|
147 |
+
"gpl-2.0": "GPL-2.0-only",
|
148 |
+
"gpl2": "GPL-2.0-only",
|
149 |
+
"gpl v2": "GPL-2.0-only",
|
150 |
+
"lgpl": "LGPL-3.0-only",
|
151 |
+
"lgpl-3": "LGPL-3.0-only",
|
152 |
+
"lgpl-3.0": "LGPL-3.0-only",
|
153 |
+
"bsd": "BSD-3-Clause",
|
154 |
+
"bsd-3": "BSD-3-Clause",
|
155 |
+
"bsd-3-clause": "BSD-3-Clause",
|
156 |
+
"bsd-2": "BSD-2-Clause",
|
157 |
+
"bsd-2-clause": "BSD-2-Clause",
|
158 |
+
"cc": "CC-BY-4.0",
|
159 |
+
"cc-by": "CC-BY-4.0",
|
160 |
+
"cc-by-4.0": "CC-BY-4.0",
|
161 |
+
"cc-by-sa": "CC-BY-SA-4.0",
|
162 |
+
"cc-by-sa-4.0": "CC-BY-SA-4.0",
|
163 |
+
"cc-by-nc": "CC-BY-NC-4.0",
|
164 |
+
"cc-by-nc-4.0": "CC-BY-NC-4.0",
|
165 |
+
"cc0": "CC0-1.0",
|
166 |
+
"cc0-1.0": "CC0-1.0",
|
167 |
+
"public domain": "CC0-1.0",
|
168 |
+
"unlicense": "Unlicense",
|
169 |
+
"proprietary": "NONE",
|
170 |
+
"commercial": "NONE",
|
171 |
+
}
|
172 |
+
|
173 |
+
if not license_text:
|
174 |
+
return None
|
175 |
+
|
176 |
+
normalized = re.sub(r'[^\w\s-]', '', license_text.lower())
|
177 |
+
|
178 |
+
if normalized in license_mappings:
|
179 |
+
return license_mappings[normalized]
|
180 |
+
|
181 |
+
for key, value in license_mappings.items():
|
182 |
+
if key in normalized:
|
183 |
+
return value
|
184 |
+
|
185 |
+
return license_text
|
186 |
+
|
187 |
+
|
188 |
+
def validate_spdx(license_entry):
|
189 |
+
spdx_licenses = [
|
190 |
+
"MIT", "Apache-2.0", "GPL-3.0-only", "GPL-2.0-only", "LGPL-3.0-only",
|
191 |
+
"BSD-3-Clause", "BSD-2-Clause", "CC-BY-4.0", "CC-BY-SA-4.0", "CC0-1.0",
|
192 |
+
"Unlicense", "NONE"
|
193 |
+
]
|
194 |
+
if isinstance(license_entry, list):
|
195 |
+
return all(lic in spdx_licenses for lic in license_entry)
|
196 |
+
return license_entry in spdx_licenses
|
197 |
+
|
198 |
+
|
199 |
+
def check_field_in_aibom(aibom: Dict[str, Any], field: str) -> bool:
|
200 |
+
"""
|
201 |
+
Check if a field is present in the AIBOM.
|
202 |
+
|
203 |
+
Args:
|
204 |
+
aibom: The AIBOM to check
|
205 |
+
field: The field name to check
|
206 |
+
|
207 |
+
Returns:
|
208 |
+
True if the field is present, False otherwise
|
209 |
+
"""
|
210 |
+
# Check in root level
|
211 |
+
if field in aibom:
|
212 |
+
return True
|
213 |
+
|
214 |
+
# Check in metadata
|
215 |
+
if "metadata" in aibom:
|
216 |
+
metadata = aibom["metadata"]
|
217 |
+
if field in metadata:
|
218 |
+
return True
|
219 |
+
|
220 |
+
# Check in metadata properties
|
221 |
+
if "properties" in metadata:
|
222 |
+
for prop in metadata["properties"]:
|
223 |
+
if prop.get("name") == f"spdx:{field}" or prop.get("name") == field:
|
224 |
+
return True
|
225 |
+
|
226 |
+
# Check in components
|
227 |
+
if "components" in aibom and aibom["components"]:
|
228 |
+
component = aibom["components"][0] # Use first component
|
229 |
+
|
230 |
+
if field in component:
|
231 |
+
return True
|
232 |
+
|
233 |
+
# Check in component properties
|
234 |
+
if "properties" in component:
|
235 |
+
for prop in component["properties"]:
|
236 |
+
if prop.get("name") == f"spdx:{field}" or prop.get("name") == field:
|
237 |
+
return True
|
238 |
+
|
239 |
+
# Check in model card
|
240 |
+
if "modelCard" in component:
|
241 |
+
model_card = component["modelCard"]
|
242 |
+
|
243 |
+
if field in model_card:
|
244 |
+
return True
|
245 |
+
|
246 |
+
# Check in model parameters
|
247 |
+
if "modelParameters" in model_card:
|
248 |
+
if field in model_card["modelParameters"]:
|
249 |
+
return True
|
250 |
+
|
251 |
+
# Check in model parameters properties
|
252 |
+
if "properties" in model_card["modelParameters"]:
|
253 |
+
for prop in model_card["modelParameters"]["properties"]:
|
254 |
+
if prop.get("name") == f"spdx:{field}" or prop.get("name") == field:
|
255 |
+
return True
|
256 |
+
|
257 |
+
# Check in considerations
|
258 |
+
if "considerations" in model_card:
|
259 |
+
if field in model_card["considerations"]:
|
260 |
+
return True
|
261 |
+
|
262 |
+
# Check in specific consideration sections
|
263 |
+
for section in ["technicalLimitations", "ethicalConsiderations", "environmentalConsiderations"]:
|
264 |
+
if section in model_card["considerations"]:
|
265 |
+
if field == "limitation" and section == "technicalLimitations":
|
266 |
+
return True
|
267 |
+
if field == "safetyRiskAssessment" and section == "ethicalConsiderations":
|
268 |
+
return True
|
269 |
+
if field == "energyConsumption" and section == "environmentalConsiderations":
|
270 |
+
return True
|
271 |
+
|
272 |
+
# Check in external references
|
273 |
+
if field == "downloadLocation" and "externalReferences" in aibom:
|
274 |
+
for ref in aibom["externalReferences"]:
|
275 |
+
if ref.get("type") == "distribution":
|
276 |
+
return True
|
277 |
+
|
278 |
+
return False
|
279 |
+
|
280 |
+
|
281 |
+
def determine_completeness_profile(aibom: Dict[str, Any], score: float) -> Dict[str, Any]:
|
282 |
+
"""
|
283 |
+
Determine which completeness profile the AIBOM satisfies.
|
284 |
+
|
285 |
+
Args:
|
286 |
+
aibom: The AIBOM to check
|
287 |
+
score: The calculated score
|
288 |
+
|
289 |
+
Returns:
|
290 |
+
Dictionary with profile information
|
291 |
+
"""
|
292 |
+
satisfied_profiles = []
|
293 |
+
|
294 |
+
for profile_name, profile in COMPLETENESS_PROFILES.items():
|
295 |
+
# Check if all required fields are present
|
296 |
+
all_required_present = all(check_field_in_aibom(aibom, field) for field in profile["required_fields"])
|
297 |
+
|
298 |
+
# Check if score meets minimum
|
299 |
+
score_sufficient = score >= profile["minimum_score"]
|
300 |
+
|
301 |
+
if all_required_present and score_sufficient:
|
302 |
+
satisfied_profiles.append(profile_name)
|
303 |
+
|
304 |
+
# Return the highest satisfied profile
|
305 |
+
if "advanced" in satisfied_profiles:
|
306 |
+
return {
|
307 |
+
"name": "advanced",
|
308 |
+
"description": COMPLETENESS_PROFILES["advanced"]["description"],
|
309 |
+
"satisfied": True
|
310 |
+
}
|
311 |
+
elif "standard" in satisfied_profiles:
|
312 |
+
return {
|
313 |
+
"name": "standard",
|
314 |
+
"description": COMPLETENESS_PROFILES["standard"]["description"],
|
315 |
+
"satisfied": True
|
316 |
+
}
|
317 |
+
elif "basic" in satisfied_profiles:
|
318 |
+
return {
|
319 |
+
"name": "basic",
|
320 |
+
"description": COMPLETENESS_PROFILES["basic"]["description"],
|
321 |
+
"satisfied": True
|
322 |
+
}
|
323 |
+
else:
|
324 |
+
return {
|
325 |
+
"name": "incomplete",
|
326 |
+
"description": "Does not satisfy any completeness profile",
|
327 |
+
"satisfied": False
|
328 |
+
}
|
329 |
+
|
330 |
+
|
331 |
+
def apply_completeness_penalties(original_score: float, missing_fields: Dict[str, List[str]]) -> Dict[str, Any]:
|
332 |
+
"""
|
333 |
+
Apply penalties based on missing critical fields.
|
334 |
+
|
335 |
+
Args:
|
336 |
+
original_score: The original calculated score
|
337 |
+
missing_fields: Dictionary of missing fields by tier
|
338 |
+
|
339 |
+
Returns:
|
340 |
+
Dictionary with penalty information
|
341 |
+
"""
|
342 |
+
# Count missing fields by tier
|
343 |
+
missing_critical_count = len(missing_fields["critical"])
|
344 |
+
missing_important_count = len(missing_fields["important"])
|
345 |
+
|
346 |
+
# Calculate penalty based on missing critical fields
|
347 |
+
if missing_critical_count > 3:
|
348 |
+
penalty_factor = 0.8 # 20% penalty
|
349 |
+
penalty_reason = "Multiple critical fields missing"
|
350 |
+
elif missing_critical_count > 0:
|
351 |
+
penalty_factor = 0.9 # 10% penalty
|
352 |
+
penalty_reason = "Some critical fields missing"
|
353 |
+
elif missing_important_count > 5:
|
354 |
+
penalty_factor = 0.95 # 5% penalty
|
355 |
+
penalty_reason = "Several important fields missing"
|
356 |
+
else:
|
357 |
+
# No penalty
|
358 |
+
penalty_factor = 1.0
|
359 |
+
penalty_reason = None
|
360 |
+
|
361 |
+
adjusted_score = original_score * penalty_factor
|
362 |
+
|
363 |
+
return {
|
364 |
+
"adjusted_score": round(adjusted_score, 1), # Round to 1 decimal place
|
365 |
+
"penalty_applied": penalty_reason is not None,
|
366 |
+
"penalty_reason": penalty_reason,
|
367 |
+
"penalty_factor": penalty_factor
|
368 |
+
}
|
369 |
+
|
370 |
+
|
371 |
+
def generate_field_recommendations(missing_fields: Dict[str, List[str]]) -> List[Dict[str, Any]]:
|
372 |
+
"""
|
373 |
+
Generate recommendations for missing fields.
|
374 |
+
|
375 |
+
Args:
|
376 |
+
missing_fields: Dictionary of missing fields by tier
|
377 |
+
|
378 |
+
Returns:
|
379 |
+
List of recommendations
|
380 |
+
"""
|
381 |
+
recommendations = []
|
382 |
+
|
383 |
+
# Prioritize critical fields
|
384 |
+
for field in missing_fields["critical"]:
|
385 |
+
if field in VALIDATION_MESSAGES:
|
386 |
+
recommendations.append({
|
387 |
+
"priority": "high",
|
388 |
+
"field": field,
|
389 |
+
"message": VALIDATION_MESSAGES[field]["missing"],
|
390 |
+
"recommendation": VALIDATION_MESSAGES[field]["recommendation"]
|
391 |
+
})
|
392 |
+
else:
|
393 |
+
recommendations.append({
|
394 |
+
"priority": "high",
|
395 |
+
"field": field,
|
396 |
+
"message": f"Missing critical field: {field}",
|
397 |
+
"recommendation": f"Add {field} to improve documentation completeness"
|
398 |
+
})
|
399 |
+
|
400 |
+
# Then important fields
|
401 |
+
for field in missing_fields["important"]:
|
402 |
+
if field in VALIDATION_MESSAGES:
|
403 |
+
recommendations.append({
|
404 |
+
"priority": "medium",
|
405 |
+
"field": field,
|
406 |
+
"message": VALIDATION_MESSAGES[field]["missing"],
|
407 |
+
"recommendation": VALIDATION_MESSAGES[field]["recommendation"]
|
408 |
+
})
|
409 |
+
else:
|
410 |
+
recommendations.append({
|
411 |
+
"priority": "medium",
|
412 |
+
"field": field,
|
413 |
+
"message": f"Missing important field: {field}",
|
414 |
+
"recommendation": f"Consider adding {field} for better documentation"
|
415 |
+
})
|
416 |
+
|
417 |
+
# Finally supplementary fields (limit to top 5)
|
418 |
+
supplementary_count = 0
|
419 |
+
for field in missing_fields["supplementary"]:
|
420 |
+
if supplementary_count >= 5:
|
421 |
+
break
|
422 |
+
|
423 |
+
recommendations.append({
|
424 |
+
"priority": "low",
|
425 |
+
"field": field,
|
426 |
+
"message": f"Missing supplementary field: {field}",
|
427 |
+
"recommendation": f"Consider adding {field} for comprehensive documentation"
|
428 |
+
})
|
429 |
+
supplementary_count += 1
|
430 |
+
|
431 |
+
return recommendations
|
432 |
+
|
433 |
+
|
434 |
+
def _validate_ai_requirements(aibom: Dict[str, Any]) -> List[Dict[str, Any]]:
|
435 |
+
"""
|
436 |
+
Validate AI-specific requirements for an AIBOM.
|
437 |
+
|
438 |
+
Args:
|
439 |
+
aibom: The AIBOM to validate
|
440 |
+
|
441 |
+
Returns:
|
442 |
+
List of validation issues
|
443 |
+
"""
|
444 |
+
issues = []
|
445 |
+
issue_codes = set()
|
446 |
+
|
447 |
+
# Check required fields
|
448 |
+
for field in ["bomFormat", "specVersion", "serialNumber", "version"]:
|
449 |
+
if field not in aibom:
|
450 |
+
issues.append({
|
451 |
+
"severity": ValidationSeverity.ERROR.value,
|
452 |
+
"code": f"MISSING_{field.upper()}",
|
453 |
+
"message": f"Missing required field: {field}",
|
454 |
+
"path": f"$.{field}"
|
455 |
+
})
|
456 |
+
issue_codes.add(f"MISSING_{field.upper()}")
|
457 |
+
|
458 |
+
# Check bomFormat
|
459 |
+
if "bomFormat" in aibom and aibom["bomFormat"] != "CycloneDX":
|
460 |
+
issues.append({
|
461 |
+
"severity": ValidationSeverity.ERROR.value,
|
462 |
+
"code": "INVALID_BOM_FORMAT",
|
463 |
+
"message": f"Invalid bomFormat: {aibom['bomFormat']}. Must be 'CycloneDX'",
|
464 |
+
"path": "$.bomFormat"
|
465 |
+
})
|
466 |
+
issue_codes.add("INVALID_BOM_FORMAT")
|
467 |
+
|
468 |
+
# Check specVersion
|
469 |
+
if "specVersion" in aibom and aibom["specVersion"] != "1.6":
|
470 |
+
issues.append({
|
471 |
+
"severity": ValidationSeverity.ERROR.value,
|
472 |
+
"code": "INVALID_SPEC_VERSION",
|
473 |
+
"message": f"Invalid specVersion: {aibom['specVersion']}. Must be '1.6'",
|
474 |
+
"path": "$.specVersion"
|
475 |
+
})
|
476 |
+
issue_codes.add("INVALID_SPEC_VERSION")
|
477 |
+
|
478 |
+
# Check serialNumber
|
479 |
+
if "serialNumber" in aibom and not aibom["serialNumber"].startswith("urn:uuid:"):
|
480 |
+
issues.append({
|
481 |
+
"severity": ValidationSeverity.ERROR.value,
|
482 |
+
"code": "INVALID_SERIAL_NUMBER",
|
483 |
+
"message": f"Invalid serialNumber format: {aibom['serialNumber']}. Must start with 'urn:uuid:'",
|
484 |
+
"path": "$.serialNumber"
|
485 |
+
})
|
486 |
+
issue_codes.add("INVALID_SERIAL_NUMBER")
|
487 |
+
|
488 |
+
# Check version
|
489 |
+
if "version" in aibom:
|
490 |
+
if not isinstance(aibom["version"], int):
|
491 |
+
issues.append({
|
492 |
+
"severity": ValidationSeverity.ERROR.value,
|
493 |
+
"code": "INVALID_VERSION_TYPE",
|
494 |
+
"message": f"Invalid version type: {type(aibom['version'])}. Must be an integer",
|
495 |
+
"path": "$.version"
|
496 |
+
})
|
497 |
+
issue_codes.add("INVALID_VERSION_TYPE")
|
498 |
+
elif aibom["version"] <= 0:
|
499 |
+
issues.append({
|
500 |
+
"severity": ValidationSeverity.ERROR.value,
|
501 |
+
"code": "INVALID_VERSION_VALUE",
|
502 |
+
"message": f"Invalid version value: {aibom['version']}. Must be positive",
|
503 |
+
"path": "$.version"
|
504 |
+
})
|
505 |
+
issue_codes.add("INVALID_VERSION_VALUE")
|
506 |
+
|
507 |
+
# Check metadata
|
508 |
+
if "metadata" not in aibom:
|
509 |
+
issues.append({
|
510 |
+
"severity": ValidationSeverity.ERROR.value,
|
511 |
+
"code": "MISSING_METADATA",
|
512 |
+
"message": "Missing metadata section",
|
513 |
+
"path": "$.metadata"
|
514 |
+
})
|
515 |
+
issue_codes.add("MISSING_METADATA")
|
516 |
+
else:
|
517 |
+
metadata = aibom["metadata"]
|
518 |
+
|
519 |
+
# Check timestamp
|
520 |
+
if "timestamp" not in metadata:
|
521 |
+
issues.append({
|
522 |
+
"severity": ValidationSeverity.WARNING.value,
|
523 |
+
"code": "MISSING_TIMESTAMP",
|
524 |
+
"message": "Missing timestamp in metadata",
|
525 |
+
"path": "$.metadata.timestamp"
|
526 |
+
})
|
527 |
+
issue_codes.add("MISSING_TIMESTAMP")
|
528 |
+
|
529 |
+
# Check tools
|
530 |
+
if "tools" not in metadata or not metadata["tools"] or len(metadata["tools"]) == 0:
|
531 |
+
issues.append({
|
532 |
+
"severity": ValidationSeverity.WARNING.value,
|
533 |
+
"code": "MISSING_TOOLS",
|
534 |
+
"message": "Missing tools in metadata",
|
535 |
+
"path": "$.metadata.tools"
|
536 |
+
})
|
537 |
+
issue_codes.add("MISSING_TOOLS")
|
538 |
+
|
539 |
+
# Check authors
|
540 |
+
if "authors" not in metadata or not metadata["authors"] or len(metadata["authors"]) == 0:
|
541 |
+
issues.append({
|
542 |
+
"severity": ValidationSeverity.WARNING.value,
|
543 |
+
"code": "MISSING_AUTHORS",
|
544 |
+
"message": "Missing authors in metadata",
|
545 |
+
"path": "$.metadata.authors"
|
546 |
+
})
|
547 |
+
issue_codes.add("MISSING_AUTHORS")
|
548 |
+
else:
|
549 |
+
# Check author properties
|
550 |
+
for i, author in enumerate(metadata["authors"]):
|
551 |
+
if "url" in author:
|
552 |
+
issues.append({
|
553 |
+
"severity": ValidationSeverity.ERROR.value,
|
554 |
+
"code": "INVALID_AUTHOR_PROPERTY",
|
555 |
+
"message": "Author objects should not contain 'url' property, use 'email' instead",
|
556 |
+
"path": f"$.metadata.authors[{i}].url"
|
557 |
+
})
|
558 |
+
issue_codes.add("INVALID_AUTHOR_PROPERTY")
|
559 |
+
|
560 |
+
# Check properties
|
561 |
+
if "properties" not in metadata or not metadata["properties"] or len(metadata["properties"]) == 0:
|
562 |
+
issues.append({
|
563 |
+
"severity": ValidationSeverity.INFO.value,
|
564 |
+
"code": "MISSING_PROPERTIES",
|
565 |
+
"message": "Missing properties in metadata",
|
566 |
+
"path": "$.metadata.properties"
|
567 |
+
})
|
568 |
+
issue_codes.add("MISSING_PROPERTIES")
|
569 |
+
|
570 |
+
# Check components
|
571 |
+
if "components" not in aibom or not aibom["components"] or len(aibom["components"]) == 0:
|
572 |
+
issues.append({
|
573 |
+
"severity": ValidationSeverity.ERROR.value,
|
574 |
+
"code": "MISSING_COMPONENTS",
|
575 |
+
"message": "Missing components section or empty components array",
|
576 |
+
"path": "$.components"
|
577 |
+
})
|
578 |
+
issue_codes.add("MISSING_COMPONENTS")
|
579 |
+
else:
|
580 |
+
components = aibom["components"]
|
581 |
+
|
582 |
+
# Check first component (AI model)
|
583 |
+
component = components[0]
|
584 |
+
|
585 |
+
# Check type
|
586 |
+
if "type" not in component:
|
587 |
+
issues.append({
|
588 |
+
"severity": ValidationSeverity.ERROR.value,
|
589 |
+
"code": "MISSING_COMPONENT_TYPE",
|
590 |
+
"message": "Missing type in first component",
|
591 |
+
"path": "$.components[0].type"
|
592 |
+
})
|
593 |
+
issue_codes.add("MISSING_COMPONENT_TYPE")
|
594 |
+
elif component["type"] != "machine-learning-model":
|
595 |
+
issues.append({
|
596 |
+
"severity": ValidationSeverity.ERROR.value,
|
597 |
+
"code": "INVALID_COMPONENT_TYPE",
|
598 |
+
"message": f"Invalid type in first component: {component['type']}. Must be 'machine-learning-model'",
|
599 |
+
"path": "$.components[0].type"
|
600 |
+
})
|
601 |
+
issue_codes.add("INVALID_COMPONENT_TYPE")
|
602 |
+
|
603 |
+
# Check name
|
604 |
+
if "name" not in component or not component["name"]:
|
605 |
+
issues.append({
|
606 |
+
"severity": ValidationSeverity.ERROR.value,
|
607 |
+
"code": "MISSING_COMPONENT_NAME",
|
608 |
+
"message": "Missing name in first component",
|
609 |
+
"path": "$.components[0].name"
|
610 |
+
})
|
611 |
+
issue_codes.add("MISSING_COMPONENT_NAME")
|
612 |
+
|
613 |
+
# Check bom-ref
|
614 |
+
if "bom-ref" not in component or not component["bom-ref"]:
|
615 |
+
issues.append({
|
616 |
+
"severity": ValidationSeverity.ERROR.value,
|
617 |
+
"code": "MISSING_BOM_REF",
|
618 |
+
"message": "Missing bom-ref in first component",
|
619 |
+
"path": "$.components[0].bom-ref"
|
620 |
+
})
|
621 |
+
issue_codes.add("MISSING_BOM_REF")
|
622 |
+
|
623 |
+
# Check purl
|
624 |
+
if "purl" not in component or not component["purl"]:
|
625 |
+
issues.append({
|
626 |
+
"severity": ValidationSeverity.WARNING.value,
|
627 |
+
"code": "MISSING_PURL",
|
628 |
+
"message": "Missing purl in first component",
|
629 |
+
"path": "$.components[0].purl"
|
630 |
+
})
|
631 |
+
issue_codes.add("MISSING_PURL")
|
632 |
+
elif not component["purl"].startswith("pkg:"):
|
633 |
+
issues.append({
|
634 |
+
"severity": ValidationSeverity.ERROR.value,
|
635 |
+
"code": "INVALID_PURL_FORMAT",
|
636 |
+
"message": f"Invalid purl format: {component['purl']}. Must start with 'pkg:'",
|
637 |
+
"path": "$.components[0].purl"
|
638 |
+
})
|
639 |
+
issue_codes.add("INVALID_PURL_FORMAT")
|
640 |
+
elif "@" not in component["purl"]:
|
641 |
+
issues.append({
|
642 |
+
"severity": ValidationSeverity.WARNING.value,
|
643 |
+
"code": "MISSING_VERSION_IN_PURL",
|
644 |
+
"message": f"Missing version in purl: {component['purl']}. Should include version after '@'",
|
645 |
+
"path": "$.components[0].purl"
|
646 |
+
})
|
647 |
+
issue_codes.add("MISSING_VERSION_IN_PURL")
|
648 |
+
|
649 |
+
# Check description
|
650 |
+
if "description" not in component or not component["description"]:
|
651 |
+
issues.append({
|
652 |
+
"severity": ValidationSeverity.WARNING.value,
|
653 |
+
"code": "MISSING_DESCRIPTION",
|
654 |
+
"message": "Missing description in first component",
|
655 |
+
"path": "$.components[0].description"
|
656 |
+
})
|
657 |
+
issue_codes.add("MISSING_DESCRIPTION")
|
658 |
+
elif len(component["description"]) < 20:
|
659 |
+
issues.append({
|
660 |
+
"severity": ValidationSeverity.INFO.value,
|
661 |
+
"code": "SHORT_DESCRIPTION",
|
662 |
+
"message": f"Description is too short: {len(component['description'])} characters. Recommended minimum is 20 characters",
|
663 |
+
"path": "$.components[0].description"
|
664 |
+
})
|
665 |
+
issue_codes.add("SHORT_DESCRIPTION")
|
666 |
+
|
667 |
+
# Check modelCard
|
668 |
+
if "modelCard" not in component or not component["modelCard"]:
|
669 |
+
issues.append({
|
670 |
+
"severity": ValidationSeverity.WARNING.value,
|
671 |
+
"code": "MISSING_MODEL_CARD",
|
672 |
+
"message": "Missing modelCard in first component",
|
673 |
+
"path": "$.components[0].modelCard"
|
674 |
+
})
|
675 |
+
issue_codes.add("MISSING_MODEL_CARD")
|
676 |
+
else:
|
677 |
+
model_card = component["modelCard"]
|
678 |
+
|
679 |
+
# Check modelParameters
|
680 |
+
if "modelParameters" not in model_card or not model_card["modelParameters"]:
|
681 |
+
issues.append({
|
682 |
+
"severity": ValidationSeverity.WARNING.value,
|
683 |
+
"code": "MISSING_MODEL_PARAMETERS",
|
684 |
+
"message": "Missing modelParameters in modelCard",
|
685 |
+
"path": "$.components[0].modelCard.modelParameters"
|
686 |
+
})
|
687 |
+
issue_codes.add("MISSING_MODEL_PARAMETERS")
|
688 |
+
|
689 |
+
# Check considerations
|
690 |
+
if "considerations" not in model_card or not model_card["considerations"]:
|
691 |
+
issues.append({
|
692 |
+
"severity": ValidationSeverity.WARNING.value,
|
693 |
+
"code": "MISSING_CONSIDERATIONS",
|
694 |
+
"message": "Missing considerations in modelCard",
|
695 |
+
"path": "$.components[0].modelCard.considerations"
|
696 |
+
})
|
697 |
+
issue_codes.add("MISSING_CONSIDERATIONS")
|
698 |
+
|
699 |
+
return issues
|
700 |
+
|
701 |
+
|
702 |
+
def _generate_validation_recommendations(issues: List[Dict[str, Any]]) -> List[str]:
|
703 |
+
"""
|
704 |
+
Generate recommendations based on validation issues.
|
705 |
+
|
706 |
+
Args:
|
707 |
+
issues: List of validation issues
|
708 |
+
|
709 |
+
Returns:
|
710 |
+
List of recommendations
|
711 |
+
"""
|
712 |
+
recommendations = []
|
713 |
+
issue_codes = set(issue["code"] for issue in issues)
|
714 |
+
|
715 |
+
# Generate recommendations based on issue codes
|
716 |
+
if "MISSING_COMPONENTS" in issue_codes:
|
717 |
+
recommendations.append("Add at least one component to the AIBOM")
|
718 |
+
|
719 |
+
if "MISSING_COMPONENT_TYPE" in issue_codes or "INVALID_COMPONENT_TYPE" in issue_codes:
|
720 |
+
recommendations.append("Ensure all AI components have type 'machine-learning-model'")
|
721 |
+
|
722 |
+
if "MISSING_PURL" in issue_codes or "INVALID_PURL_FORMAT" in issue_codes:
|
723 |
+
recommendations.append("Ensure all components have a valid PURL starting with 'pkg:'")
|
724 |
+
|
725 |
+
if "MISSING_VERSION_IN_PURL" in issue_codes:
|
726 |
+
recommendations.append("Include version information in PURLs using '@' syntax (e.g., pkg:huggingface/org/model@version)")
|
727 |
+
|
728 |
+
if "MISSING_MODEL_CARD" in issue_codes:
|
729 |
+
recommendations.append("Add a model card section to AI components")
|
730 |
+
|
731 |
+
if "MISSING_MODEL_PARAMETERS" in issue_codes:
|
732 |
+
recommendations.append("Include model parameters in the model card section")
|
733 |
+
|
734 |
+
if "MISSING_CONSIDERATIONS" in issue_codes:
|
735 |
+
recommendations.append("Add ethical considerations, limitations, and risks to the model card")
|
736 |
+
|
737 |
+
if "MISSING_METADATA" in issue_codes:
|
738 |
+
recommendations.append("Add metadata section to the AIBOM")
|
739 |
+
|
740 |
+
if "MISSING_TOOLS" in issue_codes:
|
741 |
+
recommendations.append("Include tools information in the metadata section")
|
742 |
+
|
743 |
+
if "MISSING_AUTHORS" in issue_codes:
|
744 |
+
recommendations.append("Add authors information to the metadata section")
|
745 |
+
|
746 |
+
if "MISSING_PROPERTIES" in issue_codes:
|
747 |
+
recommendations.append("Include additional properties in the metadata section")
|
748 |
+
|
749 |
+
if "INVALID_AUTHOR_PROPERTY" in issue_codes:
|
750 |
+
recommendations.append("Remove 'url' property from author objects and use 'email' instead to comply with CycloneDX schema")
|
751 |
+
|
752 |
+
return recommendations
|
753 |
+
|
754 |
+
|
755 |
+
def validate_aibom(aibom: Dict[str, Any]) -> Dict[str, Any]:
|
756 |
+
"""
|
757 |
+
Validate an AIBOM against AI-specific requirements.
|
758 |
+
|
759 |
+
Args:
|
760 |
+
aibom: The AIBOM to validate
|
761 |
+
|
762 |
+
Returns:
|
763 |
+
Validation report with issues and recommendations
|
764 |
+
"""
|
765 |
+
# Initialize validation report
|
766 |
+
report = {
|
767 |
+
"valid": True,
|
768 |
+
"ai_valid": True,
|
769 |
+
"issues": [],
|
770 |
+
"recommendations": [],
|
771 |
+
"summary": {
|
772 |
+
"error_count": 0,
|
773 |
+
"warning_count": 0,
|
774 |
+
"info_count": 0
|
775 |
+
}
|
776 |
+
}
|
777 |
+
|
778 |
+
# Validate AI-specific requirements
|
779 |
+
ai_issues = _validate_ai_requirements(aibom)
|
780 |
+
if ai_issues:
|
781 |
+
report["ai_valid"] = False
|
782 |
+
report["valid"] = False
|
783 |
+
report["issues"].extend(ai_issues)
|
784 |
+
|
785 |
+
# Generate recommendations
|
786 |
+
report["recommendations"] = _generate_validation_recommendations(report["issues"])
|
787 |
+
|
788 |
+
# Update summary counts
|
789 |
+
for issue in report["issues"]:
|
790 |
+
if issue["severity"] == ValidationSeverity.ERROR.value:
|
791 |
+
report["summary"]["error_count"] += 1
|
792 |
+
elif issue["severity"] == ValidationSeverity.WARNING.value:
|
793 |
+
report["summary"]["warning_count"] += 1
|
794 |
+
elif issue["severity"] == ValidationSeverity.INFO.value:
|
795 |
+
report["summary"]["info_count"] += 1
|
796 |
+
|
797 |
+
return report
|
798 |
+
|
799 |
+
|
800 |
+
def get_validation_summary(report: Dict[str, Any]) -> str:
|
801 |
+
"""
|
802 |
+
Get a human-readable summary of the validation report.
|
803 |
+
|
804 |
+
Args:
|
805 |
+
report: Validation report
|
806 |
+
|
807 |
+
Returns:
|
808 |
+
Human-readable summary
|
809 |
+
"""
|
810 |
+
if report["valid"]:
|
811 |
+
summary = "✅ AIBOM is valid and complies with AI requirements.\n"
|
812 |
+
else:
|
813 |
+
summary = "❌ AIBOM validation failed.\n"
|
814 |
+
|
815 |
+
summary += f"\nSummary:\n"
|
816 |
+
summary += f"- Errors: {report['summary']['error_count']}\n"
|
817 |
+
summary += f"- Warnings: {report['summary']['warning_count']}\n"
|
818 |
+
summary += f"- Info: {report['summary']['info_count']}\n"
|
819 |
+
|
820 |
+
if not report["valid"]:
|
821 |
+
summary += "\nIssues:\n"
|
822 |
+
for issue in report["issues"]:
|
823 |
+
severity = issue["severity"].upper()
|
824 |
+
code = issue["code"]
|
825 |
+
message = issue["message"]
|
826 |
+
path = issue["path"]
|
827 |
+
summary += f"- [{severity}] {code}: {message} (at {path})\n"
|
828 |
+
|
829 |
+
summary += "\nRecommendations:\n"
|
830 |
+
for i, recommendation in enumerate(report["recommendations"], 1):
|
831 |
+
summary += f"{i}. {recommendation}\n"
|
832 |
+
|
833 |
+
return summary
|
834 |
+
|
835 |
+
|
836 |
+
def calculate_industry_neutral_score(aibom: Dict[str, Any]) -> Dict[str, Any]:
|
837 |
+
"""
|
838 |
+
Calculate completeness score using industry best practices without explicit standard references.
|
839 |
+
|
840 |
+
Args:
|
841 |
+
aibom: The AIBOM to score
|
842 |
+
|
843 |
+
Returns:
|
844 |
+
Dictionary containing score and recommendations
|
845 |
+
"""
|
846 |
+
field_checklist = {}
|
847 |
+
max_scores = {
|
848 |
+
"required_fields": 20,
|
849 |
+
"metadata": 20,
|
850 |
+
"component_basic": 20,
|
851 |
+
"component_model_card": 30,
|
852 |
+
"external_references": 10
|
853 |
+
}
|
854 |
+
|
855 |
+
# Track missing fields by tier
|
856 |
+
missing_fields = {
|
857 |
+
"critical": [],
|
858 |
+
"important": [],
|
859 |
+
"supplementary": []
|
860 |
+
}
|
861 |
+
|
862 |
+
# Score each field based on classification
|
863 |
+
scores_by_category = {category: 0 for category in max_scores.keys()}
|
864 |
+
max_possible_by_category = {category: 0 for category in max_scores.keys()}
|
865 |
+
|
866 |
+
for field, classification in FIELD_CLASSIFICATION.items():
|
867 |
+
tier = classification["tier"]
|
868 |
+
weight = classification["weight"]
|
869 |
+
category = classification["category"]
|
870 |
+
|
871 |
+
# Add to max possible score for this category
|
872 |
+
max_possible_by_category[category] += weight
|
873 |
+
|
874 |
+
# Check if field is present
|
875 |
+
is_present = check_field_in_aibom(aibom, field)
|
876 |
+
|
877 |
+
if is_present:
|
878 |
+
scores_by_category[category] += weight
|
879 |
+
else:
|
880 |
+
missing_fields[tier].append(field)
|
881 |
+
|
882 |
+
# Add to field checklist with appropriate indicators
|
883 |
+
importance_indicator = "★★★" if tier == "critical" else "★★" if tier == "important" else "★"
|
884 |
+
field_checklist[field] = f"{'✔' if is_present else '✘'} {importance_indicator}"
|
885 |
+
|
886 |
+
# Normalize category scores to max_scores
|
887 |
+
normalized_scores = {}
|
888 |
+
for category in scores_by_category:
|
889 |
+
if max_possible_by_category[category] > 0:
|
890 |
+
# Normalize to the max score for this category
|
891 |
+
normalized_score = (scores_by_category[category] / max_possible_by_category[category]) * max_scores[category]
|
892 |
+
normalized_scores[category] = min(normalized_score, max_scores[category])
|
893 |
+
else:
|
894 |
+
normalized_scores[category] = 0
|
895 |
+
|
896 |
+
# Calculate total score (sum of weighted normalized scores)
|
897 |
+
total_score = 0
|
898 |
+
for category, score in normalized_scores.items():
|
899 |
+
# Each category contributes its percentage to the total
|
900 |
+
category_weight = max_scores[category] / sum(max_scores.values())
|
901 |
+
total_score += score * category_weight
|
902 |
+
|
903 |
+
# Round to one decimal place
|
904 |
+
total_score = round(total_score, 1)
|
905 |
+
|
906 |
+
# Ensure score is between 0 and 100
|
907 |
+
total_score = max(0, min(total_score, 100))
|
908 |
+
|
909 |
+
# Determine completeness profile
|
910 |
+
profile = determine_completeness_profile(aibom, total_score)
|
911 |
+
|
912 |
+
# Apply penalties for missing critical fields
|
913 |
+
penalty_result = apply_completeness_penalties(total_score, missing_fields)
|
914 |
+
|
915 |
+
# Generate recommendations
|
916 |
+
recommendations = generate_field_recommendations(missing_fields)
|
917 |
+
|
918 |
+
return {
|
919 |
+
"total_score": penalty_result["adjusted_score"],
|
920 |
+
"section_scores": normalized_scores,
|
921 |
+
"max_scores": max_scores,
|
922 |
+
"field_checklist": field_checklist,
|
923 |
+
"field_categorization": get_field_categorization_for_display(aibom),
|
924 |
+
"field_tiers": {field: info["tier"] for field, info in FIELD_CLASSIFICATION.items()},
|
925 |
+
"missing_fields": missing_fields,
|
926 |
+
"completeness_profile": profile,
|
927 |
+
"penalty_applied": penalty_result["penalty_applied"],
|
928 |
+
"penalty_reason": penalty_result["penalty_reason"],
|
929 |
+
"recommendations": recommendations
|
930 |
+
}
|
931 |
+
|
932 |
+
|
933 |
+
def calculate_completeness_score(aibom: Dict[str, Any], validate: bool = True, use_best_practices: bool = True) -> Dict[str, Any]:
|
934 |
+
"""
|
935 |
+
Calculate completeness score for an AIBOM and optionally validate against AI requirements.
|
936 |
+
Enhanced with industry best practices scoring.
|
937 |
+
|
938 |
+
Args:
|
939 |
+
aibom: The AIBOM to score and validate
|
940 |
+
validate: Whether to perform validation
|
941 |
+
use_best_practices: Whether to use enhanced industry best practices scoring
|
942 |
+
|
943 |
+
Returns:
|
944 |
+
Dictionary containing score and validation results
|
945 |
+
"""
|
946 |
+
# If using best practices scoring, use the enhanced industry-neutral approach
|
947 |
+
if use_best_practices:
|
948 |
+
result = calculate_industry_neutral_score(aibom)
|
949 |
+
|
950 |
+
# Add validation if requested
|
951 |
+
if validate:
|
952 |
+
validation_result = validate_aibom(aibom)
|
953 |
+
result["validation"] = validation_result
|
954 |
+
|
955 |
+
# Adjust score based on validation results
|
956 |
+
if not validation_result["valid"]:
|
957 |
+
# Count errors and warnings
|
958 |
+
error_count = validation_result["summary"]["error_count"]
|
959 |
+
warning_count = validation_result["summary"]["warning_count"]
|
960 |
+
|
961 |
+
# Apply penalties to the score
|
962 |
+
if error_count > 0:
|
963 |
+
# Severe penalty for errors (up to 50% reduction)
|
964 |
+
error_penalty = min(0.5, error_count * 0.1)
|
965 |
+
result["total_score"] = round(result["total_score"] * (1 - error_penalty), 1)
|
966 |
+
result["validation_penalty"] = f"-{int(error_penalty * 100)}% due to {error_count} schema errors"
|
967 |
+
elif warning_count > 0:
|
968 |
+
# Minor penalty for warnings (up to 20% reduction)
|
969 |
+
warning_penalty = min(0.2, warning_count * 0.05)
|
970 |
+
result["total_score"] = round(result["total_score"] * (1 - warning_penalty), 1)
|
971 |
+
result["validation_penalty"] = f"-{int(warning_penalty * 100)}% due to {warning_count} schema warnings"
|
972 |
+
|
973 |
+
result = add_enhanced_field_display_to_result(result, aibom)
|
974 |
+
|
975 |
+
return result
|
976 |
+
|
977 |
+
# Otherwise, use the original scoring method
|
978 |
+
field_checklist = {}
|
979 |
+
max_scores = {
|
980 |
+
"required_fields": 20,
|
981 |
+
"metadata": 20,
|
982 |
+
"component_basic": 20,
|
983 |
+
"component_model_card": 30,
|
984 |
+
"external_references": 10
|
985 |
+
}
|
986 |
+
|
987 |
+
# Required Fields (20 points max)
|
988 |
+
required_fields = ["bomFormat", "specVersion", "serialNumber", "version"]
|
989 |
+
required_score = sum([5 if aibom.get(field) else 0 for field in required_fields])
|
990 |
+
for field in required_fields:
|
991 |
+
field_checklist[field] = "✔" if aibom.get(field) else "✘"
|
992 |
+
|
993 |
+
# Metadata (20 points max)
|
994 |
+
metadata = aibom.get("metadata", {})
|
995 |
+
metadata_fields = ["timestamp", "tools", "authors", "component"]
|
996 |
+
metadata_score = sum([5 if metadata.get(field) else 0 for field in metadata_fields])
|
997 |
+
for field in metadata_fields:
|
998 |
+
field_checklist[f"metadata.{field}"] = "✔" if metadata.get(field) else "✘"
|
999 |
+
|
1000 |
+
# Component Basic Info (20 points max)
|
1001 |
+
components = aibom.get("components", [])
|
1002 |
+
component_score = 0
|
1003 |
+
|
1004 |
+
if components:
|
1005 |
+
# Use the first component as specified in the design
|
1006 |
+
comp = components[0]
|
1007 |
+
comp_fields = ["type", "name", "bom-ref", "purl", "description", "licenses"]
|
1008 |
+
component_score = sum([
|
1009 |
+
2 if comp.get("type") else 0,
|
1010 |
+
4 if comp.get("name") else 0,
|
1011 |
+
2 if comp.get("bom-ref") else 0,
|
1012 |
+
4 if comp.get("purl") and re.match(r'^pkg:huggingface/.+', comp["purl"]) else 0,
|
1013 |
+
4 if comp.get("description") and len(comp["description"]) > 20 else 0,
|
1014 |
+
4 if comp.get("licenses") and validate_spdx(comp["licenses"]) else 0
|
1015 |
+
])
|
1016 |
+
for field in comp_fields:
|
1017 |
+
field_checklist[f"component.{field}"] = "✔" if comp.get(field) else "✘"
|
1018 |
+
if field == "purl" and comp.get(field) and not re.match(r'^pkg:huggingface/.+', comp["purl"]):
|
1019 |
+
field_checklist[f"component.{field}"] = "✘"
|
1020 |
+
if field == "description" and comp.get(field) and len(comp["description"]) <= 20:
|
1021 |
+
field_checklist[f"component.{field}"] = "✘"
|
1022 |
+
if field == "licenses" and comp.get(field) and not validate_spdx(comp["licenses"]):
|
1023 |
+
field_checklist[f"component.{field}"] = "✘"
|
1024 |
+
|
1025 |
+
# Model Card Section (30 points max)
|
1026 |
+
model_card_score = 0
|
1027 |
+
|
1028 |
+
if components:
|
1029 |
+
# Use the first component's model card as specified in the design
|
1030 |
+
comp = components[0]
|
1031 |
+
card = comp.get("modelCard", {})
|
1032 |
+
card_fields = ["modelParameters", "quantitativeAnalysis", "considerations"]
|
1033 |
+
model_card_score = sum([
|
1034 |
+
10 if card.get("modelParameters") else 0,
|
1035 |
+
10 if card.get("quantitativeAnalysis") else 0,
|
1036 |
+
10 if card.get("considerations") and isinstance(card["considerations"], dict) and len(str(card["considerations"])) > 50 else 0
|
1037 |
+
])
|
1038 |
+
for field in card_fields:
|
1039 |
+
field_checklist[f"modelCard.{field}"] = "✔" if field in card else "✘"
|
1040 |
+
if field == "considerations" and field in card and (not isinstance(card["considerations"], dict) or len(str(card["considerations"])) <= 50):
|
1041 |
+
field_checklist[f"modelCard.{field}"] = "✘"
|
1042 |
+
|
1043 |
+
# External References (10 points max)
|
1044 |
+
ext_refs = []
|
1045 |
+
if components and components[0].get("externalReferences"):
|
1046 |
+
ext_refs = components[0].get("externalReferences")
|
1047 |
+
ext_score = 0
|
1048 |
+
for ref in ext_refs:
|
1049 |
+
url = ref.get("url", "").lower()
|
1050 |
+
if "modelcard" in url:
|
1051 |
+
ext_score += 4
|
1052 |
+
elif "huggingface.co" in url or "github.com" in url:
|
1053 |
+
ext_score += 3
|
1054 |
+
elif "dataset" in url:
|
1055 |
+
ext_score += 3
|
1056 |
+
ext_score = min(ext_score, 10)
|
1057 |
+
field_checklist["externalReferences"] = "✔" if ext_refs else "✘"
|
1058 |
+
|
1059 |
+
# Calculate total score
|
1060 |
+
section_scores = {
|
1061 |
+
"required_fields": required_score,
|
1062 |
+
"metadata": metadata_score,
|
1063 |
+
"component_basic": component_score,
|
1064 |
+
"component_model_card": model_card_score,
|
1065 |
+
"external_references": ext_score
|
1066 |
+
}
|
1067 |
+
|
1068 |
+
# Calculate weighted total score
|
1069 |
+
total_score = (
|
1070 |
+
(section_scores["required_fields"] / max_scores["required_fields"]) * 20 +
|
1071 |
+
(section_scores["metadata"] / max_scores["metadata"]) * 20 +
|
1072 |
+
(section_scores["component_basic"] / max_scores["component_basic"]) * 20 +
|
1073 |
+
(section_scores["component_model_card"] / max_scores["component_model_card"]) * 30 +
|
1074 |
+
(section_scores["external_references"] / max_scores["external_references"]) * 10
|
1075 |
+
)
|
1076 |
+
|
1077 |
+
# Round to one decimal place
|
1078 |
+
total_score = round(total_score, 1)
|
1079 |
+
|
1080 |
+
# Ensure score is between 0 and 100
|
1081 |
+
total_score = max(0, min(total_score, 100))
|
1082 |
+
|
1083 |
+
result = {
|
1084 |
+
"total_score": total_score,
|
1085 |
+
"section_scores": section_scores,
|
1086 |
+
"max_scores": max_scores,
|
1087 |
+
"field_checklist": field_checklist
|
1088 |
+
}
|
1089 |
+
|
1090 |
+
# Add validation if requested
|
1091 |
+
if validate:
|
1092 |
+
validation_result = validate_aibom(aibom)
|
1093 |
+
result["validation"] = validation_result
|
1094 |
+
|
1095 |
+
# Adjust score based on validation results
|
1096 |
+
if not validation_result["valid"]:
|
1097 |
+
# Count errors and warnings
|
1098 |
+
error_count = validation_result["summary"]["error_count"]
|
1099 |
+
warning_count = validation_result["summary"]["warning_count"]
|
1100 |
+
|
1101 |
+
# Apply penalties to the score
|
1102 |
+
if error_count > 0:
|
1103 |
+
# Severe penalty for errors (up to 50% reduction)
|
1104 |
+
error_penalty = min(0.5, error_count * 0.1)
|
1105 |
+
result["total_score"] = round(result["total_score"] * (1 - error_penalty), 1)
|
1106 |
+
result["validation_penalty"] = f"-{int(error_penalty * 100)}% due to {error_count} schema errors"
|
1107 |
+
elif warning_count > 0:
|
1108 |
+
# Minor penalty for warnings (up to 20% reduction)
|
1109 |
+
warning_penalty = min(0.2, warning_count * 0.05)
|
1110 |
+
result["total_score"] = round(result["total_score"] * (1 - warning_penalty), 1)
|
1111 |
+
result["validation_penalty"] = f"-{int(warning_penalty * 100)}% due to {warning_count} schema warnings"
|
1112 |
+
|
1113 |
+
result = add_enhanced_field_display_to_result(result, aibom)
|
1114 |
+
|
1115 |
+
return result
|
1116 |
+
|
1117 |
+
|
1118 |
+
def merge_metadata(primary: Dict[str, Any], secondary: Dict[str, Any]) -> Dict[str, Any]:
|
1119 |
+
result = secondary.copy()
|
1120 |
+
for key, value in primary.items():
|
1121 |
+
if value is not None:
|
1122 |
+
if key in result and isinstance(value, dict) and isinstance(result[key], dict):
|
1123 |
+
result[key] = merge_metadata(value, result[key])
|
1124 |
+
else:
|
1125 |
+
result[key] = value
|
1126 |
+
return result
|
1127 |
+
|
1128 |
+
|
1129 |
+
def extract_model_id_parts(model_id: str) -> Dict[str, str]:
|
1130 |
+
parts = model_id.split("/")
|
1131 |
+
if len(parts) == 1:
|
1132 |
+
return {"owner": None, "name": parts[0]}
|
1133 |
+
return {"owner": parts[0], "name": "/".join(parts[1:])}
|
1134 |
+
|
1135 |
+
|
1136 |
+
def create_purl(model_id: str) -> str:
|
1137 |
+
parts = extract_model_id_parts(model_id)
|
1138 |
+
if parts["owner"]:
|
1139 |
+
return f"pkg:huggingface/{parts['owner']}/{parts['name']}"
|
1140 |
+
return f"pkg:huggingface/{parts['name']}"
|
1141 |
+
|
1142 |
+
|
1143 |
+
def get_field_categorization_for_display(aibom: Dict[str, Any]) -> Dict[str, Any]:
|
1144 |
+
"""
|
1145 |
+
Hardcoded field categorization with dynamic status detection.
|
1146 |
+
"""
|
1147 |
+
|
1148 |
+
# Standard CycloneDX Fields
|
1149 |
+
standard_cyclonedx_definitions = {
|
1150 |
+
"bomFormat": {"json_path": "bomFormat", "importance": "Critical"},
|
1151 |
+
"specVersion": {"json_path": "specVersion", "importance": "Critical"},
|
1152 |
+
"serialNumber": {"json_path": "serialNumber", "importance": "Critical"},
|
1153 |
+
"version": {"json_path": "version", "importance": "Critical"},
|
1154 |
+
"metadata.timestamp": {"json_path": "metadata.timestamp", "importance": "Important"},
|
1155 |
+
"metadata.tools": {"json_path": "metadata.tools", "importance": "Important"},
|
1156 |
+
"metadata.component": {"json_path": "metadata.component", "importance": "Important"},
|
1157 |
+
"component.type": {"json_path": "components[].type", "importance": "Important"},
|
1158 |
+
"component.name": {"json_path": "components[].name", "importance": "Critical"},
|
1159 |
+
"component.bom-ref": {"json_path": "components[].bom-ref", "importance": "Important"},
|
1160 |
+
"component.purl": {"json_path": "components[].purl", "importance": "Important"},
|
1161 |
+
"component.description": {"json_path": "components[].description", "importance": "Important"},
|
1162 |
+
"component.licenses": {"json_path": "components[].licenses", "importance": "Important"},
|
1163 |
+
"externalReferences": {"json_path": "components[].externalReferences", "importance": "Supplementary"},
|
1164 |
+
"downloadLocation": {"json_path": "components[].externalReferences[].url", "importance": "Critical"},
|
1165 |
+
}
|
1166 |
+
|
1167 |
+
# AI-Specific Extension Fields
|
1168 |
+
ai_specific_definitions = {
|
1169 |
+
# Model card structure fields
|
1170 |
+
"modelCard.modelParameters": {"json_path": "components[].modelCard.modelParameters", "importance": "Important"},
|
1171 |
+
"modelCard.quantitativeAnalysis": {"json_path": "components[].modelCard.quantitativeAnalysis", "importance": "Important"},
|
1172 |
+
"modelCard.considerations": {"json_path": "components[].modelCard.considerations", "importance": "Important"},
|
1173 |
+
|
1174 |
+
# Properties-based fields
|
1175 |
+
"primaryPurpose": {"json_path": "metadata.properties[].name=\"primaryPurpose\"", "importance": "Critical"},
|
1176 |
+
"suppliedBy": {"json_path": "metadata.properties[].name=\"suppliedBy\"", "importance": "Critical"},
|
1177 |
+
"typeOfModel": {"json_path": "components[].modelCard.properties[].name=\"typeOfModel\"", "importance": "Important"},
|
1178 |
+
"energyConsumption": {"json_path": "components[].modelCard.properties[].name=\"energyConsumption\"", "importance": "Important"},
|
1179 |
+
"hyperparameter": {"json_path": "components[].modelCard.properties[].name=\"hyperparameter\"", "importance": "Important"},
|
1180 |
+
"limitation": {"json_path": "components[].modelCard.properties[].name=\"limitation\"", "importance": "Important"},
|
1181 |
+
"safetyRiskAssessment": {"json_path": "components[].modelCard.properties[].name=\"safetyRiskAssessment\"", "importance": "Important"},
|
1182 |
+
"modelExplainability": {"json_path": "components[].modelCard.properties[].name=\"modelExplainability\"", "importance": "Supplementary"},
|
1183 |
+
"standardCompliance": {"json_path": "components[].modelCard.properties[].name=\"standardCompliance\"", "importance": "Supplementary"},
|
1184 |
+
"domain": {"json_path": "components[].modelCard.properties[].name=\"domain\"", "importance": "Supplementary"},
|
1185 |
+
"energyQuantity": {"json_path": "components[].modelCard.properties[].name=\"energyQuantity\"", "importance": "Supplementary"},
|
1186 |
+
"energyUnit": {"json_path": "components[].modelCard.properties[].name=\"energyUnit\"", "importance": "Supplementary"},
|
1187 |
+
"informationAboutTraining": {"json_path": "components[].modelCard.properties[].name=\"informationAboutTraining\"", "importance": "Supplementary"},
|
1188 |
+
"informationAboutApplication": {"json_path": "components[].modelCard.properties[].name=\"informationAboutApplication\"", "importance": "Supplementary"},
|
1189 |
+
"metric": {"json_path": "components[].modelCard.properties[].name=\"metric\"", "importance": "Supplementary"},
|
1190 |
+
"metricDecisionThreshold": {"json_path": "components[].modelCard.properties[].name=\"metricDecisionThreshold\"", "importance": "Supplementary"},
|
1191 |
+
"modelDataPreprocessing": {"json_path": "components[].modelCard.properties[].name=\"modelDataPreprocessing\"", "importance": "Supplementary"},
|
1192 |
+
"autonomyType": {"json_path": "components[].modelCard.properties[].name=\"autonomyType\"", "importance": "Supplementary"},
|
1193 |
+
"useSensitivePersonalInformation": {"json_path": "components[].modelCard.properties[].name=\"useSensitivePersonalInformation\"", "importance": "Supplementary"},
|
1194 |
+
}
|
1195 |
+
|
1196 |
+
# DYNAMIC: Check status for each field
|
1197 |
+
def check_field_presence(field_key):
|
1198 |
+
"""Simple field presence detection"""
|
1199 |
+
if field_key == "bomFormat":
|
1200 |
+
return "bomFormat" in aibom
|
1201 |
+
elif field_key == "specVersion":
|
1202 |
+
return "specVersion" in aibom
|
1203 |
+
elif field_key == "serialNumber":
|
1204 |
+
return "serialNumber" in aibom
|
1205 |
+
elif field_key == "version":
|
1206 |
+
return "version" in aibom
|
1207 |
+
elif field_key == "metadata.timestamp":
|
1208 |
+
return "metadata" in aibom and "timestamp" in aibom["metadata"]
|
1209 |
+
elif field_key == "metadata.tools":
|
1210 |
+
return "metadata" in aibom and "tools" in aibom["metadata"]
|
1211 |
+
elif field_key == "metadata.component":
|
1212 |
+
return "metadata" in aibom and "component" in aibom["metadata"]
|
1213 |
+
elif field_key == "component.type":
|
1214 |
+
return "components" in aibom and aibom["components"] and "type" in aibom["components"][0]
|
1215 |
+
elif field_key == "component.name":
|
1216 |
+
return "components" in aibom and aibom["components"] and "name" in aibom["components"][0]
|
1217 |
+
elif field_key == "component.bom-ref":
|
1218 |
+
return "components" in aibom and aibom["components"] and "bom-ref" in aibom["components"][0]
|
1219 |
+
elif field_key == "component.purl":
|
1220 |
+
return "components" in aibom and aibom["components"] and "purl" in aibom["components"][0]
|
1221 |
+
elif field_key == "component.description":
|
1222 |
+
return "components" in aibom and aibom["components"] and "description" in aibom["components"][0]
|
1223 |
+
elif field_key == "component.licenses":
|
1224 |
+
return "components" in aibom and aibom["components"] and "licenses" in aibom["components"][0]
|
1225 |
+
elif field_key == "externalReferences":
|
1226 |
+
return ("externalReferences" in aibom or
|
1227 |
+
("components" in aibom and aibom["components"] and "externalReferences" in aibom["components"][0]))
|
1228 |
+
elif field_key == "downloadLocation":
|
1229 |
+
if "externalReferences" in aibom:
|
1230 |
+
for ref in aibom["externalReferences"]:
|
1231 |
+
if ref.get("type") == "distribution":
|
1232 |
+
return True
|
1233 |
+
if "components" in aibom and aibom["components"] and "externalReferences" in aibom["components"][0]:
|
1234 |
+
return len(aibom["components"][0]["externalReferences"]) > 0
|
1235 |
+
return False
|
1236 |
+
elif field_key == "modelCard.modelParameters":
|
1237 |
+
return ("components" in aibom and aibom["components"] and
|
1238 |
+
"modelCard" in aibom["components"][0] and
|
1239 |
+
"modelParameters" in aibom["components"][0]["modelCard"])
|
1240 |
+
elif field_key == "modelCard.quantitativeAnalysis":
|
1241 |
+
return ("components" in aibom and aibom["components"] and
|
1242 |
+
"modelCard" in aibom["components"][0] and
|
1243 |
+
"quantitativeAnalysis" in aibom["components"][0]["modelCard"])
|
1244 |
+
elif field_key == "modelCard.considerations":
|
1245 |
+
return ("components" in aibom and aibom["components"] and
|
1246 |
+
"modelCard" in aibom["components"][0] and
|
1247 |
+
"considerations" in aibom["components"][0]["modelCard"])
|
1248 |
+
elif field_key == "primaryPurpose":
|
1249 |
+
if "metadata" in aibom and "properties" in aibom["metadata"]:
|
1250 |
+
for prop in aibom["metadata"]["properties"]:
|
1251 |
+
if prop.get("name") == "primaryPurpose":
|
1252 |
+
return True
|
1253 |
+
return False
|
1254 |
+
elif field_key == "suppliedBy":
|
1255 |
+
if "metadata" in aibom and "properties" in aibom["metadata"]:
|
1256 |
+
for prop in aibom["metadata"]["properties"]:
|
1257 |
+
if prop.get("name") == "suppliedBy":
|
1258 |
+
return True
|
1259 |
+
return False
|
1260 |
+
elif field_key == "typeOfModel":
|
1261 |
+
if ("components" in aibom and aibom["components"] and
|
1262 |
+
"modelCard" in aibom["components"][0] and
|
1263 |
+
"properties" in aibom["components"][0]["modelCard"]):
|
1264 |
+
for prop in aibom["components"][0]["modelCard"]["properties"]:
|
1265 |
+
if prop.get("name") == "typeOfModel":
|
1266 |
+
return True
|
1267 |
+
return False
|
1268 |
+
else:
|
1269 |
+
# For other AI-specific fields, check in modelCard properties
|
1270 |
+
if ("components" in aibom and aibom["components"] and
|
1271 |
+
"modelCard" in aibom["components"][0] and
|
1272 |
+
"properties" in aibom["components"][0]["modelCard"]):
|
1273 |
+
for prop in aibom["components"][0]["modelCard"]["properties"]:
|
1274 |
+
if prop.get("name") == field_key:
|
1275 |
+
return True
|
1276 |
+
return False
|
1277 |
+
|
1278 |
+
# Build result with dynamic status
|
1279 |
+
standard_fields = {}
|
1280 |
+
for field_key, field_info in standard_cyclonedx_definitions.items():
|
1281 |
+
standard_fields[field_key] = {
|
1282 |
+
"status": "✔" if check_field_presence(field_key) else "✘",
|
1283 |
+
"field_name": field_key,
|
1284 |
+
"json_path": field_info["json_path"],
|
1285 |
+
"importance": field_info["importance"]
|
1286 |
+
}
|
1287 |
+
|
1288 |
+
ai_fields = {}
|
1289 |
+
for field_key, field_info in ai_specific_definitions.items():
|
1290 |
+
ai_fields[field_key] = {
|
1291 |
+
"status": "✔" if check_field_presence(field_key) else "✘",
|
1292 |
+
"field_name": field_key,
|
1293 |
+
"json_path": field_info["json_path"],
|
1294 |
+
"importance": field_info["importance"]
|
1295 |
+
}
|
1296 |
+
|
1297 |
+
return {
|
1298 |
+
"standard_cyclonedx_fields": standard_fields,
|
1299 |
+
"ai_specific_extension_fields": ai_fields
|
1300 |
+
}
|
1301 |
+
|
1302 |
+
|
1303 |
+
def add_enhanced_field_display_to_result(result: Dict[str, Any], aibom: Dict[str, Any]) -> Dict[str, Any]:
|
1304 |
+
"""Add field categorization to result"""
|
1305 |
+
enhanced_result = result.copy()
|
1306 |
+
enhanced_result["field_display"] = get_field_categorization_for_display(aibom)
|
1307 |
+
return enhanced_result
|
templates/error.html
ADDED
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Error Generating AI SBOM</title>
|
7 |
+
<style>
|
8 |
+
body {
|
9 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
10 |
+
margin: 0;
|
11 |
+
padding: 0;
|
12 |
+
line-height: 1.6;
|
13 |
+
color: #333;
|
14 |
+
background-color: #f9f9f9;
|
15 |
+
}
|
16 |
+
.container {
|
17 |
+
max-width: 1000px;
|
18 |
+
margin: 0 auto;
|
19 |
+
padding: 0 20px;
|
20 |
+
}
|
21 |
+
|
22 |
+
/* Header styling */
|
23 |
+
.header {
|
24 |
+
background-color: #ffffff;
|
25 |
+
padding: 15px 20px;
|
26 |
+
border-bottom: 1px solid #e9ecef;
|
27 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
28 |
+
display: flex;
|
29 |
+
align-items: center;
|
30 |
+
margin-bottom: 30px;
|
31 |
+
}
|
32 |
+
.header img {
|
33 |
+
height: 60px;
|
34 |
+
margin-right: 15px;
|
35 |
+
}
|
36 |
+
/* Added header-content div for layout */
|
37 |
+
.header .header-content {
|
38 |
+
display: flex;
|
39 |
+
flex-direction: column; /* Stack title and count */
|
40 |
+
}
|
41 |
+
.header h1 {
|
42 |
+
margin: 0;
|
43 |
+
font-size: 28px;
|
44 |
+
color: #2c3e50;
|
45 |
+
font-weight: 600;
|
46 |
+
margin-bottom: 5px; /* Space between title and count */
|
47 |
+
}
|
48 |
+
/* Added style for sbom-count */
|
49 |
+
.header .sbom-count {
|
50 |
+
font-size: 14px;
|
51 |
+
color: #555;
|
52 |
+
font-weight: 500;
|
53 |
+
}
|
54 |
+
|
55 |
+
/* Content styling */
|
56 |
+
.content-section {
|
57 |
+
background-color: #ffffff;
|
58 |
+
border-radius: 8px;
|
59 |
+
padding: 25px;
|
60 |
+
margin-bottom: 30px;
|
61 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
62 |
+
}
|
63 |
+
|
64 |
+
.content-section h2 {
|
65 |
+
color: #2c3e50;
|
66 |
+
margin-top: 0;
|
67 |
+
margin-bottom: 20px;
|
68 |
+
font-size: 22px;
|
69 |
+
border-bottom: 2px solid #f0f0f0;
|
70 |
+
padding-bottom: 10px;
|
71 |
+
}
|
72 |
+
|
73 |
+
.content-section p {
|
74 |
+
margin-bottom: 20px;
|
75 |
+
font-size: 16px;
|
76 |
+
line-height: 1.7;
|
77 |
+
color: #555;
|
78 |
+
}
|
79 |
+
|
80 |
+
/* Error styling */
|
81 |
+
.error-section {
|
82 |
+
background-color: #ffffff;
|
83 |
+
border-radius: 8px;
|
84 |
+
padding: 25px;
|
85 |
+
margin-bottom: 30px;
|
86 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
87 |
+
}
|
88 |
+
|
89 |
+
.error-section h2 {
|
90 |
+
color: #e74c3c;
|
91 |
+
margin-top: 0;
|
92 |
+
margin-bottom: 20px;
|
93 |
+
font-size: 22px;
|
94 |
+
border-bottom: 2px solid #f0f0f0;
|
95 |
+
padding-bottom: 10px;
|
96 |
+
}
|
97 |
+
|
98 |
+
.error-message {
|
99 |
+
background-color: #ffebee;
|
100 |
+
border-left: 4px solid #e74c3c;
|
101 |
+
padding: 15px;
|
102 |
+
border-radius: 4px;
|
103 |
+
margin: 20px 0;
|
104 |
+
font-size: 16px;
|
105 |
+
line-height: 1.7;
|
106 |
+
color: #555;
|
107 |
+
}
|
108 |
+
|
109 |
+
/* Button styling */
|
110 |
+
.button {
|
111 |
+
display: inline-block;
|
112 |
+
padding: 12px 20px;
|
113 |
+
background-color: #3498db;
|
114 |
+
color: white;
|
115 |
+
border: none;
|
116 |
+
border-radius: 6px;
|
117 |
+
cursor: pointer;
|
118 |
+
font-size: 15px;
|
119 |
+
font-weight: 500;
|
120 |
+
text-decoration: none;
|
121 |
+
transition: background-color 0.3s;
|
122 |
+
margin-bottom: 20px;
|
123 |
+
}
|
124 |
+
|
125 |
+
.button:hover {
|
126 |
+
background-color: #2980b9;
|
127 |
+
text-decoration: none;
|
128 |
+
}
|
129 |
+
|
130 |
+
/* Support section styling */
|
131 |
+
.support-section {
|
132 |
+
background-color: #ffffff;
|
133 |
+
border-radius: 8px;
|
134 |
+
padding: 25px;
|
135 |
+
margin-bottom: 30px;
|
136 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
137 |
+
}
|
138 |
+
|
139 |
+
.support-section h2 {
|
140 |
+
color: #2c3e50;
|
141 |
+
margin-top: 0;
|
142 |
+
margin-bottom: 20px;
|
143 |
+
font-size: 22px;
|
144 |
+
border-bottom: 2px solid #f0f0f0;
|
145 |
+
padding-bottom: 10px;
|
146 |
+
}
|
147 |
+
|
148 |
+
.support-section p {
|
149 |
+
margin-bottom: 20px;
|
150 |
+
font-size: 16px;
|
151 |
+
line-height: 1.7;
|
152 |
+
color: #555;
|
153 |
+
}
|
154 |
+
|
155 |
+
a {
|
156 |
+
color: #3498db;
|
157 |
+
text-decoration: none;
|
158 |
+
transition: color 0.3s;
|
159 |
+
}
|
160 |
+
|
161 |
+
a:hover {
|
162 |
+
color: #2980b9;
|
163 |
+
text-decoration: underline;
|
164 |
+
}
|
165 |
+
|
166 |
+
/* Footer styling */
|
167 |
+
.footer {
|
168 |
+
text-align: center;
|
169 |
+
padding: 20px;
|
170 |
+
color: #7f8c8d;
|
171 |
+
font-size: 14px;
|
172 |
+
margin-top: 30px;
|
173 |
+
}
|
174 |
+
</style>
|
175 |
+
</head>
|
176 |
+
<body>
|
177 |
+
<!-- Header with logo, title, and SBOM count -->
|
178 |
+
<div class="header">
|
179 |
+
<a href="https://aetheris.ai/" target="_blank">
|
180 |
+
<img src="https://huggingface.co/spaces/aetheris-ai/aibom-generator/resolve/main/templates/images/AetherisAI-logo.png" alt="Aetheris AI Logo">
|
181 |
+
</a>
|
182 |
+
<!-- Added header-content div -->
|
183 |
+
<div class="header-content">
|
184 |
+
<h1>AI SBOM Generator</h1>
|
185 |
+
</div>
|
186 |
+
</div>
|
187 |
+
|
188 |
+
<div class="container">
|
189 |
+
<!-- Error Section -->
|
190 |
+
<div class="error-section">
|
191 |
+
<h2>Error Generating AI SBOM</h2>
|
192 |
+
<div class="error-message">
|
193 |
+
<p>{{ error }}</p>
|
194 |
+
</div>
|
195 |
+
<a href="/" class="button">Try Again</a>
|
196 |
+
</div>
|
197 |
+
|
198 |
+
<!-- Support Section -->
|
199 |
+
<div class="support-section">
|
200 |
+
<h2>Need Help?</h2>
|
201 |
+
<p>If the error persists, please log an issue on our <a href="https://github.com/aetheris-ai/aibom-generator/issues" target="_blank" rel="noopener noreferrer">GitHub issues page</a>. Include the error message above and any additional details that might help us troubleshoot the problem.</p>
|
202 |
+
</div>
|
203 |
+
|
204 |
+
<!-- Info Section -->
|
205 |
+
<div class="content-section" style="text-align: center;>
|
206 |
+
<!-- Display the SBOM count -->
|
207 |
+
<div class="sbom-count">🚀 Generated AI SBOMs using this tool: <strong>{{ sbom_count if sbom_count else 'N/A' }}</strong></div>
|
208 |
+
</div>
|
209 |
+
|
210 |
+
<!-- Footer -->
|
211 |
+
<div class="footer">
|
212 |
+
<p>© 2025 AI SBOM Generator | Powered by Aetheris AI</p>
|
213 |
+
</div>
|
214 |
+
</div>
|
215 |
+
</body>
|
216 |
+
</html>
|
templates/improved_scoring_template.html
ADDED
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<title>AIBOM Generated - Improved Scoring</title>
|
6 |
+
<style>
|
7 |
+
body { font-family: Arial, sans-serif; margin: 20px; color: #333; }
|
8 |
+
h2, h3 { color: #2c3e50; }
|
9 |
+
|
10 |
+
/* Table styles */
|
11 |
+
table { border-collapse: collapse; width: 100%; margin: 15px 0 25px 0; }
|
12 |
+
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
13 |
+
th { background-color: #f4f4f4; }
|
14 |
+
|
15 |
+
/* Progress bar styles */
|
16 |
+
.progress-container {
|
17 |
+
width: 100%;
|
18 |
+
background-color: #f1f1f1;
|
19 |
+
border-radius: 5px;
|
20 |
+
margin: 5px 0;
|
21 |
+
}
|
22 |
+
.progress-bar {
|
23 |
+
height: 24px;
|
24 |
+
border-radius: 5px;
|
25 |
+
display: flex;
|
26 |
+
align-items: center;
|
27 |
+
justify-content: center;
|
28 |
+
color: white;
|
29 |
+
font-weight: bold;
|
30 |
+
transition: width 1s;
|
31 |
+
}
|
32 |
+
.excellent { background-color: #27ae60; }
|
33 |
+
.good { background-color: #2980b9; }
|
34 |
+
.fair { background-color: #f39c12; }
|
35 |
+
.poor { background-color: #e74c3c; }
|
36 |
+
|
37 |
+
/* Field checklist styles */
|
38 |
+
.field-list { list-style: none; padding-left: 0; }
|
39 |
+
.missing { color: #e74c3c; }
|
40 |
+
.present { color: #27ae60; }
|
41 |
+
|
42 |
+
/* Improvement section styles */
|
43 |
+
.improvement {
|
44 |
+
color: #2c3e50;
|
45 |
+
background-color: #ecf0f1;
|
46 |
+
padding: 15px;
|
47 |
+
border-radius: 5px;
|
48 |
+
margin-bottom: 20px;
|
49 |
+
}
|
50 |
+
.improvement-value { color: #27ae60; font-weight: bold; }
|
51 |
+
.ai-badge {
|
52 |
+
background-color: #3498db;
|
53 |
+
color: white;
|
54 |
+
padding: 3px 8px;
|
55 |
+
border-radius: 3px;
|
56 |
+
font-size: 0.8em;
|
57 |
+
margin-left: 10px;
|
58 |
+
}
|
59 |
+
|
60 |
+
/* Score explanation styles */
|
61 |
+
.score-explanation {
|
62 |
+
background-color: #f8f9fa;
|
63 |
+
border: 1px solid #e9ecef;
|
64 |
+
border-radius: 5px;
|
65 |
+
padding: 15px;
|
66 |
+
margin: 20px 0;
|
67 |
+
}
|
68 |
+
.calculation-step {
|
69 |
+
font-family: monospace;
|
70 |
+
margin: 5px 0;
|
71 |
+
}
|
72 |
+
.weight-indicator {
|
73 |
+
font-size: 0.9em;
|
74 |
+
color: #7f8c8d;
|
75 |
+
margin-left: 5px;
|
76 |
+
}
|
77 |
+
|
78 |
+
/* Collapsible section styles */
|
79 |
+
.collapsible {
|
80 |
+
background-color: #f1f1f1;
|
81 |
+
color: #444;
|
82 |
+
cursor: pointer;
|
83 |
+
padding: 18px;
|
84 |
+
width: 100%;
|
85 |
+
border: none;
|
86 |
+
text-align: left;
|
87 |
+
outline: none;
|
88 |
+
font-size: 15px;
|
89 |
+
border-radius: 5px;
|
90 |
+
margin: 10px 0;
|
91 |
+
}
|
92 |
+
.active, .collapsible:hover {
|
93 |
+
background-color: #e0e0e0;
|
94 |
+
}
|
95 |
+
.content {
|
96 |
+
padding: 0 18px;
|
97 |
+
max-height: 0;
|
98 |
+
overflow: hidden;
|
99 |
+
transition: max-height 0.2s ease-out;
|
100 |
+
background-color: #f9f9f9;
|
101 |
+
border-radius: 0 0 5px 5px;
|
102 |
+
}
|
103 |
+
</style>
|
104 |
+
</head>
|
105 |
+
<body>
|
106 |
+
<a href="/">Generate another AI SBOM</a>
|
107 |
+
<h2>AI SBOM Generated for {{ model_id }}</h2>
|
108 |
+
|
109 |
+
{% if enhancement_report and enhancement_report.ai_enhanced %}
|
110 |
+
<div class="improvement">
|
111 |
+
<h3>AI Enhancement Results</h3>
|
112 |
+
<p>This AIBOM was enhanced using <strong>{{ enhancement_report.ai_model }}</strong></p>
|
113 |
+
|
114 |
+
<p>Original Score:
|
115 |
+
<div class="progress-container">
|
116 |
+
<div class="progress-bar {% if enhancement_report.original_score.total_score >= 80 %}excellent{% elif enhancement_report.original_score.total_score >= 60 %}good{% elif enhancement_report.original_score.total_score >= 40 %}fair{% else %}poor{% endif %}"
|
117 |
+
style="width: {{ enhancement_report.original_score.total_score }}%">
|
118 |
+
{{ enhancement_report.original_score.total_score }}%
|
119 |
+
</div>
|
120 |
+
</div>
|
121 |
+
</p>
|
122 |
+
|
123 |
+
<p>Enhanced Score:
|
124 |
+
<div class="progress-container">
|
125 |
+
<div class="progress-bar {% if enhancement_report.final_score.total_score >= 80 %}excellent{% elif enhancement_report.final_score.total_score >= 60 %}good{% elif enhancement_report.final_score.total_score >= 40 %}fair{% else %}poor{% endif %}"
|
126 |
+
style="width: {{ enhancement_report.final_score.total_score }}%">
|
127 |
+
{{ enhancement_report.final_score.total_score }}%
|
128 |
+
</div>
|
129 |
+
</div>
|
130 |
+
</p>
|
131 |
+
|
132 |
+
<p>Improvement: <span class="improvement-value">+{{ enhancement_report.improvement }} points</span></p>
|
133 |
+
</div>
|
134 |
+
{% endif %}
|
135 |
+
|
136 |
+
<h3>Overall AIBOM Completeness
|
137 |
+
{% if enhancement_report and enhancement_report.ai_enhanced %}
|
138 |
+
<span class="ai-badge">AI Enhanced</span>
|
139 |
+
{% endif %}
|
140 |
+
</h3>
|
141 |
+
|
142 |
+
<div class="progress-container">
|
143 |
+
<div class="progress-bar {% if completeness_score.total_score >= 80 %}excellent{% elif completeness_score.total_score >= 60 %}good{% elif completeness_score.total_score >= 40 %}fair{% else %}poor{% endif %}"
|
144 |
+
style="width: {{ completeness_score.total_score }}%">
|
145 |
+
{{ completeness_score.total_score }}%
|
146 |
+
</div>
|
147 |
+
</div>
|
148 |
+
|
149 |
+
<p>
|
150 |
+
{% if completeness_score.total_score >= 80 %}
|
151 |
+
<strong>Excellent:</strong> This AIBOM is very comprehensive and provides thorough documentation.
|
152 |
+
{% elif completeness_score.total_score >= 60 %}
|
153 |
+
<strong>Good:</strong> This AIBOM contains most essential information but could be improved.
|
154 |
+
{% elif completeness_score.total_score >= 40 %}
|
155 |
+
<strong>Fair:</strong> This AIBOM has basic information but is missing several important details.
|
156 |
+
{% else %}
|
157 |
+
<strong>Needs Improvement:</strong> This AIBOM is missing critical information and requires significant enhancement.
|
158 |
+
{% endif %}
|
159 |
+
</p>
|
160 |
+
|
161 |
+
<h3>Section Completion</h3>
|
162 |
+
<table>
|
163 |
+
<thead>
|
164 |
+
<tr>
|
165 |
+
<th>Section</th>
|
166 |
+
<th>Completion</th>
|
167 |
+
<th>Weight</th>
|
168 |
+
<th>Contribution</th>
|
169 |
+
</tr>
|
170 |
+
</thead>
|
171 |
+
<tbody>
|
172 |
+
{% for section, score in completeness_score.section_scores.items() %}
|
173 |
+
{% set max_score = completeness_score.max_scores[section] %}
|
174 |
+
{% set percentage = (score / max_score * 100) | round %}
|
175 |
+
{% set weight = 0.2 if section == 'required_fields' else 0.2 if section == 'metadata' else 0.2 if section == 'component_basic' else 0.3 if section == 'component_model_card' else 0.1 %}
|
176 |
+
{% set contribution = (score * weight) | round(1) %}
|
177 |
+
<tr>
|
178 |
+
<td>{{ section | replace('_', ' ') | title }}</td>
|
179 |
+
<td>
|
180 |
+
<div class="progress-container">
|
181 |
+
<div class="progress-bar {% if percentage >= 80 %}excellent{% elif percentage >= 60 %}good{% elif percentage >= 40 %}fair{% else %}poor{% endif %}"
|
182 |
+
style="width: {{ percentage }}%">
|
183 |
+
{{ score }}/{{ max_score }} ({{ percentage }}%)
|
184 |
+
</div>
|
185 |
+
</div>
|
186 |
+
</td>
|
187 |
+
<td>{{ (weight * 100) | int }}%</td>
|
188 |
+
<td>{{ contribution }} points</td>
|
189 |
+
</tr>
|
190 |
+
{% endfor %}
|
191 |
+
</tbody>
|
192 |
+
</table>
|
193 |
+
|
194 |
+
<button class="collapsible">How is the score calculated?</button>
|
195 |
+
<div class="content">
|
196 |
+
<div class="score-explanation">
|
197 |
+
<h4>Score Calculation Breakdown</h4>
|
198 |
+
<p>The overall score is a weighted average of section scores:</p>
|
199 |
+
|
200 |
+
<div class="calculation-step">Required Fields: {{ completeness_score.section_scores.required_fields }} × 0.20 = {{ (completeness_score.section_scores.required_fields * 0.2) | round(1) }} points</div>
|
201 |
+
<div class="calculation-step">Metadata: {{ completeness_score.section_scores.metadata }} × 0.20 = {{ (completeness_score.section_scores.metadata * 0.2) | round(1) }} points</div>
|
202 |
+
<div class="calculation-step">Component Basic: {{ completeness_score.section_scores.component_basic }} × 0.20 = {{ (completeness_score.section_scores.component_basic * 0.2) | round(1) }} points</div>
|
203 |
+
<div class="calculation-step">Model Card: {{ completeness_score.section_scores.component_model_card }} × 0.30 = {{ (completeness_score.section_scores.component_model_card * 0.3) | round(1) }} points</div>
|
204 |
+
<div class="calculation-step">External References: {{ completeness_score.section_scores.external_references }} × 0.10 = {{ (completeness_score.section_scores.external_references * 0.1) | round(1) }} points</div>
|
205 |
+
<div class="calculation-step"><strong>Total: {{ completeness_score.total_score }} points</strong></div>
|
206 |
+
|
207 |
+
<p>Each section has a different weight in the final calculation to reflect its importance:</p>
|
208 |
+
<ul>
|
209 |
+
<li>Required Fields: 20% weight</li>
|
210 |
+
<li>Metadata: 20% weight</li>
|
211 |
+
<li>Component Basic: 20% weight</li>
|
212 |
+
<li>Model Card: 30% weight (higher weight as it contains critical AI information)</li>
|
213 |
+
<li>External References: 10% weight</li>
|
214 |
+
</ul>
|
215 |
+
</div>
|
216 |
+
</div>
|
217 |
+
|
218 |
+
<h3>Field Checklist</h3>
|
219 |
+
<ul class="field-list">
|
220 |
+
{% for field, status in completeness_score.field_checklist.items() %}
|
221 |
+
{% if status == "✔" %}
|
222 |
+
<li class="present">{{ status }} {{ field }}</li>
|
223 |
+
{% else %}
|
224 |
+
<li class="missing">{{ status }} {{ field }}</li>
|
225 |
+
{% endif %}
|
226 |
+
{% endfor %}
|
227 |
+
</ul>
|
228 |
+
|
229 |
+
<h3>
|
230 |
+
Download AI SBOM in CycloneDX format for {{ model_id }}
|
231 |
+
<button onclick="downloadJSON()">Download JSON</button>
|
232 |
+
</h3>
|
233 |
+
|
234 |
+
<pre id="aibom-json">{{ aibom | tojson(indent=2) }}</pre>
|
235 |
+
|
236 |
+
<script>
|
237 |
+
function downloadJSON() {
|
238 |
+
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(document.getElementById('aibom-json').textContent);
|
239 |
+
const downloadAnchorNode = document.createElement('a');
|
240 |
+
downloadAnchorNode.setAttribute("href", dataStr);
|
241 |
+
downloadAnchorNode.setAttribute("download", "{{ model_id }}-aibom.json");
|
242 |
+
document.body.appendChild(downloadAnchorNode);
|
243 |
+
downloadAnchorNode.click();
|
244 |
+
downloadAnchorNode.remove();
|
245 |
+
}
|
246 |
+
|
247 |
+
// Collapsible sections
|
248 |
+
var coll = document.getElementsByClassName("collapsible");
|
249 |
+
for (var i = 0; i < coll.length; i++) {
|
250 |
+
coll[i].addEventListener("click", function() {
|
251 |
+
this.classList.toggle("active");
|
252 |
+
var content = this.nextElementSibling;
|
253 |
+
if (content.style.maxHeight) {
|
254 |
+
content.style.maxHeight = null;
|
255 |
+
} else {
|
256 |
+
content.style.maxHeight = content.scrollHeight + "px";
|
257 |
+
}
|
258 |
+
});
|
259 |
+
}
|
260 |
+
</script>
|
261 |
+
</body>
|
262 |
+
</html>
|
templates/index.html
ADDED
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>AI SBOM Generator</title>
|
7 |
+
<style>
|
8 |
+
body {
|
9 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
10 |
+
margin: 0;
|
11 |
+
padding: 0;
|
12 |
+
line-height: 1.6;
|
13 |
+
color: #333;
|
14 |
+
background-color: #f9f9f9;
|
15 |
+
}
|
16 |
+
.container {
|
17 |
+
max-width: 1000px;
|
18 |
+
margin: 0 auto;
|
19 |
+
padding: 0 20px;
|
20 |
+
}
|
21 |
+
|
22 |
+
/* Header styling */
|
23 |
+
.header {
|
24 |
+
background-color: #ffffff;
|
25 |
+
padding: 15px 20px;
|
26 |
+
border-bottom: 1px solid #e9ecef;
|
27 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
28 |
+
display: flex;
|
29 |
+
align-items: center;
|
30 |
+
margin-bottom: 30px;
|
31 |
+
}
|
32 |
+
.header img {
|
33 |
+
height: 60px;
|
34 |
+
margin-right: 15px;
|
35 |
+
}
|
36 |
+
/* Added header-content div for layout */
|
37 |
+
.header .header-content {
|
38 |
+
display: flex;
|
39 |
+
flex-direction: column; /* Stack title and count */
|
40 |
+
}
|
41 |
+
.header h1 {
|
42 |
+
margin: 0;
|
43 |
+
font-size: 28px;
|
44 |
+
color: #2c3e50;
|
45 |
+
font-weight: 600;
|
46 |
+
margin-bottom: 5px; /* Space between title and count */
|
47 |
+
}
|
48 |
+
/* Added style for sbom-count */
|
49 |
+
.header .sbom-count {
|
50 |
+
font-size: 14px;
|
51 |
+
color: #555;
|
52 |
+
font-weight: 500;
|
53 |
+
}
|
54 |
+
|
55 |
+
/* Content styling */
|
56 |
+
.content-section {
|
57 |
+
background-color: #ffffff;
|
58 |
+
border-radius: 8px;
|
59 |
+
padding: 25px;
|
60 |
+
margin-bottom: 30px;
|
61 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
62 |
+
}
|
63 |
+
|
64 |
+
.content-section h2 {
|
65 |
+
color: #2c3e50;
|
66 |
+
margin-top: 0;
|
67 |
+
margin-bottom: 20px;
|
68 |
+
font-size: 22px;
|
69 |
+
border-bottom: 2px solid #f0f0f0;
|
70 |
+
padding-bottom: 10px;
|
71 |
+
}
|
72 |
+
|
73 |
+
.content-section p {
|
74 |
+
margin-bottom: 20px;
|
75 |
+
font-size: 16px;
|
76 |
+
line-height: 1.7;
|
77 |
+
color: #555;
|
78 |
+
}
|
79 |
+
|
80 |
+
/* Form styling */
|
81 |
+
.form-section {
|
82 |
+
background-color: #ffffff;
|
83 |
+
border-radius: 8px;
|
84 |
+
padding: 25px;
|
85 |
+
margin-bottom: 30px;
|
86 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
87 |
+
}
|
88 |
+
|
89 |
+
.form-section p {
|
90 |
+
margin-bottom: 20px;
|
91 |
+
font-size: 16px;
|
92 |
+
color: #555;
|
93 |
+
}
|
94 |
+
|
95 |
+
form {
|
96 |
+
margin: 20px 0;
|
97 |
+
}
|
98 |
+
|
99 |
+
input[type="text"] {
|
100 |
+
padding: 12px;
|
101 |
+
border: 1px solid #ddd;
|
102 |
+
border-radius: 6px;
|
103 |
+
margin-right: 10px;
|
104 |
+
width: 350px;
|
105 |
+
font-size: 15px;
|
106 |
+
transition: border-color 0.3s;
|
107 |
+
}
|
108 |
+
|
109 |
+
input[type="text"]:focus {
|
110 |
+
border-color: #3498db;
|
111 |
+
outline: none;
|
112 |
+
box-shadow: 0 0 5px rgba(52, 152, 219, 0.3);
|
113 |
+
}
|
114 |
+
|
115 |
+
button {
|
116 |
+
padding: 12px 20px;
|
117 |
+
background-color: #3498db;
|
118 |
+
color: white;
|
119 |
+
border: none;
|
120 |
+
border-radius: 6px;
|
121 |
+
cursor: pointer;
|
122 |
+
font-size: 15px;
|
123 |
+
font-weight: 500;
|
124 |
+
transition: background-color 0.3s;
|
125 |
+
}
|
126 |
+
|
127 |
+
button:hover {
|
128 |
+
background-color: #2980b9;
|
129 |
+
}
|
130 |
+
|
131 |
+
/* Style for disabled button */
|
132 |
+
button:disabled {
|
133 |
+
background-color: #bdc3c7; /* Lighter grey */
|
134 |
+
cursor: not-allowed;
|
135 |
+
}
|
136 |
+
|
137 |
+
code {
|
138 |
+
background-color: #f8f9fa;
|
139 |
+
padding: 2px 5px;
|
140 |
+
border-radius: 4px;
|
141 |
+
font-family: monospace;
|
142 |
+
font-size: 14px;
|
143 |
+
color: #e74c3c;
|
144 |
+
}
|
145 |
+
|
146 |
+
/* Footer styling */
|
147 |
+
.footer {
|
148 |
+
text-align: center;
|
149 |
+
padding: 20px;
|
150 |
+
color: #7f8c8d;
|
151 |
+
font-size: 14px;
|
152 |
+
margin-top: 30px;
|
153 |
+
}
|
154 |
+
</style>
|
155 |
+
<!-- Invisible Captcha v2 -->
|
156 |
+
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
157 |
+
</head>
|
158 |
+
<body>
|
159 |
+
<!-- Header with logo, title, and SBOM count -->
|
160 |
+
<div class="header">
|
161 |
+
<a href="https://aetheris.ai/" target="_blank">
|
162 |
+
<img src="https://huggingface.co/spaces/aetheris-ai/aibom-generator/resolve/main/templates/images/AetherisAI-logo.png" alt="Aetheris AI Logo">
|
163 |
+
</a>
|
164 |
+
<!-- Added header-content div -->
|
165 |
+
<div class="header-content">
|
166 |
+
<h1>AI SBOM Generator</h1>
|
167 |
+
</div>
|
168 |
+
</div>
|
169 |
+
|
170 |
+
<div class="container">
|
171 |
+
<!-- Form Section (Moved to top) -->
|
172 |
+
<div class="form-section">
|
173 |
+
<h2>Generate Your AI SBOM</h2>
|
174 |
+
<p>
|
175 |
+
Enter a model on Hugging Face, in a format <code><organization-or-username>/<model-name></code> (easy copy button), or model's URL, to generate AI SBOM in CycloneDX format. You can browse available models in the <a href="https://huggingface.co/models" target="_blank" rel="noopener noreferrer">Hugging Face models repository</a>.
|
176 |
+
</p>
|
177 |
+
<!-- Added id="sbom-form" to the form -->
|
178 |
+
<form id="sbom-form" action="/generate" method="post" style="display: flex; flex-direction: row; align-items: center; width: 100%;">
|
179 |
+
<input type="text" name="model_id" placeholder="e.g., openai/whisper-tiny" required style="flex: 1; max-width: 70%; margin-right: 10px;">
|
180 |
+
<input type="hidden" name="g_recaptcha_response" id="g-recaptcha-response">
|
181 |
+
<button
|
182 |
+
class="g-recaptcha"
|
183 |
+
data-sitekey="6Ld57kcrAAAAAL7X-BF2EYLN5Adsom2VnFOnGsYR"
|
184 |
+
data-callback="onSubmit"
|
185 |
+
data-action="submit"
|
186 |
+
id="generate-button"
|
187 |
+
type="button">Generate AI SBOM</button>
|
188 |
+
</form>
|
189 |
+
<div style="font-size: 12px; color: #777; margin-top: 10px;">
|
190 |
+
This site is protected by reCAPTCHA and the Google
|
191 |
+
<a href="https://policies.google.com/privacy">Privacy Policy</a> and
|
192 |
+
<a href="https://policies.google.com/terms">Terms of Service</a> apply.
|
193 |
+
</div>
|
194 |
+
</div>
|
195 |
+
|
196 |
+
<!-- Tool Description Section -->
|
197 |
+
<div class="content-section">
|
198 |
+
<h2>About This Tool</h2>
|
199 |
+
<p>This open-source tool helps you generate AI SBOMs for models hosted on Hugging Face. It automatically extracts and formats key information—such as model metadata, training datasets, dependencies, and configurations—into a standardized, machine-readable SBOM using the CycloneDX JSON format. While not all models have consistent metadata quality and much of the information is unstructured, this tool helps navigate those gaps by extracting available data and organizing it into a clear, standardized structure to support transparency, security, and compliance.</p>
|
200 |
+
</div>
|
201 |
+
|
202 |
+
<!-- Introduction Section -->
|
203 |
+
<div class="content-section">
|
204 |
+
<h2>Understanding AI SBOMs</h2>
|
205 |
+
<p>An AI SBOM (Artificial Intelligence Software Bill of Materials, also known as AIBOM / ML-BOM or SBOM for AI) is a detailed, structured inventory that lists the components and dependencies involved in building and operating an AI system—such as pre-trained models, datasets, libraries, and configuration parameters. Much like a traditional SBOM for software, an AI SBOM brings transparency to what goes into an AI system, enabling organizations to assess security, compliance, and ethical risks. It is essential for managing AI supply chain risks, supporting regulatory requirements, ensuring model provenance, and enabling incident response and audits. As AI systems grow more complex and widely adopted, AI SBOMs become critical for maintaining trust, accountability, and control over how AI technologies are developed, integrated, and deployed.</p>
|
206 |
+
</div>
|
207 |
+
|
208 |
+
<!-- Support Section -->
|
209 |
+
<div class="content-section">
|
210 |
+
<h2>Feedback</h2>
|
211 |
+
<p>For feedback or improvement requests please create a <a href="https://github.com/aetheris-ai/aibom-generator/issues" target="_blank" rel="noopener noreferrer">GitHub issue</a>.</p>
|
212 |
+
</div>
|
213 |
+
|
214 |
+
<!-- Social Section -->
|
215 |
+
<div class="content-section" style="text-align: center;">
|
216 |
+
<h3>🗣️ Help Us Spread the Word</h3>
|
217 |
+
<p>If you find this tool useful, share it with your network! <a href="https://sbom.aetheris.ai" target="_blank" rel="noopener noreferrer">https://sbom.aetheris.ai</a></p>
|
218 |
+
<a href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fsbom.aetheris.ai" target="_blank" rel="noopener noreferrer" style="text-decoration: none;">
|
219 |
+
<button style="background-color: #0077b5;">🔗 Share on LinkedIn</button>
|
220 |
+
</a>
|
221 |
+
<p style="margin-top: 10px; font-size: 14px;">
|
222 |
+
Follow us for updates:
|
223 |
+
<a href="https://www.linkedin.com/company/aetheris-ai" target="_blank" rel="noopener noreferrer">@Aetheris AI</a>
|
224 |
+
</p>
|
225 |
+
</div>
|
226 |
+
|
227 |
+
<!-- Info Section -->
|
228 |
+
<div class="content-section" style="text-align: center;>
|
229 |
+
<!-- Display the SBOM count -->
|
230 |
+
<div class="sbom-count">🚀 Generated AI SBOMs using this tool: <strong>{{ sbom_count if sbom_count else 'N/A' }}</strong></div>
|
231 |
+
</div>
|
232 |
+
|
233 |
+
<!-- Footer -->
|
234 |
+
<div class="footer">
|
235 |
+
<p>© 2025 AI SBOM Generator | Powered by Aetheris AI</p>
|
236 |
+
</div>
|
237 |
+
</div>
|
238 |
+
|
239 |
+
<!-- JavaScript for loading indicator, and Captcha -->
|
240 |
+
<script>
|
241 |
+
function onSubmit(token ) {
|
242 |
+
// Set the token in the hidden input field
|
243 |
+
document.getElementById('g-recaptcha-response').value = token;
|
244 |
+
var button = document.getElementById('generate-button');
|
245 |
+
button.disabled = true;
|
246 |
+
button.textContent = 'Generating...';
|
247 |
+
// Now submit the form with the token
|
248 |
+
document.getElementById('sbom-form').submit();
|
249 |
+
}
|
250 |
+
</script>
|
251 |
+
<script>
|
252 |
+
function onSubmit(token ) {
|
253 |
+
console.log("reCAPTCHA callback executed with token:", token.substring(0, 10) + "...");
|
254 |
+
|
255 |
+
// Set the token in the hidden input
|
256 |
+
document.getElementById('g-recaptcha-response').value = token;
|
257 |
+
console.log("Token set in input:", document.getElementById('g-recaptcha-response').value.substring(0, 10) + "...");
|
258 |
+
|
259 |
+
// Disable button and change text
|
260 |
+
var button = document.getElementById('generate-button');
|
261 |
+
button.disabled = true;
|
262 |
+
button.textContent = 'Generating...';
|
263 |
+
|
264 |
+
// Submit the form
|
265 |
+
console.log("Submitting form");
|
266 |
+
document.getElementById('sbom-form').submit();
|
267 |
+
}
|
268 |
+
</script>
|
269 |
+
</body>
|
270 |
+
</html>
|
templates/result.html
ADDED
@@ -0,0 +1,1275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>AI SBOM Generated</title>
|
7 |
+
<style>
|
8 |
+
body {
|
9 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
10 |
+
margin: 0;
|
11 |
+
padding: 0;
|
12 |
+
line-height: 1.6;
|
13 |
+
color: #333;
|
14 |
+
background-color: #f9f9f9;
|
15 |
+
}
|
16 |
+
.container {
|
17 |
+
max-width: 1000px;
|
18 |
+
margin: 0 auto;
|
19 |
+
padding: 0 20px;
|
20 |
+
}
|
21 |
+
|
22 |
+
/* Header styling */
|
23 |
+
.header {
|
24 |
+
background-color: #ffffff;
|
25 |
+
padding: 15px 20px;
|
26 |
+
border-bottom: 1px solid #e9ecef;
|
27 |
+
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
28 |
+
display: flex;
|
29 |
+
align-items: center;
|
30 |
+
margin-bottom: 30px;
|
31 |
+
}
|
32 |
+
.header img {
|
33 |
+
height: 60px;
|
34 |
+
margin-right: 15px;
|
35 |
+
}
|
36 |
+
.header h1 {
|
37 |
+
margin: 0;
|
38 |
+
font-size: 28px;
|
39 |
+
color: #2c3e50;
|
40 |
+
font-weight: 600;
|
41 |
+
}
|
42 |
+
|
43 |
+
/* header-content div for layout */
|
44 |
+
.header .header-content {
|
45 |
+
display: flex;
|
46 |
+
flex-direction: column; /* Stack title and count */
|
47 |
+
}
|
48 |
+
.header h1 {
|
49 |
+
margin: 0;
|
50 |
+
font-size: 28px;
|
51 |
+
color: #2c3e50;
|
52 |
+
font-weight: 600;
|
53 |
+
margin-bottom: 5px; /* Space between title and count */
|
54 |
+
}
|
55 |
+
/* Added style for sbom-count */
|
56 |
+
.header .sbom-count {
|
57 |
+
font-size: 14px;
|
58 |
+
color: #555;
|
59 |
+
font-weight: 500;
|
60 |
+
}
|
61 |
+
|
62 |
+
/* Content styling */
|
63 |
+
.content-section {
|
64 |
+
background-color: #ffffff;
|
65 |
+
border-radius: 8px;
|
66 |
+
padding: 25px;
|
67 |
+
margin-bottom: 30px;
|
68 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
69 |
+
}
|
70 |
+
|
71 |
+
.content-section h2 {
|
72 |
+
color: #2c3e50;
|
73 |
+
margin-top: 0;
|
74 |
+
margin-bottom: 20px;
|
75 |
+
font-size: 22px;
|
76 |
+
border-bottom: 2px solid #f0f0f0;
|
77 |
+
padding-bottom: 10px;
|
78 |
+
}
|
79 |
+
|
80 |
+
.content-section h3 {
|
81 |
+
color: #2c3e50;
|
82 |
+
margin-top: 0;
|
83 |
+
margin-bottom: 15px;
|
84 |
+
font-size: 20px;
|
85 |
+
}
|
86 |
+
|
87 |
+
.content-section p {
|
88 |
+
margin-bottom: 20px;
|
89 |
+
font-size: 16px;
|
90 |
+
line-height: 1.7;
|
91 |
+
color: #555;
|
92 |
+
}
|
93 |
+
|
94 |
+
/* Button styling */
|
95 |
+
.button {
|
96 |
+
display: inline-block;
|
97 |
+
padding: 12px 20px;
|
98 |
+
background-color: #7f8c8d;
|
99 |
+
color: white;
|
100 |
+
border: none;
|
101 |
+
border-radius: 6px;
|
102 |
+
cursor: pointer;
|
103 |
+
font-size: 15px;
|
104 |
+
font-weight: 500;
|
105 |
+
text-decoration: none;
|
106 |
+
transition: background-color 0.3s;
|
107 |
+
margin-bottom: 20px;
|
108 |
+
}
|
109 |
+
|
110 |
+
.button:hover {
|
111 |
+
background-color: #95a5a6;
|
112 |
+
text-decoration: none;
|
113 |
+
}
|
114 |
+
|
115 |
+
button {
|
116 |
+
padding: 12px 20px;
|
117 |
+
background-color: #3498db;
|
118 |
+
color: white;
|
119 |
+
border: none;
|
120 |
+
border-radius: 6px;
|
121 |
+
cursor: pointer;
|
122 |
+
font-size: 15px;
|
123 |
+
font-weight: 500;
|
124 |
+
transition: background-color 0.3s;
|
125 |
+
}
|
126 |
+
|
127 |
+
button:hover {
|
128 |
+
background-color: #2980b9;
|
129 |
+
}
|
130 |
+
|
131 |
+
/* Table styling */
|
132 |
+
table {
|
133 |
+
border-collapse: collapse;
|
134 |
+
width: 100%;
|
135 |
+
margin-top: 15px;
|
136 |
+
margin-bottom: 20px;
|
137 |
+
}
|
138 |
+
th, td {
|
139 |
+
border: 1px solid #e9ecef;
|
140 |
+
padding: 12px;
|
141 |
+
}
|
142 |
+
th {
|
143 |
+
background-color: #f8f9fa;
|
144 |
+
color: #2c3e50;
|
145 |
+
font-weight: 600;
|
146 |
+
}
|
147 |
+
|
148 |
+
/* Styling for field checklist items */
|
149 |
+
.check-mark { color: #27ae60; } /* Green color for check marks */
|
150 |
+
.x-mark { color: #e74c3c; } /* Red color for x marks */
|
151 |
+
.field-name { color: #000; } /* Black color for field names */
|
152 |
+
.field-stars { color: #000; } /* Black color for importance stars */
|
153 |
+
|
154 |
+
.improvement {
|
155 |
+
color: #2c3e50;
|
156 |
+
background-color: #ecf0f1;
|
157 |
+
padding: 20px;
|
158 |
+
border-radius: 8px;
|
159 |
+
margin-bottom: 30px;
|
160 |
+
border-left: 4px solid #3498db;
|
161 |
+
}
|
162 |
+
.improvement-value { color: #27ae60; font-weight: bold; }
|
163 |
+
.ai-badge {
|
164 |
+
background-color: #3498db;
|
165 |
+
color: white;
|
166 |
+
padding: 3px 8px;
|
167 |
+
border-radius: 3px;
|
168 |
+
font-size: 0.8em;
|
169 |
+
margin-left: 10px;
|
170 |
+
}
|
171 |
+
|
172 |
+
/* Styles for human-friendly viewer */
|
173 |
+
.aibom-viewer {
|
174 |
+
margin: 20px 0;
|
175 |
+
border: 1px solid #e9ecef;
|
176 |
+
border-radius: 8px;
|
177 |
+
padding: 20px;
|
178 |
+
background-color: #f9f9f9;
|
179 |
+
}
|
180 |
+
.aibom-section {
|
181 |
+
margin-bottom: 20px;
|
182 |
+
padding: 20px;
|
183 |
+
border-radius: 8px;
|
184 |
+
background-color: white;
|
185 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
186 |
+
}
|
187 |
+
.aibom-section h4 {
|
188 |
+
margin-top: 0;
|
189 |
+
color: #2c3e50;
|
190 |
+
border-bottom: 2px solid #f0f0f0;
|
191 |
+
padding-bottom: 10px;
|
192 |
+
margin-bottom: 15px;
|
193 |
+
font-size: 18px;
|
194 |
+
}
|
195 |
+
.aibom-property {
|
196 |
+
display: flex;
|
197 |
+
margin: 10px 0;
|
198 |
+
}
|
199 |
+
.property-name {
|
200 |
+
font-weight: bold;
|
201 |
+
width: 200px;
|
202 |
+
color: #34495e;
|
203 |
+
}
|
204 |
+
.property-value {
|
205 |
+
flex: 1;
|
206 |
+
color: #555;
|
207 |
+
line-height: 1.6;
|
208 |
+
}
|
209 |
+
.aibom-tabs {
|
210 |
+
display: flex;
|
211 |
+
border-bottom: 1px solid #e9ecef;
|
212 |
+
margin-bottom: 20px;
|
213 |
+
}
|
214 |
+
.aibom-tab {
|
215 |
+
padding: 12px 20px;
|
216 |
+
cursor: pointer;
|
217 |
+
background-color: #f8f9fa;
|
218 |
+
margin-right: 5px;
|
219 |
+
border-radius: 8px 8px 0 0;
|
220 |
+
font-weight: 500;
|
221 |
+
transition: all 0.3s ease;
|
222 |
+
}
|
223 |
+
.aibom-tab.active {
|
224 |
+
background-color: #6c7a89;
|
225 |
+
color: white;
|
226 |
+
}
|
227 |
+
.aibom-tab:hover:not(.active) {
|
228 |
+
background-color: #e9ecef;
|
229 |
+
}
|
230 |
+
.tab-content {
|
231 |
+
display: none;
|
232 |
+
}
|
233 |
+
.tab-content.active {
|
234 |
+
display: block;
|
235 |
+
}
|
236 |
+
.json-view {
|
237 |
+
background-color: #f8f9fa;
|
238 |
+
border: 1px solid #e9ecef;
|
239 |
+
border-radius: 8px;
|
240 |
+
padding: 20px;
|
241 |
+
overflow: auto;
|
242 |
+
max-height: 500px;
|
243 |
+
font-family: monospace;
|
244 |
+
line-height: 1.5;
|
245 |
+
}
|
246 |
+
.collapsible {
|
247 |
+
cursor: pointer;
|
248 |
+
position: relative;
|
249 |
+
transition: all 0.3s ease;
|
250 |
+
}
|
251 |
+
.collapsible:after {
|
252 |
+
content: '+';
|
253 |
+
position: absolute;
|
254 |
+
right: 10px;
|
255 |
+
font-weight: bold;
|
256 |
+
}
|
257 |
+
.collapsible.active:after {
|
258 |
+
content: '-';
|
259 |
+
}
|
260 |
+
.collapsible-content {
|
261 |
+
max-height: 0;
|
262 |
+
overflow: hidden;
|
263 |
+
transition: max-height 0.3s ease-out;
|
264 |
+
}
|
265 |
+
.collapsible-content.active {
|
266 |
+
max-height: 500px;
|
267 |
+
}
|
268 |
+
.tag {
|
269 |
+
display: inline-block;
|
270 |
+
background-color: #e9ecef;
|
271 |
+
padding: 4px 10px;
|
272 |
+
border-radius: 16px;
|
273 |
+
margin: 3px;
|
274 |
+
font-size: 0.9em;
|
275 |
+
}
|
276 |
+
.key-info {
|
277 |
+
background-color: #e3f2fd;
|
278 |
+
border-left: 4px solid #2196F3;
|
279 |
+
padding: 20px;
|
280 |
+
margin-bottom: 20px;
|
281 |
+
border-radius: 8px;
|
282 |
+
}
|
283 |
+
|
284 |
+
/* Progress bar styles */
|
285 |
+
.progress-container {
|
286 |
+
width: 100%;
|
287 |
+
background-color: #f1f1f1;
|
288 |
+
border-radius: 8px;
|
289 |
+
margin: 8px 0;
|
290 |
+
overflow: hidden;
|
291 |
+
}
|
292 |
+
.progress-bar {
|
293 |
+
height: 24px;
|
294 |
+
border-radius: 8px;
|
295 |
+
text-align: center;
|
296 |
+
line-height: 24px;
|
297 |
+
color: white;
|
298 |
+
font-size: 14px;
|
299 |
+
font-weight: 500;
|
300 |
+
display: flex;
|
301 |
+
align-items: center;
|
302 |
+
justify-content: center;
|
303 |
+
transition: width 0.5s ease;
|
304 |
+
}
|
305 |
+
.progress-excellent {
|
306 |
+
background-color: #4CAF50; /* Green */
|
307 |
+
}
|
308 |
+
.progress-good {
|
309 |
+
background-color: #2196F3; /* Blue */
|
310 |
+
}
|
311 |
+
.progress-fair {
|
312 |
+
background-color: #FF9800; /* Orange */
|
313 |
+
}
|
314 |
+
.progress-poor {
|
315 |
+
background-color: #f44336; /* Red */
|
316 |
+
}
|
317 |
+
.score-table {
|
318 |
+
width: 100%;
|
319 |
+
margin-bottom: 20px;
|
320 |
+
}
|
321 |
+
.score-table th {
|
322 |
+
text-align: left;
|
323 |
+
padding: 12px;
|
324 |
+
background-color: #f8f9fa;
|
325 |
+
}
|
326 |
+
.score-table td {
|
327 |
+
padding: 12px;
|
328 |
+
}
|
329 |
+
.score-weight {
|
330 |
+
font-size: 0.9em;
|
331 |
+
color: #666;
|
332 |
+
margin-left: 5px;
|
333 |
+
}
|
334 |
+
.score-label {
|
335 |
+
display: inline-block;
|
336 |
+
padding: 3px 8px;
|
337 |
+
border-radius: 4px;
|
338 |
+
color: white;
|
339 |
+
font-size: 0.9em;
|
340 |
+
margin-left: 5px;
|
341 |
+
background-color: transparent; /* Make background transparent */
|
342 |
+
}
|
343 |
+
.total-score-container {
|
344 |
+
display: flex;
|
345 |
+
align-items: center;
|
346 |
+
margin-bottom: 25px;
|
347 |
+
background-color: white;
|
348 |
+
padding: 20px;
|
349 |
+
border-radius: 8px;
|
350 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
351 |
+
}
|
352 |
+
.total-score {
|
353 |
+
font-size: 28px;
|
354 |
+
font-weight: bold;
|
355 |
+
margin-right: 20px;
|
356 |
+
color: #2c3e50;
|
357 |
+
}
|
358 |
+
.total-progress {
|
359 |
+
flex: 1;
|
360 |
+
}
|
361 |
+
|
362 |
+
/* Styles for improved user understanding */
|
363 |
+
.tooltip {
|
364 |
+
position: relative;
|
365 |
+
display: inline-block;
|
366 |
+
cursor: help;
|
367 |
+
}
|
368 |
+
.tooltip .tooltiptext {
|
369 |
+
visibility: hidden;
|
370 |
+
width: 300px;
|
371 |
+
background-color: #34495e;
|
372 |
+
color: #fff;
|
373 |
+
text-align: left;
|
374 |
+
border-radius: 6px;
|
375 |
+
padding: 12px;
|
376 |
+
position: absolute;
|
377 |
+
z-index: 1;
|
378 |
+
bottom: 125%;
|
379 |
+
left: 50%;
|
380 |
+
margin-left: -150px;
|
381 |
+
opacity: 0;
|
382 |
+
transition: opacity 0.3s;
|
383 |
+
font-size: 0.9em;
|
384 |
+
line-height: 1.5;
|
385 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
386 |
+
}
|
387 |
+
.tooltip:hover .tooltiptext {
|
388 |
+
visibility: visible;
|
389 |
+
opacity: 1;
|
390 |
+
}
|
391 |
+
.tooltip .tooltiptext::after {
|
392 |
+
content: "";
|
393 |
+
position: absolute;
|
394 |
+
top: 100%;
|
395 |
+
left: 50%;
|
396 |
+
margin-left: -5px;
|
397 |
+
border-width: 5px;
|
398 |
+
border-style: solid;
|
399 |
+
border-color: #34495e transparent transparent transparent;
|
400 |
+
}
|
401 |
+
.missing-fields {
|
402 |
+
background-color: #ffebee;
|
403 |
+
border-left: 4px solid #f44336;
|
404 |
+
padding: 20px;
|
405 |
+
margin: 20px 0;
|
406 |
+
border-radius: 8px;
|
407 |
+
}
|
408 |
+
.missing-fields h4 {
|
409 |
+
margin-top: 0;
|
410 |
+
color: #d32f2f;
|
411 |
+
margin-bottom: 15px;
|
412 |
+
}
|
413 |
+
.missing-fields ul {
|
414 |
+
margin-bottom: 0;
|
415 |
+
padding-left: 20px;
|
416 |
+
}
|
417 |
+
.recommendations {
|
418 |
+
background-color: #e8f5e9;
|
419 |
+
border-left: 4px solid #4caf50;
|
420 |
+
padding: 20px;
|
421 |
+
margin: 20px 0;
|
422 |
+
border-radius: 8px;
|
423 |
+
}
|
424 |
+
.recommendations h4 {
|
425 |
+
margin-top: 0;
|
426 |
+
color: #2e7d32;
|
427 |
+
margin-bottom: 15px;
|
428 |
+
}
|
429 |
+
.importance-indicator {
|
430 |
+
display: inline-block;
|
431 |
+
margin-left: 5px;
|
432 |
+
}
|
433 |
+
.high-importance {
|
434 |
+
color: #d32f2f;
|
435 |
+
}
|
436 |
+
.medium-importance {
|
437 |
+
color: #ff9800;
|
438 |
+
}
|
439 |
+
.low-importance {
|
440 |
+
color: #2196f3;
|
441 |
+
}
|
442 |
+
.scoring-rubric {
|
443 |
+
background-color: #e3f2fd;
|
444 |
+
border-left: 4px solid #2196f3;
|
445 |
+
padding: 20px;
|
446 |
+
margin: 20px 0;
|
447 |
+
border-radius: 8px;
|
448 |
+
}
|
449 |
+
.scoring-rubric h4 {
|
450 |
+
margin-top: 0;
|
451 |
+
color: #1565c0;
|
452 |
+
margin-bottom: 15px;
|
453 |
+
}
|
454 |
+
.scoring-rubric table {
|
455 |
+
width: 100%;
|
456 |
+
margin-top: 15px;
|
457 |
+
}
|
458 |
+
.scoring-rubric th, .scoring-rubric td {
|
459 |
+
padding: 10px;
|
460 |
+
text-align: left;
|
461 |
+
}
|
462 |
+
.note-box {
|
463 |
+
background-color: #fffbea; /* Lighter yellow background */
|
464 |
+
border-left: 4px solid #ffc107;
|
465 |
+
padding: 20px;
|
466 |
+
margin: 20px 0;
|
467 |
+
border-radius: 8px;
|
468 |
+
}
|
469 |
+
.download-section {
|
470 |
+
margin: 20px 0;
|
471 |
+
display: flex;
|
472 |
+
align-items: center;
|
473 |
+
}
|
474 |
+
.download-section p {
|
475 |
+
margin: 0;
|
476 |
+
margin-right: 15px;
|
477 |
+
}
|
478 |
+
|
479 |
+
/* Styles for completeness profile */
|
480 |
+
.completeness-profile {
|
481 |
+
background-color: #e8f5e9;
|
482 |
+
border-radius: 8px;
|
483 |
+
padding: 20px;
|
484 |
+
margin: 20px 0;
|
485 |
+
border-left: 4px solid #4caf50;
|
486 |
+
}
|
487 |
+
.profile-badge {
|
488 |
+
display: inline-block;
|
489 |
+
padding: 5px 12px;
|
490 |
+
border-radius: 20px;
|
491 |
+
color: white;
|
492 |
+
font-weight: bold;
|
493 |
+
margin-right: 10px;
|
494 |
+
}
|
495 |
+
.profile-basic {
|
496 |
+
background-color: #ff9800;
|
497 |
+
}
|
498 |
+
.profile-standard {
|
499 |
+
background-color: #2196f3;
|
500 |
+
}
|
501 |
+
.profile-advanced {
|
502 |
+
background-color: #4caf50;
|
503 |
+
}
|
504 |
+
/* Contrast for profile status */
|
505 |
+
.profile-incomplete {
|
506 |
+
background-color: #f44336;
|
507 |
+
color: white; /* Ensure text is visible on red background */
|
508 |
+
}
|
509 |
+
.field-tier {
|
510 |
+
display: inline-block;
|
511 |
+
width: 12px;
|
512 |
+
height: 12px;
|
513 |
+
border-radius: 50%;
|
514 |
+
margin-right: 5px;
|
515 |
+
}
|
516 |
+
.tier-critical {
|
517 |
+
background-color: #d32f2f;
|
518 |
+
}
|
519 |
+
.tier-important {
|
520 |
+
background-color: #ff9800;
|
521 |
+
}
|
522 |
+
.tier-supplementary {
|
523 |
+
background-color: #2196f3;
|
524 |
+
}
|
525 |
+
.tier-legend {
|
526 |
+
display: flex;
|
527 |
+
margin: 15px 0;
|
528 |
+
font-size: 0.9em;
|
529 |
+
}
|
530 |
+
.tier-legend-item {
|
531 |
+
display: flex;
|
532 |
+
align-items: center;
|
533 |
+
margin-right: 20px;
|
534 |
+
}
|
535 |
+
/* Style for validation penalty explanation */
|
536 |
+
.validation-penalty-info {
|
537 |
+
background-color: #fff3e0;
|
538 |
+
border-left: 4px solid #ff9800;
|
539 |
+
padding: 20px;
|
540 |
+
margin: 20px 0;
|
541 |
+
border-radius: 8px;
|
542 |
+
font-size: 0.95em;
|
543 |
+
}
|
544 |
+
.validation-penalty-info h4 {
|
545 |
+
margin-top: 0;
|
546 |
+
color: #e65100;
|
547 |
+
margin-bottom: 15px;
|
548 |
+
}
|
549 |
+
|
550 |
+
/* Section for score calculation explanation */
|
551 |
+
.score-calculation {
|
552 |
+
margin-top: 30px;
|
553 |
+
padding: 25px;
|
554 |
+
background-color: #ffffff;
|
555 |
+
border-radius: 8px;
|
556 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
557 |
+
}
|
558 |
+
.score-calculation h3 {
|
559 |
+
margin-top: 0;
|
560 |
+
color: #2c3e50;
|
561 |
+
border-bottom: 2px solid #f0f0f0;
|
562 |
+
padding-bottom: 10px;
|
563 |
+
margin-bottom: 20px;
|
564 |
+
}
|
565 |
+
.calculation-section {
|
566 |
+
margin-bottom: 25px;
|
567 |
+
}
|
568 |
+
|
569 |
+
/* Footer styling */
|
570 |
+
.footer {
|
571 |
+
text-align: center;
|
572 |
+
padding: 20px;
|
573 |
+
color: #7f8c8d;
|
574 |
+
font-size: 14px;
|
575 |
+
margin-top: 30px;
|
576 |
+
}
|
577 |
+
|
578 |
+
/* Responsive adjustments */
|
579 |
+
@media (max-width: 768px) {
|
580 |
+
.aibom-property {
|
581 |
+
flex-direction: column;
|
582 |
+
}
|
583 |
+
.property-name {
|
584 |
+
width: 100%;
|
585 |
+
margin-bottom: 5px;
|
586 |
+
}
|
587 |
+
.total-score-container {
|
588 |
+
flex-direction: column;
|
589 |
+
align-items: flex-start;
|
590 |
+
}
|
591 |
+
.total-score {
|
592 |
+
margin-bottom: 10px;
|
593 |
+
}
|
594 |
+
.aibom-tabs {
|
595 |
+
flex-wrap: wrap;
|
596 |
+
}
|
597 |
+
.aibom-tab {
|
598 |
+
margin-bottom: 5px;
|
599 |
+
}
|
600 |
+
}
|
601 |
+
</style>
|
602 |
+
</head>
|
603 |
+
<body>
|
604 |
+
<!-- Header with logo, title, and SBOM count -->
|
605 |
+
<div class="header">
|
606 |
+
<a href="https://aetheris.ai/" target="_blank">
|
607 |
+
<img src="https://huggingface.co/spaces/aetheris-ai/aibom-generator/resolve/main/templates/images/AetherisAI-logo.png" alt="Aetheris AI Logo">
|
608 |
+
</a>
|
609 |
+
<!-- Header-content div -->
|
610 |
+
<div class="header-content">
|
611 |
+
<h1>AI SBOM Generator</h1>
|
612 |
+
</div>
|
613 |
+
</div>
|
614 |
+
|
615 |
+
|
616 |
+
<div class="container">
|
617 |
+
<div class="content-section">
|
618 |
+
<h2>AI SBOM Generated for {{ model_id }}</h2>
|
619 |
+
|
620 |
+
<a href="/" class="button">Generate another AI SBOM</a>
|
621 |
+
|
622 |
+
<div class="download-section">
|
623 |
+
<p>Download generated AI SBOM in CycloneDX format</p>
|
624 |
+
<button onclick="downloadJSON()">Download JSON</button>
|
625 |
+
</div>
|
626 |
+
|
627 |
+
{% if enhancement_report and enhancement_report.ai_enhanced %}
|
628 |
+
<div class="improvement">
|
629 |
+
<h3>AI Enhancement Results</h3>
|
630 |
+
<p>This AI SBOM was enhanced using <strong>{{ enhancement_report.ai_model }}</strong></p>
|
631 |
+
<p>Original Score: {{ enhancement_report.original_score.total_score|round(1) }}/100</p>
|
632 |
+
<p>Enhanced Score: {{ enhancement_report.final_score.total_score|round(1) }}/100</p>
|
633 |
+
<p>Improvement: <span class="improvement-value">+{{ enhancement_report.improvement|round(1) }} points</span></p>
|
634 |
+
</div>
|
635 |
+
{% endif %}
|
636 |
+
</div>
|
637 |
+
|
638 |
+
<!-- Human-friendly AI SBOM Viewer -->
|
639 |
+
<div class="note-box">
|
640 |
+
<p><strong>Note:</strong> This page displays the AI SBOM in a human-friendly format for easier readability.
|
641 |
+
The downloaded JSON file follows the standard CycloneDX format required for interoperability with other tools.</p>
|
642 |
+
</div>
|
643 |
+
|
644 |
+
<div class="aibom-tabs">
|
645 |
+
<div class="aibom-tab active" onclick="switchTab('human-view')">Human-Friendly View</div>
|
646 |
+
<div class="aibom-tab" onclick="switchTab('json-view')">JSON View</div>
|
647 |
+
<div class="aibom-tab" onclick="switchTab('field-checklist')">Field Checklist</div>
|
648 |
+
<div class="aibom-tab" onclick="switchTab('score-view')">Score Report</div>
|
649 |
+
</div>
|
650 |
+
|
651 |
+
<div id="human-view" class="tab-content active">
|
652 |
+
<div class="aibom-viewer">
|
653 |
+
<!-- Key Information Section -->
|
654 |
+
<div class="aibom-section key-info">
|
655 |
+
<h4>Key Information</h4>
|
656 |
+
<div class="aibom-property">
|
657 |
+
<div class="property-name">Model Name:</div>
|
658 |
+
<div class="property-value">{{ aibom.components[0].name if aibom.components and aibom.components[0].name else 'Not specified' }}</div>
|
659 |
+
</div>
|
660 |
+
<div class="aibom-property">
|
661 |
+
<div class="property-name">Type:</div>
|
662 |
+
<div class="property-value">{{ aibom.components[0].type if aibom.components and aibom.components[0].type else 'Not specified' }}</div>
|
663 |
+
</div>
|
664 |
+
<div class="aibom-property">
|
665 |
+
<div class="property-name">Version:</div>
|
666 |
+
<div class="property-value">{{ aibom.components[0].version if aibom.components and aibom.components[0].version else 'Not specified' }}</div>
|
667 |
+
</div>
|
668 |
+
<div class="aibom-property">
|
669 |
+
<div class="property-name">PURL:</div>
|
670 |
+
<div class="property-value">{{ aibom.components[0].purl if aibom.components and aibom.components[0].purl else 'Not specified' }}</div>
|
671 |
+
</div>
|
672 |
+
{% if aibom.components and aibom.components[0].description %}
|
673 |
+
<div class="aibom-property">
|
674 |
+
<div class="property-name">Description:</div>
|
675 |
+
<div class="property-value">{{ aibom.components[0].description }}</div>
|
676 |
+
</div>
|
677 |
+
{% endif %}
|
678 |
+
</div>
|
679 |
+
|
680 |
+
<!-- Model Card Section -->
|
681 |
+
{% if aibom.components and aibom.components[0].modelCard %}
|
682 |
+
<div class="aibom-section">
|
683 |
+
<h4 class="collapsible" onclick="toggleCollapsible(this)">Model Card</h4>
|
684 |
+
<div class="collapsible-content">
|
685 |
+
{% if aibom.components[0].modelCard.modelParameters %}
|
686 |
+
<div class="aibom-property">
|
687 |
+
<div class="property-name">Model Parameters:</div>
|
688 |
+
<div class="property-value">
|
689 |
+
<ul>
|
690 |
+
{% for key, value in aibom.components[0].modelCard.modelParameters.items() %}
|
691 |
+
<li><strong>{{ key }}:</strong> {{ value }}</li>
|
692 |
+
{% endfor %}
|
693 |
+
</ul>
|
694 |
+
</div>
|
695 |
+
</div>
|
696 |
+
{% endif %}
|
697 |
+
|
698 |
+
{% if aibom.components[0].modelCard.considerations %}
|
699 |
+
<div class="aibom-property">
|
700 |
+
<div class="property-name">Considerations:</div>
|
701 |
+
<div class="property-value">
|
702 |
+
<ul>
|
703 |
+
{% for key, value in aibom.components[0].modelCard.considerations.items() %}
|
704 |
+
<li><strong>{{ key }}:</strong> {{ value }}</li>
|
705 |
+
{% endfor %}
|
706 |
+
</ul>
|
707 |
+
</div>
|
708 |
+
</div>
|
709 |
+
{% endif %}
|
710 |
+
</div>
|
711 |
+
</div>
|
712 |
+
{% endif %}
|
713 |
+
|
714 |
+
<!-- External References Section -->
|
715 |
+
{% if aibom.components and aibom.components[0].externalReferences %}
|
716 |
+
<div class="aibom-section">
|
717 |
+
<h4 class="collapsible" onclick="toggleCollapsible(this)">External References</h4>
|
718 |
+
<div class="collapsible-content">
|
719 |
+
<ul>
|
720 |
+
{% for ref in aibom.components[0].externalReferences %}
|
721 |
+
<li>
|
722 |
+
<strong>{{ ref.type }}:</strong>
|
723 |
+
<a href="{{ ref.url }}" target="_blank">{{ ref.url }}</a>
|
724 |
+
{% if ref.comment %}
|
725 |
+
<br><em>{{ ref.comment }}</em>
|
726 |
+
{% endif %}
|
727 |
+
</li>
|
728 |
+
{% endfor %}
|
729 |
+
</ul>
|
730 |
+
</div>
|
731 |
+
</div>
|
732 |
+
{% endif %}
|
733 |
+
</div>
|
734 |
+
</div>
|
735 |
+
|
736 |
+
<div id="json-view" class="tab-content">
|
737 |
+
<div class="json-view">
|
738 |
+
<pre>{{ aibom | tojson(indent=2) }}</pre>
|
739 |
+
</div>
|
740 |
+
</div>
|
741 |
+
|
742 |
+
<div id="field-checklist" class="tab-content">
|
743 |
+
<div class="content-section">
|
744 |
+
<h3>Field Checklist & Mapping</h3>
|
745 |
+
|
746 |
+
<!-- Field Tier Legend -->
|
747 |
+
<div class="tier-legend">
|
748 |
+
<div class="tier-legend-item">
|
749 |
+
<span class="field-tier tier-critical"></span>
|
750 |
+
<span>Critical</span>
|
751 |
+
</div>
|
752 |
+
<div class="tier-legend-item">
|
753 |
+
<span class="field-tier tier-important"></span>
|
754 |
+
<span>Important</span>
|
755 |
+
</div>
|
756 |
+
<div class="tier-legend-item">
|
757 |
+
<span class="field-tier tier-supplementary"></span>
|
758 |
+
<span>Supplementary</span>
|
759 |
+
</div>
|
760 |
+
</div>
|
761 |
+
|
762 |
+
<p>This table shows how fields map to the CycloneDX specification and their status in your AI SBOM.</p>
|
763 |
+
|
764 |
+
<div class="field-mapping-container">
|
765 |
+
<h4>Standard CycloneDX Fields</h4>
|
766 |
+
<p>These fields are part of the official CycloneDX specification and are used in all SBOMs:</p>
|
767 |
+
<table class="field-mapping-table">
|
768 |
+
<thead>
|
769 |
+
<tr>
|
770 |
+
<th>Status</th>
|
771 |
+
<th>Field Name</th>
|
772 |
+
<th>CycloneDX JSON Path</th>
|
773 |
+
<th>Info</th>
|
774 |
+
<th>Importance</th>
|
775 |
+
</tr>
|
776 |
+
</thead>
|
777 |
+
<tbody>
|
778 |
+
{% for field_key, field_data in completeness_score.field_categorization.standard_cyclonedx_fields.items() %}
|
779 |
+
<tr class="{% if field_data.status == '✔' %}present-field{% else %}missing-field{% endif %}">
|
780 |
+
<td class="status-cell">
|
781 |
+
{% if field_data.status == "✔" %}
|
782 |
+
<span class="check-mark">✔</span>
|
783 |
+
{% else %}
|
784 |
+
<span class="x-mark">✘</span>
|
785 |
+
{% endif %}
|
786 |
+
</td>
|
787 |
+
<td>{{ field_data.field_name }}</td>
|
788 |
+
<td>{{ field_data.json_path }}</td>
|
789 |
+
<td>
|
790 |
+
<span class="tooltip">(?)
|
791 |
+
<span class="tooltiptext">{{ field_data.field_name }} field information.</span>
|
792 |
+
</span>
|
793 |
+
</td>
|
794 |
+
<td>
|
795 |
+
<span class="field-tier tier-{{ field_data.importance|lower }}"></span>
|
796 |
+
{{ field_data.importance }}
|
797 |
+
</td>
|
798 |
+
</tr>
|
799 |
+
{% endfor %}
|
800 |
+
</tbody>
|
801 |
+
</table>
|
802 |
+
|
803 |
+
|
804 |
+
<h4>AI-Specific Extension Fields</h4>
|
805 |
+
<p>These fields extend the CycloneDX specification specifically for AI models:</p>
|
806 |
+
<table class="field-mapping-table">
|
807 |
+
<thead>
|
808 |
+
<tr>
|
809 |
+
<th>Status</th>
|
810 |
+
<th>Field Name</th>
|
811 |
+
<th>CycloneDX JSON Path</th>
|
812 |
+
<th>Info</th>
|
813 |
+
<th>Importance</th>
|
814 |
+
</tr>
|
815 |
+
</thead>
|
816 |
+
<tbody>
|
817 |
+
{% for field_key, field_data in completeness_score.field_categorization.ai_specific_extension_fields.items() %}
|
818 |
+
<tr class="{% if field_data.status == '✔' %}present-field{% else %}missing-field{% endif %}">
|
819 |
+
<td class="status-cell">
|
820 |
+
{% if field_data.status == "✔" %}
|
821 |
+
<span class="check-mark">✔</span>
|
822 |
+
{% else %}
|
823 |
+
<span class="x-mark">✘</span>
|
824 |
+
{% endif %}
|
825 |
+
</td>
|
826 |
+
<td>{{ field_data.field_name }}</td>
|
827 |
+
<td>{{ field_data.json_path }}</td>
|
828 |
+
<td>
|
829 |
+
<span class="tooltip">(?)
|
830 |
+
<span class="tooltiptext">{{ field_data.field_name }} field information.</span>
|
831 |
+
</span>
|
832 |
+
</td>
|
833 |
+
<td>
|
834 |
+
<span class="field-tier tier-{{ field_data.importance|lower }}"></span>
|
835 |
+
{{ field_data.importance }}
|
836 |
+
</td>
|
837 |
+
</tr>
|
838 |
+
{% endfor %}
|
839 |
+
</tbody>
|
840 |
+
</table>
|
841 |
+
</div>
|
842 |
+
|
843 |
+
<style>
|
844 |
+
.field-mapping-container {
|
845 |
+
margin-top: 20px;
|
846 |
+
max-width: 100%;
|
847 |
+
overflow-x: auto;
|
848 |
+
}
|
849 |
+
.field-mapping-table {
|
850 |
+
width: 100%;
|
851 |
+
border-collapse: collapse;
|
852 |
+
margin-bottom: 30px;
|
853 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
854 |
+
border-radius: 8px;
|
855 |
+
overflow: hidden;
|
856 |
+
table-layout: fixed;
|
857 |
+
}
|
858 |
+
.field-mapping-table th {
|
859 |
+
background-color: #f5f5f5;
|
860 |
+
padding: 12px 15px;
|
861 |
+
text-align: left;
|
862 |
+
font-weight: 600;
|
863 |
+
color: #333;
|
864 |
+
border-bottom: 2px solid #ddd;
|
865 |
+
}
|
866 |
+
.field-mapping-table td {
|
867 |
+
padding: 10px 15px;
|
868 |
+
border-bottom: 1px solid #eee;
|
869 |
+
vertical-align: middle;
|
870 |
+
word-wrap: break-word;
|
871 |
+
word-break: break-word;
|
872 |
+
max-width: 250px;
|
873 |
+
}
|
874 |
+
.field-mapping-table tr:last-child td {
|
875 |
+
border-bottom: none;
|
876 |
+
}
|
877 |
+
.field-mapping-table tr:hover {
|
878 |
+
background-color: #f9f9f9;
|
879 |
+
}
|
880 |
+
.status-cell {
|
881 |
+
text-align: center;
|
882 |
+
width: 60px;
|
883 |
+
}
|
884 |
+
.present-field {
|
885 |
+
background-color: #f0f7f0;
|
886 |
+
}
|
887 |
+
.missing-field {
|
888 |
+
background-color: #fff7f7;
|
889 |
+
}
|
890 |
+
.check-mark {
|
891 |
+
color: #4caf50;
|
892 |
+
font-weight: bold;
|
893 |
+
font-size: 18px;
|
894 |
+
}
|
895 |
+
.x-mark {
|
896 |
+
color: #f44336;
|
897 |
+
font-weight: bold;
|
898 |
+
font-size: 18px;
|
899 |
+
}
|
900 |
+
</style>
|
901 |
+
</div>
|
902 |
+
</div>
|
903 |
+
|
904 |
+
<div id="score-view" class="tab-content">
|
905 |
+
<div class="content-section">
|
906 |
+
<h3>AI SBOM Completeness Score</h3>
|
907 |
+
|
908 |
+
<!-- Completeness Profile Section -->
|
909 |
+
{% if completeness_score.completeness_profile %}
|
910 |
+
<div class="completeness-profile">
|
911 |
+
<h4>Completeness Profile:
|
912 |
+
<span class="profile-badge profile-{{ completeness_score.completeness_profile.name|lower }}">
|
913 |
+
{{ completeness_score.completeness_profile.name }}
|
914 |
+
</span>
|
915 |
+
</h4>
|
916 |
+
<p>{{ completeness_score.completeness_profile.description }}</p>
|
917 |
+
|
918 |
+
{% if completeness_score.completeness_profile.next_level %}
|
919 |
+
<p><strong>Next level:</strong> {{ completeness_score.completeness_profile.next_level.name }}
|
920 |
+
({{ completeness_score.completeness_profile.next_level.missing_fields_count }} fields to add)</p>
|
921 |
+
{% endif %}
|
922 |
+
</div>
|
923 |
+
{% endif %}
|
924 |
+
|
925 |
+
<!-- Total Score with Progress Bar -->
|
926 |
+
<div class="total-score-container">
|
927 |
+
<div class="total-score">{{ completeness_score.total_score|round(1) }}/100</div>
|
928 |
+
<div class="total-progress">
|
929 |
+
<div class="progress-container">
|
930 |
+
{% set score_percent = (completeness_score.total_score / 100) * 100 %}
|
931 |
+
{% set score_class = 'progress-poor' %}
|
932 |
+
{% set score_label = 'Poor' %}
|
933 |
+
|
934 |
+
{% if score_percent >= 90 %}
|
935 |
+
{% set score_class = 'progress-excellent' %}
|
936 |
+
{% set score_label = 'Excellent' %}
|
937 |
+
{% elif score_percent >= 70 %}
|
938 |
+
{% set score_class = 'progress-good' %}
|
939 |
+
{% set score_label = 'Good' %}
|
940 |
+
{% elif score_percent >= 50 %}
|
941 |
+
{% set score_class = 'progress-fair' %}
|
942 |
+
{% set score_label = 'Fair' %}
|
943 |
+
{% endif %}
|
944 |
+
|
945 |
+
<div class="progress-bar {{ score_class }}" style="width: {{ score_percent }}%">
|
946 |
+
{{ score_percent|int }}% {{ score_label }}
|
947 |
+
</div>
|
948 |
+
</div>
|
949 |
+
</div>
|
950 |
+
</div>
|
951 |
+
|
952 |
+
<!-- Validation Penalty Explanation -->
|
953 |
+
{% if completeness_score.validation_penalty %}
|
954 |
+
<div class="validation-penalty-info">
|
955 |
+
<h4>About the Validation Penalty</h4>
|
956 |
+
<p>Your score includes a penalty because the AIBOM has schema validation issues. These are structural problems that don't comply with the CycloneDX specification requirements.</p>
|
957 |
+
<p><strong>How to fix this:</strong> Look at the "Fix Validation Issues" section in the recommendations below. Fixing these issues will remove the penalty and improve your overall score.</p>
|
958 |
+
</div>
|
959 |
+
{% endif %}
|
960 |
+
|
961 |
+
<!-- Section Scores with Progress Bars and Tooltips -->
|
962 |
+
<table class="score-table">
|
963 |
+
<thead>
|
964 |
+
<tr>
|
965 |
+
<th>Section</th>
|
966 |
+
<th>Score</th>
|
967 |
+
<th>Weight</th>
|
968 |
+
<th>Progress</th>
|
969 |
+
</tr>
|
970 |
+
</thead>
|
971 |
+
<tbody>
|
972 |
+
{% set weights = {'required_fields': 20, 'metadata': 20, 'component_basic': 20, 'component_model_card': 30, 'external_references': 10} %}
|
973 |
+
{% set tooltips = {
|
974 |
+
'required_fields': 'Basic SBOM fields required by the CycloneDX specification: bomFormat, specVersion, serialNumber, and version.',
|
975 |
+
'metadata': 'Information about the AI SBOM itself: timestamp, tools used to generate it, authors, and component metadata.',
|
976 |
+
'component_basic': 'Basic information about the AI model: type, name, bom-ref, PURL, description, and licenses.',
|
977 |
+
'component_model_card': 'Detailed information about the model: parameters, quantitative analysis, and ethical considerations.',
|
978 |
+
'external_references': 'Links to external resources like model cards, repositories, and datasets.'
|
979 |
+
} %}
|
980 |
+
{% set display_names = {
|
981 |
+
'required_fields': 'Required Fields',
|
982 |
+
'metadata': 'Metadata',
|
983 |
+
'component_basic': 'Component Basic',
|
984 |
+
'component_model_card': 'Model Card',
|
985 |
+
'external_references': 'External References'
|
986 |
+
} %}
|
987 |
+
{% for section, score in completeness_score.section_scores.items() %}
|
988 |
+
<tr>
|
989 |
+
<td>
|
990 |
+
{{ display_names[section] }}
|
991 |
+
<span class="tooltip">(?)
|
992 |
+
<span class="tooltiptext">{{ tooltips[section] }}</span>
|
993 |
+
</span>
|
994 |
+
</td>
|
995 |
+
<td>{{ score|round(1) }}/{{ completeness_score.max_scores[section] }}</td>
|
996 |
+
<td>{{ weights[section] }}%</td>
|
997 |
+
<td style="width: 50%;">
|
998 |
+
<div class="progress-container">
|
999 |
+
{% set percent = (score / completeness_score.max_scores[section]) * 100 %}
|
1000 |
+
{% set class = 'progress-poor' %}
|
1001 |
+
|
1002 |
+
{% if percent >= 90 %}
|
1003 |
+
{% set class = 'progress-excellent' %}
|
1004 |
+
{% elif percent >= 70 %}
|
1005 |
+
{% set class = 'progress-good' %}
|
1006 |
+
{% elif percent >= 50 %}
|
1007 |
+
{% set class = 'progress-fair' %}
|
1008 |
+
{% endif %}
|
1009 |
+
|
1010 |
+
<div class="progress-bar {{ class }}" style="width: {{ percent }}%">
|
1011 |
+
{{ percent|int }}%
|
1012 |
+
</div>
|
1013 |
+
</div>
|
1014 |
+
</td>
|
1015 |
+
</tr>
|
1016 |
+
{% endfor %}
|
1017 |
+
</tbody>
|
1018 |
+
</table>
|
1019 |
+
|
1020 |
+
<!-- How the Overall Score is Calculated Section -->
|
1021 |
+
<div class="score-calculation">
|
1022 |
+
<h3>How the Overall Score is Calculated</h3>
|
1023 |
+
|
1024 |
+
<!-- Missing Fields Section -->
|
1025 |
+
<div class="calculation-section missing-fields">
|
1026 |
+
<h4>Critical Missing Fields</h4>
|
1027 |
+
<p>The following fields are missing or incomplete and have the biggest impact on your score:</p>
|
1028 |
+
<ul>
|
1029 |
+
{% set missing_critical = [] %}
|
1030 |
+
{% for field, status in completeness_score.field_checklist.items() %}
|
1031 |
+
{% if "✘" in status %}
|
1032 |
+
{% if completeness_score.field_tiers and field in completeness_score.field_tiers and completeness_score.field_tiers[field] == 'critical' %}
|
1033 |
+
{% set _ = missing_critical.append(field) %}
|
1034 |
+
<li>
|
1035 |
+
<strong>{{ field }}</strong>
|
1036 |
+
<span class="field-tier tier-critical"></span>
|
1037 |
+
{% if field == "component.description" %}
|
1038 |
+
- Add a detailed description of the model (at least 20 characters)
|
1039 |
+
{% elif field == "component.purl" %}
|
1040 |
+
- Add a valid PURL in the format pkg:huggingface/[owner]/[name]@[version]
|
1041 |
+
{% elif field == "modelCard.modelParameters" %}
|
1042 |
+
- Add model parameters section with architecture, size, and training details
|
1043 |
+
{% elif field == "primaryPurpose" %}
|
1044 |
+
- Add primary purpose information (what the model is designed for)
|
1045 |
+
{% else %}
|
1046 |
+
- This field is required for comprehensive documentation
|
1047 |
+
{% endif %}
|
1048 |
+
</li>
|
1049 |
+
{% endif %}
|
1050 |
+
{% endif %}
|
1051 |
+
{% endfor %}
|
1052 |
+
{% if missing_critical|length == 0 %}
|
1053 |
+
<li>No critical fields are missing. Great job!</li>
|
1054 |
+
{% endif %}
|
1055 |
+
</ul>
|
1056 |
+
</div>
|
1057 |
+
|
1058 |
+
<!-- Recommendations Section -->
|
1059 |
+
<div class="calculation-section recommendations">
|
1060 |
+
<h4>Recommendations to Improve Your Score</h4>
|
1061 |
+
<ol>
|
1062 |
+
{% if completeness_score.section_scores.component_model_card < completeness_score.max_scores.component_model_card %}
|
1063 |
+
<li>
|
1064 |
+
<strong>Enhance Model Card</strong> (+{{ ((completeness_score.max_scores.component_model_card - completeness_score.section_scores.component_model_card) * 0.3)|round(1) }} points):
|
1065 |
+
<ul>
|
1066 |
+
{% if completeness_score.missing_fields.critical %}
|
1067 |
+
{% for field in completeness_score.missing_fields.critical %}
|
1068 |
+
{% if field == "modelCard.modelParameters" or field == "modelCard.considerations" %}
|
1069 |
+
<li>Add {{ field }} information</li>
|
1070 |
+
{% endif %}
|
1071 |
+
{% endfor %}
|
1072 |
+
{% endif %}
|
1073 |
+
</ul>
|
1074 |
+
</li>
|
1075 |
+
{% endif %}
|
1076 |
+
|
1077 |
+
{% if completeness_score.section_scores.component_basic < completeness_score.max_scores.component_basic %}
|
1078 |
+
<li>
|
1079 |
+
<strong>Add Basic Component Information</strong> (+{{ ((completeness_score.max_scores.component_basic - completeness_score.section_scores.component_basic) * 0.2)|round(1) }} points):
|
1080 |
+
<ul>
|
1081 |
+
{% if completeness_score.missing_fields.critical %}
|
1082 |
+
{% for field in completeness_score.missing_fields.critical %}
|
1083 |
+
{% if field == "name" or field == "description" or field == "purl" %}
|
1084 |
+
<li>Add {{ field }} information</li>
|
1085 |
+
{% endif %}
|
1086 |
+
{% endfor %}
|
1087 |
+
{% endif %}
|
1088 |
+
{% if completeness_score.missing_fields.important %}
|
1089 |
+
{% for field in completeness_score.missing_fields.important %}
|
1090 |
+
{% if field == "type" or field == "licenses" %}
|
1091 |
+
<li>Add {{ field }} information</li>
|
1092 |
+
{% endif %}
|
1093 |
+
{% endfor %}
|
1094 |
+
{% endif %}
|
1095 |
+
</ul>
|
1096 |
+
</li>
|
1097 |
+
{% endif %}
|
1098 |
+
|
1099 |
+
{% if completeness_score.section_scores.metadata < completeness_score.max_scores.metadata %}
|
1100 |
+
<li>
|
1101 |
+
<strong>Add Metadata</strong> (+{{ ((completeness_score.max_scores.metadata - completeness_score.section_scores.metadata) * 0.2)|round(1) }} points):
|
1102 |
+
<ul>
|
1103 |
+
{% if completeness_score.missing_fields.critical %}
|
1104 |
+
{% for field in completeness_score.missing_fields.critical %}
|
1105 |
+
{% if field == "primaryPurpose" or field == "suppliedBy" %}
|
1106 |
+
<li>Add {{ field }} information</li>
|
1107 |
+
{% endif %}
|
1108 |
+
{% endfor %}
|
1109 |
+
{% endif %}
|
1110 |
+
{% if completeness_score.missing_fields.supplementary %}
|
1111 |
+
{% for field in completeness_score.missing_fields.supplementary %}
|
1112 |
+
{% if field == "standardCompliance" or field == "domain" or field == "autonomyType" %}
|
1113 |
+
<li>Add {{ field }} information</li>
|
1114 |
+
{% endif %}
|
1115 |
+
{% endfor %}
|
1116 |
+
{% endif %}
|
1117 |
+
</ul>
|
1118 |
+
</li>
|
1119 |
+
{% endif %}
|
1120 |
+
|
1121 |
+
{% if completeness_score.section_scores.external_references < completeness_score.max_scores.external_references %}
|
1122 |
+
<li>
|
1123 |
+
<strong>Add External References</strong> (+{{ ((completeness_score.max_scores.external_references - completeness_score.section_scores.external_references) * 0.1)|round(1) }} points):
|
1124 |
+
<ul>
|
1125 |
+
{% if completeness_score.missing_fields.critical %}
|
1126 |
+
{% for field in completeness_score.missing_fields.critical %}
|
1127 |
+
{% if field == "downloadLocation" %}
|
1128 |
+
<li>Add download location reference</li>
|
1129 |
+
{% endif %}
|
1130 |
+
{% endfor %}
|
1131 |
+
{% endif %}
|
1132 |
+
<li>Add links to model card, repository, and dataset</li>
|
1133 |
+
</ul>
|
1134 |
+
</li>
|
1135 |
+
{% endif %}
|
1136 |
+
|
1137 |
+
{% if completeness_score.validation and not completeness_score.validation.valid %}
|
1138 |
+
<li>
|
1139 |
+
<strong>Fix Validation Issues</strong> (remove validation penalty):
|
1140 |
+
<ul>
|
1141 |
+
{% for recommendation in completeness_score.validation.recommendations %}
|
1142 |
+
<li>{{ recommendation }}</li>
|
1143 |
+
{% endfor %}
|
1144 |
+
</ul>
|
1145 |
+
</li>
|
1146 |
+
{% endif %}
|
1147 |
+
</ol>
|
1148 |
+
</div>
|
1149 |
+
|
1150 |
+
<!-- Scoring Rubric Section -->
|
1151 |
+
<div class="calculation-section scoring-rubric">
|
1152 |
+
<h4>Scoring Rubric</h4>
|
1153 |
+
<p>The overall score is calculated using a <strong>weighted normalization</strong> approach:</p>
|
1154 |
+
<p><strong>Total Score = Sum of (Section Score × Section Weight)</strong></p>
|
1155 |
+
<p>Where:</p>
|
1156 |
+
<ul>
|
1157 |
+
<li>Section Score = Points earned in that section</li>
|
1158 |
+
<li>Section Weight = Section's maximum points ÷ Total possible points (100)</li>
|
1159 |
+
</ul>
|
1160 |
+
|
1161 |
+
<div class="note-box">
|
1162 |
+
<p><strong>Example calculation:</strong> If your SBOM has these section scores:</p>
|
1163 |
+
<ul>
|
1164 |
+
<li>Required Fields: 20 points × 0.20 weight = 4.0 points</li>
|
1165 |
+
<li>Metadata: 15 points × 0.20 weight = 3.0 points</li>
|
1166 |
+
<li>Component Basic: 10 points × 0.20 weight = 2.0 points</li>
|
1167 |
+
<li>Model Card: 10 points × 0.30 weight = 3.0 points</li>
|
1168 |
+
<li>External References: 5 points × 0.10 weight = 0.5 points</li>
|
1169 |
+
</ul>
|
1170 |
+
<p>The total score would be 12.5 points, even though the raw section scores sum to 60 points.</p>
|
1171 |
+
<p><strong>Note:</strong> The total score is <em>not</em> the sum of section scores. Each section contributes proportionally to its weight in the final score.</p>
|
1172 |
+
</div>
|
1173 |
+
|
1174 |
+
<p>Fields are classified into three tiers based on importance:</p>
|
1175 |
+
<ul>
|
1176 |
+
<li><span class="field-tier tier-critical"></span> <strong>Critical fields</strong>: Highest weight (3-4 points each)</li>
|
1177 |
+
<li><span class="field-tier tier-important"></span> <strong>Important fields</strong>: Medium weight (2-4 points each)</li>
|
1178 |
+
<li><span class="field-tier tier-supplementary"></span> <strong>Supplementary fields</strong>: Lower weight (1-2 points each)</li>
|
1179 |
+
</ul>
|
1180 |
+
|
1181 |
+
<p>Penalties are applied for missing critical fields:</p>
|
1182 |
+
<ul>
|
1183 |
+
<li>Missing >3 critical fields: 20% penalty (score × 0.8)</li>
|
1184 |
+
<li>Missing 1-3 critical fields: 10% penalty (score × 0.9)</li>
|
1185 |
+
<li>Missing >5 important fields: 5% penalty (score × 0.95)</li>
|
1186 |
+
</ul>
|
1187 |
+
|
1188 |
+
{% if completeness_score.validation_penalty %}
|
1189 |
+
<p>Additional penalties are applied based on validation results:</p>
|
1190 |
+
<ul>
|
1191 |
+
<li>Schema errors: Up to 50% reduction (10% per error)</li>
|
1192 |
+
<li>Schema warnings: Up to 20% reduction (5% per warning)</li>
|
1193 |
+
</ul>
|
1194 |
+
{% endif %}
|
1195 |
+
</div>
|
1196 |
+
</div>
|
1197 |
+
</div>
|
1198 |
+
</div>
|
1199 |
+
|
1200 |
+
<div class="content-section" style="text-align: center;">
|
1201 |
+
<h3>🗣️ Help Us Spread the Word</h3>
|
1202 |
+
<p>If you find this tool useful, share it with your network! <a href="https://sbom.aetheris.ai" target="_blank" rel="noopener noreferrer">https://sbom.aetheris.ai</a></p>
|
1203 |
+
<a href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fsbom.aetheris.ai" target="_blank" rel="noopener noreferrer" style="text-decoration: none;">
|
1204 |
+
<button style="background-color: #0077b5;">🔗 Share on LinkedIn</button>
|
1205 |
+
</a>
|
1206 |
+
<p style="margin-top: 10px; font-size: 14px;">
|
1207 |
+
Follow us for updates:
|
1208 |
+
<a href="https://www.linkedin.com/company/aetheris-ai" target="_blank" rel="noopener noreferrer">@Aetheris AI</a>
|
1209 |
+
</p>
|
1210 |
+
</div>
|
1211 |
+
|
1212 |
+
<!-- Info Section -->
|
1213 |
+
<div class="content-section" style="text-align: center;>
|
1214 |
+
<!-- Display the SBOM count -->
|
1215 |
+
<div class="sbom-count">🚀 Generated AI SBOMs using this tool: <strong>{{ sbom_count if sbom_count else 'N/A' }}</strong></div>
|
1216 |
+
</div>
|
1217 |
+
|
1218 |
+
<!-- Footer -->
|
1219 |
+
<div class="footer">
|
1220 |
+
<p>© 2025 AI SBOM Generator | Powered by Aetheris AI</p>
|
1221 |
+
</div>
|
1222 |
+
</div>
|
1223 |
+
|
1224 |
+
<script>
|
1225 |
+
function switchTab(tabId) {
|
1226 |
+
// Hide all tab contents
|
1227 |
+
var tabContents = document.getElementsByClassName('tab-content');
|
1228 |
+
for (var i = 0; i < tabContents.length; i++) {
|
1229 |
+
tabContents[i].classList.remove('active');
|
1230 |
+
}
|
1231 |
+
|
1232 |
+
// Deactivate all tabs
|
1233 |
+
var tabs = document.getElementsByClassName('aibom-tab');
|
1234 |
+
for (var i = 0; i < tabs.length; i++) {
|
1235 |
+
tabs[i].classList.remove('active');
|
1236 |
+
}
|
1237 |
+
|
1238 |
+
// Activate the selected tab and content
|
1239 |
+
document.getElementById(tabId).classList.add('active');
|
1240 |
+
var selectedTab = document.querySelector('.aibom-tab[onclick="switchTab(\'' + tabId + '\')"]');
|
1241 |
+
selectedTab.classList.add('active');
|
1242 |
+
}
|
1243 |
+
|
1244 |
+
function toggleCollapsible(element) {
|
1245 |
+
element.classList.toggle('active');
|
1246 |
+
var content = element.nextElementSibling;
|
1247 |
+
content.classList.toggle('active');
|
1248 |
+
|
1249 |
+
if (content.classList.contains('active')) {
|
1250 |
+
content.style.maxHeight = content.scrollHeight + 'px';
|
1251 |
+
} else {
|
1252 |
+
content.style.maxHeight = '0';
|
1253 |
+
}
|
1254 |
+
}
|
1255 |
+
|
1256 |
+
function downloadJSON() {
|
1257 |
+
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify({{ aibom|tojson }}, null, 2));
|
1258 |
+
var downloadAnchorNode = document.createElement('a');
|
1259 |
+
downloadAnchorNode.setAttribute("href", dataStr);
|
1260 |
+
downloadAnchorNode.setAttribute("download", "{{ model_id|replace('/', '_') }}_aibom.json");
|
1261 |
+
document.body.appendChild(downloadAnchorNode);
|
1262 |
+
downloadAnchorNode.click();
|
1263 |
+
downloadAnchorNode.remove();
|
1264 |
+
}
|
1265 |
+
|
1266 |
+
// Initialize collapsible sections
|
1267 |
+
document.addEventListener('DOMContentLoaded', function() {
|
1268 |
+
var collapsibles = document.getElementsByClassName('collapsible');
|
1269 |
+
for (var i = 0; i < collapsibles.length; i++) {
|
1270 |
+
toggleCollapsible(collapsibles[i]);
|
1271 |
+
}
|
1272 |
+
});
|
1273 |
+
</script>
|
1274 |
+
</body>
|
1275 |
+
</html>
|