Spaces:
Sleeping
Sleeping
Download app/utils.py from dn-scribe/ai: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/spaces/dn-scribe/ai/resolve/main/app/utils.py
- Command line
-
hf download hf://spaces/dn-scribe/ai/app/utils.py
-
curl -L -o utils.py https://huggingface.co/spaces/dn-scribe/ai/resolve/main/app/utils.py
11.6 kB
| import os | |
| import logging | |
| from datetime import datetime, timedelta | |
| import aiofiles | |
| import json | |
| import string | |
| from openai import AsyncOpenAI | |
| import httpx | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # get working directory | |
| current_dir = os.path.dirname(os.path.abspath(__file__)) | |
| if "/Users/" in current_dir: | |
| # If running on MacOS, set the report directory to a specific path | |
| REPORT_DIR = "tmp/reports" | |
| else: | |
| REPORT_DIR = "/tmp/reports" | |
| PROMPT_DIR = "app/prompts" | |
| os.makedirs(REPORT_DIR, exist_ok=True) | |
| client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
| # Add a helper function to validate CVE-ID | |
| def validate_cve_id(cve_id: str): | |
| parts = cve_id.upper().split("-") | |
| if len(parts) != 3 or not parts[2].isdigit(): | |
| return False | |
| return True | |
| def is_safe_path(base_path: str, target_path: str) -> bool: | |
| """ | |
| Validate that the target path is within the base path and does not involve directory traversal. | |
| Args: | |
| base_path (str): The base directory path. | |
| target_path (str): The target file path to validate. | |
| Returns: | |
| bool: True if the target path is safe, False otherwise. | |
| """ | |
| # Resolve absolute paths | |
| base_path = os.path.abspath(base_path) | |
| target_path = os.path.abspath(target_path) | |
| # Check if the target path starts with the base path | |
| return os.path.commonpath([base_path]) == os.path.commonpath([base_path, target_path]) | |
| async def get_cve_info(cve_id: str) -> dict | None: | |
| """ | |
| Fetch CVE JSON data from cisagov/vulnrichment repo. | |
| Args: | |
| cve_id (str): The CVE identifier, e.g., "CVE-2025-1234". | |
| Returns: | |
| dict | None: Parsed CVE data or None if not found. | |
| """ | |
| try: | |
| parts = cve_id.upper().split("-") | |
| if len(parts) != 3 or not parts[2].isdigit(): | |
| logger.warning(f"Invalid CVE ID format: {cve_id}") | |
| return None | |
| year, cve_num = parts[1], parts[2].lstrip("0") | |
| padded = parts[2].zfill(4) | |
| num_len = len(padded) | |
| if num_len == 4: | |
| group = f"{padded[0]}xxx" | |
| elif num_len == 5: | |
| group = f"{padded[:2]}xxx" | |
| elif num_len == 6: | |
| group = f"{padded[:3]}xxx" | |
| else: | |
| logger.warning(f"Unsupported CVE numeric length: {cve_id}") | |
| return None | |
| url = f"https://raw.githubusercontent.com/cisagov/vulnrichment/develop/{year}/{group}/CVE-{year}-{padded}.json" | |
| logger.info(f"Fetching CVE info from {url}") | |
| async with httpx.AsyncClient(timeout=5.0) as client: | |
| resp = await client.get(url) | |
| logger.info(f"Response status code: {resp}") | |
| if resp.status_code == 200: | |
| return resp.json() | |
| elif resp.status_code == 404: | |
| logger.info(f"CVE {cve_id} not found in vulnrichment.") | |
| return None | |
| else: | |
| logger.error(f"Unexpected status {resp.status_code} for {url}") | |
| return None | |
| except Exception as e: | |
| logger.exception(f"Failed to fetch CVE info for {cve_id}: {e}") | |
| return None | |
| def _report_base_path() -> str: | |
| return REPORT_DIR | |
| def _report_path(cve_id: str, suffix=".md") -> str: | |
| return os.path.join(REPORT_DIR, f"{cve_id}{suffix}") | |
| def _prompt_base_path() -> str: | |
| return PROMPT_DIR | |
| def _prompt_path(prompt_name: str, suffix=".md") -> str: | |
| return os.path.join(PROMPT_DIR, f"{prompt_name}{suffix}") | |
| def _lock_path(cve_id: str) -> str: | |
| return os.path.join(REPORT_DIR, f".{cve_id}.lock") | |
| async def report_exists(cve_id: str, suffix: str = ".md") -> bool: | |
| path = _report_path(cve_id, suffix) | |
| if not os.path.exists(path): | |
| return False | |
| mtime = datetime.fromtimestamp(os.path.getmtime(path)) | |
| return datetime.now() - mtime < timedelta(days=5) | |
| async def load_report(cve_id: str, suffix: str = ".md") -> str: | |
| path = os.path.join(REPORT_DIR, f"{cve_id}{suffix}") | |
| logger.info(f"Loading report for cve_id={cve_id}, path={path}") | |
| async with aiofiles.open(path, "r") as f: | |
| content = await f.read() | |
| logger.info(f"Report loaded for cve_id={cve_id}, content length={len(content)}") | |
| return content if suffix == ".md" else json.loads(content) | |
| async def write_report(cve_id: str, content, suffix: str = ".md"): | |
| path = os.path.join(REPORT_DIR, f"{cve_id}{suffix}") | |
| try: | |
| async with aiofiles.open(path, "w") as f: | |
| if suffix == ".md": | |
| await f.write(content) | |
| else: | |
| await f.write(json.dumps(content, indent=2)) | |
| logger.info(f"✅ Report written: {path}") | |
| except Exception as e: | |
| logger.error(f"❌ Failed to write report to {path}: {e}") | |
| async def get_cve_prompt(cve_id: str) -> str: | |
| template = get_prompt_template("app/prompts/cve_analysis.md") | |
| cve_info_text = "" | |
| cve_info = await get_cve_info(cve_id) | |
| if cve_info: | |
| cve_info_text = json.dumps(cve_info, indent=2) | |
| else: | |
| cve_info_text = f"Could not retrieve data for {cve_id}. Search the web for information: CVSS score, description, exploitability, affected versions and references" | |
| logging.info(f"Retrieved CVE info for {cve_id}: {cve_info_text}") | |
| # if not cve_info: | |
| # raise ValueError(f"Could not retrieve data for {cve_id}") | |
| prompt = template.format(cve_id=cve_id, cve_info=cve_info_text) | |
| return prompt | |
| def get_prompt_template(path: str) -> str: | |
| """ | |
| Reads a prompt from a file and returns it as a string. | |
| Args: | |
| path (str): The file path to read the prompt from. | |
| Returns: | |
| str: The content of the file as a string. | |
| Raises: | |
| FileNotFoundError: If the file does not exist. | |
| IOError: If there is an error reading the file. | |
| """ | |
| try: | |
| with open(path, "r", encoding="utf-8") as file: | |
| return file.read() | |
| except FileNotFoundError: | |
| logger.error(f"Prompt file not found: {path}") | |
| raise FileNotFoundError(f"The specified prompt file at '{path}' was not found. Please ensure the file exists and the path is correct.") | |
| except IOError as e: | |
| logger.error(f"Error reading prompt file {path}: {e}") | |
| raise IOError(f"An error occurred while reading the prompt file at '{path}': {e}") | |
| async def generate_openai_stream(cve_id: str, json_result: bool = False): | |
| logger.info(f"Generating OpenAI stream for cve_id={cve_id}") | |
| editing_directions = "Provide the result as markdown text." | |
| if json_result: | |
| editing_directions = get_prompt_template("app/prompts/cve_analysis_json.md") # No args | |
| prompt = await get_cve_prompt(cve_id) | |
| stream = await client.chat.completions.create( | |
| model = "gpt-4o-search-preview", | |
| web_search_options={ | |
| "search_context_size": "low", | |
| }, | |
| messages=[ | |
| {"role": "system", "content": editing_directions}, | |
| {"role": "user", "content": prompt}], | |
| stream=True, | |
| ) | |
| async for chunk in stream: | |
| delta = chunk.choices[0].delta.content or "" | |
| if delta: | |
| logger.debug(f"Stream chunk received for cve_id={cve_id}, chunk length={len(delta)}") | |
| yield delta | |
| async def generate_openai_json(cve_id: str): | |
| logger.info(f"Generating OpenAI JSON for cve_id={cve_id}") | |
| prompt = await get_cve_prompt(cve_id) | |
| system_prompt = get_prompt_template("app/prompts/cve_analysis_json.md") | |
| model = "gpt-4o-search-preview" | |
| response = await client.chat.completions.create( | |
| model=model, | |
| web_search_options={ | |
| "search_context_size": "low", | |
| }, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| ) | |
| text = response.choices[0].message.content | |
| if not text.strip(): | |
| logger.error(f"OpenAI returned an empty response for cve_id={cve_id}") | |
| raise ValueError("OpenAI returned an empty response. Unable to generate JSON.") | |
| logger.info(f"OpenAI JSON generated for cve_id={cve_id}, response length={len(text)}") | |
| return text | |
| async def generate_openai_report(request_object: dict, format: str = 'json'): | |
| logger.info(f"Generating report in {format} format") | |
| response_format = { "type": "text" } | |
| system_prompt: str = '' | |
| if request_object['prompt']['source'] == 'name': | |
| template = get_prompt_template(_prompt_path(request_object['prompt']['name'])) | |
| if format == 'json': | |
| name, ext = os.path.splitext(_prompt_path(request_object['prompt']['name'])) | |
| system_prompt = get_prompt_template(f"{name}_json{ext}") | |
| response_format = { "type": "json_object" } | |
| else: | |
| template = request_object['prompt']['prompt'] | |
| prompt = safe_format(template, request_object['prompt']['params']) | |
| response = await client.chat.completions.create( | |
| model = "gpt-4o-search-preview", | |
| web_search_options={ | |
| "search_context_size": "low", | |
| }, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| # response_format=response_format, # Not supported with web search | |
| ) | |
| return response.choices[0].message.content | |
| async def ghsa_to_cve(ghsa_id: str) -> str: | |
| """ | |
| Convert a GHSA ID to a CVE ID using OpenAI's API. | |
| Args: | |
| ghsa_id (str): The GHSA identifier, e.g., "GHSA-1234-5678-9abc". | |
| Returns: | |
| str: The corresponding CVE ID or an error message. | |
| """ | |
| logger.info(f"Converting GHSA ID {ghsa_id} to CVE ID") | |
| prompt = f"Convert the following GHSA ID to a CVE ID: {ghsa_id}. Return ONLY the CVE ID (or None if not found)." | |
| model = "gpt-4o-search-preview" | |
| response = await client.chat.completions.create( | |
| model=model, | |
| web_search_options={ | |
| "search_context_size": "low", | |
| }, | |
| messages=[ | |
| {"role": "user", "content": prompt} | |
| ], | |
| ) | |
| # Log input and output token usage | |
| logger.info(f"Input tokens: {response.usage.prompt_tokens}, Output tokens: {response.usage.completion_tokens}") | |
| cve_id = response.choices[0].message.content | |
| if not cve_id.strip(): | |
| logger.error(f"OpenAI returned an empty response for cve_id={cve_id}") | |
| raise ValueError("OpenAI returned an empty response. Unable to generate JSON.") | |
| cve_id = cve_id.rstrip() # Remove any leading/trailing whitespace | |
| if validate_cve_id(cve_id): # Validate CVE-ID | |
| logger.info(f"OpenAI converted GHSA {ghsa_id} to CVE ID {cve_id}") | |
| return cve_id | |
| else: | |
| logger.error(f"OpenAI returned an invalid CVE ID: {cve_id}") | |
| raise ValueError(f"OpenAI returned an invalid CVE ID: {cve_id}") | |
| def safe_format(template: str, params: dict) -> str: | |
| # Filling a prompt string with parameters | |
| logger.info(f"Filling prompt string: {template} with parameters: {params}") | |
| formatter = string.Formatter() | |
| try: | |
| variables = [field_name for _, field_name, _, _ in formatter.parse(template) if field_name] | |
| except ValueError: | |
| raise ValueError(f'Invalid template in prompt_prompt') | |
| if not variables: | |
| return template | |
| missing = [var for var in variables if var not in params] | |
| if missing: | |
| raise KeyError(f'Missing keys for template: {missing}') | |
| filled_template = template.format_map(params) | |
| logger.info(f"Filled prompt string with the parameters: {filled_template}") | |
| return filled_template | |