Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
python -m venv venv
. ./venv/bin/activate
pip install --upgrade pip setuptools
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: OpenAPI guardrail
run: make openapi-guardrail
- name: Lint Code Base
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ cover:
install-deps-python:
[ -d "./venv" ] && . ./venv/bin/activate &&\
pip install --upgrade pip setuptools &&\
pip install -r requirements.txt
pip install -r requirements-dev.txt

install-deps-typescript:
(cd application/frontend && yarn install)
Expand Down
33 changes: 27 additions & 6 deletions application/cmd/cre_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@
import requests

from collections import deque
from typing import Any, Callable, Dict, List, Optional, Tuple
from typing import Any, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING
import hashlib
import json as _json
from rq import Queue, job, exceptions
from sqlalchemy import not_

from application.utils.external_project_parsers.base_parser import BaseParser
from application.utils.external_project_parsers.parsers import master_spreadsheet_parser
from application import create_app # type: ignore
from application.config import CMDConfig
from application.database import db
Expand All @@ -26,11 +24,12 @@
from application.utils import spreadsheet as sheet_utils
from application.utils import redis
from application.utils import db_backend
from alive_progress import alive_bar
from application.prompt_client import prompt_client as prompt_client
from application.utils import gap_analysis
from application.utils import cres_csv_export

if TYPE_CHECKING:
from application.prompt_client import prompt_client as prompt_client

logging.basicConfig()
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
Expand Down Expand Up @@ -469,6 +468,8 @@ def _standard_structure_fingerprint(resource_name: str) -> str:
return hashlib.sha256(payload.encode("utf-8")).hexdigest()

conn = redis.connect()
from application.prompt_client import prompt_client as prompt_client

ph = prompt_client.PromptHandler(database=collection)
importing_name = standard_entries[0].name
effective_calculate_gap_analysis = (
Expand Down Expand Up @@ -541,7 +542,7 @@ def _standard_structure_fingerprint(resource_name: str) -> str:
def parse_standards_from_spreadsheeet(
cre_file: List[Dict[str, Any]],
cache_location: str,
prompt_handler: prompt_client.PromptHandler,
prompt_handler: "prompt_client.PromptHandler",
) -> None:
"""given a csv with standards, build a list of standards in the db"""
if not cre_file:
Expand All @@ -568,6 +569,10 @@ def parse_standards_from_spreadsheeet(
from application.utils import import_pipeline

collection = db_connect(cache_location)
from application.utils.external_project_parsers.parsers import (
master_spreadsheet_parser,
)

parse_result = master_spreadsheet_parser.MasterSpreadsheetParser.parse_rows(
cre_file
)
Expand Down Expand Up @@ -682,6 +687,8 @@ def download_gap_analysis_from_upstream(cache: str) -> None:
pairs = [(sa, sb) for sa in standards for sb in standards if sa != sb]

if os.environ.get("BENCHMARK_MODE") == "1":
from alive_progress import alive_bar

with alive_bar(len(pairs), title="Fetching upstream Gap Analysis") as bar:
for sa, sb in pairs:
res = requests.get(
Expand Down Expand Up @@ -846,11 +853,15 @@ def backfill_gap_analysis_only(
jobs.append(j)

if jobs:
from alive_progress import alive_bar

with alive_bar(
len(jobs), title=f"GA batch {i // batch_size + 1}"
) as bar:
redis.wait_for_jobs(jobs, bar)
else:
from alive_progress import alive_bar

with alive_bar(len(batch), title=f"GA batch {i // batch_size + 1}") as bar:
for sa, sb in batch:
_compute_pair_direct(collection, sa, sb)
Expand Down Expand Up @@ -901,6 +912,8 @@ def run(args: argparse.Namespace) -> None: # pragma: no cover
cache.delete_nodes(args.delete_resource)

# individual resource importing
from application.utils.external_project_parsers.base_parser import BaseParser

if args.zap_in:
from application.utils.external_project_parsers.parsers import zap_alerts_parser

Expand Down Expand Up @@ -1014,6 +1027,8 @@ def run(args: argparse.Namespace) -> None: # pragma: no cover


def ai_client_init(database: db.Node_collection):
from application.prompt_client import prompt_client as prompt_client

return prompt_client.PromptHandler(database=database)


Expand Down Expand Up @@ -1053,6 +1068,8 @@ def prepare_for_review(cache: str) -> Tuple[str, str]:


def generate_embeddings(db_url: str) -> None:
from application.prompt_client import prompt_client as prompt_client

database = db_connect(path=db_url)
prompt_client.PromptHandler(database, load_all_embeddings=True)

Expand Down Expand Up @@ -1109,6 +1126,8 @@ def run_librarian(
"land W8); running in dry-run mode"
)

from application.prompt_client import prompt_client as prompt_client

cfg = load_config()
database = db_connect(path=cache_file)
ph = prompt_client.PromptHandler(database=database)
Expand Down Expand Up @@ -1207,6 +1226,8 @@ def run_librarian(

def regenerate_embeddings(db_url: str) -> None:
"""Wipe all embedding rows, then rebuild (CRE + every node type) like ``--generate_embeddings``."""
from application.prompt_client import prompt_client as prompt_client

database = db_connect(path=db_url)
removed = database.delete_all_embeddings()
logger.info("Removed %s embedding rows; rebuilding embeddings", removed)
Expand Down
17 changes: 12 additions & 5 deletions application/prompt_client/prompt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@
from application.defs import cre_defs
from datetime import datetime
from multiprocessing import Pool
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize
from io import BytesIO
from urllib.parse import urlparse

from application.prompt_client import embed_alignment

from playwright.sync_api import Error as PlaywrightError
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError, sync_playwright
from scipy import sparse
from sklearn.metrics.pairwise import cosine_similarity
from typing import Dict, List, Any, Tuple, Optional
Expand All @@ -22,7 +18,6 @@
from pypdf import PdfReader
except ImportError:
PdfReader = None # type: ignore[misc, assignment]
import nltk
import numpy as np
import os
import json
Expand Down Expand Up @@ -235,6 +230,10 @@ def __init__(cls):

# Function to get text content from a URL
def get_content(self, url) -> Optional[str]:
# Playwright is for scrape/import embedding generation only — not chat.
from playwright.sync_api import Error as PlaywrightError
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError

for attempts in range(1, 10):
if _is_likely_pdf_url(url):
text = _fetch_pdf_text_for_embeddings(url)
Comment on lines 232 to 239

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path("application/prompt_client/prompt_client.py")
print("exists:", path.exists())
lines = path.read_text().splitlines()
for start in (210, 220, 230, 240, 250, 260):
    end = min(start + 30, len(lines))
    print(f"\n--- lines {start}-{end} ---")
    for i in range(start, end):
        print(f"{i+1:4d}: {lines[i]}")
PY

Repository: OWASP/OpenCRE

Length of output: 8470


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

files = [
    Path("application/prompt_client/prompt_client.py"),
    Path("pyproject.toml"),
    Path("requirements.txt"),
    Path("requirements-dev.txt"),
    Path("setup.py"),
    Path("setup.cfg"),
    Path("Pipfile"),
    Path("poetry.lock"),
]

for p in files:
    if p.exists():
        print(f"\n### {p}")
        text = p.read_text(errors="ignore").splitlines()
        for i, line in enumerate(text, 1):
            if "playwright" in line.lower() or "pdf" in line.lower() or "prompt_client" in line.lower():
                print(f"{i}: {line}")
PY

Repository: OWASP/OpenCRE

Length of output: 4540


Move the Playwright imports below the PDF branch

get_content() imports playwright.sync_api before checking _is_likely_pdf_url(url), so PDF-only calls still fail when Playwright isn’t installed. Move those imports into the non-PDF path to keep PDF extraction lazy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/prompt_client/prompt_client.py` around lines 232 - 239, Move the
PlaywrightError and PlaywrightTimeoutError imports in get_content below the
_is_likely_pdf_url(url) branch, ensuring PDF URLs use
_fetch_pdf_text_for_embeddings without importing Playwright while non-PDF paths
retain access to those exception types.

Expand Down Expand Up @@ -298,6 +297,9 @@ def get_content(self, url) -> Optional[str]:

def get_html(self, url) -> Optional[str]:
"""Return raw HTML document string (for smart excerpt alignment). PDF URLs unsupported."""
from playwright.sync_api import Error as PlaywrightError
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError

for attempts in range(1, 10):
if _is_likely_pdf_url(url):
return None
Expand Down Expand Up @@ -343,6 +345,8 @@ def _ensure_smart_embed_caches(self) -> None:
] = {}

def clean_content(self, content):
from nltk.tokenize import word_tokenize # lazy: scrape/import path only

content = re.sub("\s+", " ", content.strip())

# split into words
Expand All @@ -363,6 +367,9 @@ def with_ai_client(self, ai_client):

def setup_playwright(self):
# in case we want to run without connectivity to ai_client or playwright
import nltk
from playwright.sync_api import sync_playwright

self.__playwright = sync_playwright().start()
nltk.download("punkt")
nltk.download("punkt_tab")
Expand Down
18 changes: 9 additions & 9 deletions application/tests/spreadsheet_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ def test_prepare_spreadsheet(self) -> None:

self.assertCountEqual(result, expected)

@mock.patch("application.utils.spreadsheet.gspread.service_account")
@mock.patch("application.utils.spreadsheet.gspread.oauth")
@mock.patch("gspread.service_account")
@mock.patch("gspread.oauth")
def test_read_spreadsheet_iso_numbers(
self, mock_oauth, mock_service_account
) -> None:
Expand Down Expand Up @@ -165,8 +165,8 @@ def test_read_spreadsheet_iso_numbers(

self.assertEqual(result["ISO Numericise Test"], expected)

@mock.patch("application.utils.spreadsheet.gspread.service_account")
@mock.patch("application.utils.spreadsheet.gspread.oauth")
@mock.patch("gspread.service_account")
@mock.patch("gspread.oauth")
def test_read_spreadsheet_empty_worksheet(
self, mock_oauth, mock_service_account
) -> None:
Expand All @@ -191,8 +191,8 @@ def test_read_spreadsheet_empty_worksheet(

self.assertEqual(result["Empty Sheet"], [])

@mock.patch("application.utils.spreadsheet.gspread.service_account")
@mock.patch("application.utils.spreadsheet.gspread.oauth")
@mock.patch("gspread.service_account")
@mock.patch("gspread.oauth")
def test_read_spreadsheet_short_row_padded(
self, mock_oauth, mock_service_account
) -> None:
Expand Down Expand Up @@ -221,15 +221,15 @@ def test_read_spreadsheet_short_row_padded(
self.assertEqual(result["Short Row"], [{"Col A": "only-a", "Col B": ""}])

def test_records_from_worksheet_values_duplicate_headers(self) -> None:
with self.assertRaises(gspread.exceptions.GSpreadException) as ctx:
with self.assertRaises(ValueError) as ctx:
_records_from_worksheet_values(
[["Col A", "Col B", "Col A"], ["x", "y", "z"]]
)
self.assertIn("Duplicate worksheet headers", str(ctx.exception))
self.assertIn("Col A", str(ctx.exception))

@mock.patch("application.utils.spreadsheet.gspread.service_account")
@mock.patch("application.utils.spreadsheet.gspread.oauth")
@mock.patch("gspread.service_account")
@mock.patch("gspread.oauth")
def test_read_spreadsheet_duplicate_headers(
self, mock_oauth, mock_service_account
) -> None:
Expand Down
4 changes: 3 additions & 1 deletion application/tests/web_main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,9 @@ def test_gap_analysis_create_job_id(
self.assertTrue(enqueue_call_mock.called)
_, kwargs = enqueue_call_mock.call_args
self.assertEqual("aaa->bbb", kwargs["description"])
self.assertEqual(cre_main.run_gap_pair_job, kwargs["func"])
self.assertEqual(
"application.cmd.cre_main.run_gap_pair_job", kwargs["func"]
)
self.assertEqual("aaa", kwargs["kwargs"]["importing_name"])
self.assertEqual("bbb", kwargs["kwargs"]["peer_name"])
self.assertEqual(GAP_ANALYSIS_TIMEOUT, kwargs["timeout"])
Expand Down
5 changes: 3 additions & 2 deletions application/utils/external_project_parsers/base_parser.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from application.utils.external_project_parsers import base_parser_defs
from rq import Queue
from application.utils import redis
from application.prompt_client import prompt_client as prompt_client
import logging
import time
from alive_progress import alive_bar
from application.utils.external_project_parsers.parsers import *
from application.utils import gap_analysis
import os, json
Expand All @@ -22,6 +20,7 @@ def register_resource(
db_connection_str: str,
):
from application.cmd import cre_main
from application.prompt_client import prompt_client as prompt_client

db = cre_main.db_connect(db_connection_str)

Expand Down Expand Up @@ -55,6 +54,8 @@ def call_importers(self, db_connection_str: str):
somehow finds all the importers that have been registered (either reflection for implementing classes or an explicit method that registers all available importers)
and schedules jobs to call those importers, monitors the jobs and alerts when done same as cre_main
"""
from alive_progress import alive_bar

importers = []
jobs = []
conn = redis.connect()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from typing import List, Dict, Optional
from typing import List, Dict, Optional, TYPE_CHECKING
from dataclasses import dataclass
from enum import Enum

from application.defs import cre_defs as defs
from application.prompt_client import prompt_client as prompt_client
from application.database import db

if TYPE_CHECKING:
from application.prompt_client import prompt_client as prompt_client

# abstract class/interface that shows how to import a project that is not cre or its core resources


Expand Down Expand Up @@ -125,7 +127,7 @@ class ParserInterface(object):

def parse(
database: db.Node_collection,
prompt_client: Optional[prompt_client.PromptHandler],
prompt_client: Optional["prompt_client.PromptHandler"],
) -> ParseResult:
"""
Parses the resources of a project,
Expand Down
11 changes: 6 additions & 5 deletions application/utils/spreadsheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from copy import deepcopy
from typing import Any, Dict, List, Optional, Set
import os
import gspread
import yaml
from application.database import db
from application.defs import cre_defs as defs
Expand Down Expand Up @@ -38,9 +37,7 @@ def _records_from_worksheet_values(rows: List[List[Any]]) -> List[Dict[str, Any]
headers = rows[0]
duplicates = sorted(findDups(headers))
if duplicates:
raise gspread.exceptions.GSpreadException(
f"Duplicate worksheet headers: {duplicates}"
)
raise ValueError(f"Duplicate worksheet headers: {duplicates}")
records: List[Dict[str, Any]] = []
for row in rows[1:]:
padded = list(row) + [""] * max(0, len(headers) - len(row))
Expand All @@ -57,6 +54,8 @@ def read_spreadsheet(
) -> Dict[str, Any]:
"""given remote google spreadsheet url,
reads each workbook into a collection of documents"""
import gspread # lazy: Google Sheets import is CLI/local only

result = {}
try:
if (
Expand Down Expand Up @@ -91,7 +90,7 @@ def read_spreadsheet(
logger.error('Error opening spreadsheet "%s" : "%s"' % (alias, url))
logger.error(ae)
exit(1)
except gspread.exceptions.GSpreadException as gse:
except (gspread.exceptions.GSpreadException, ValueError) as gse:
logger.error(
"If this exception says you have a duplicate cell name, the duplicate is",
findDups(wsh.row_values(1)),
Expand Down Expand Up @@ -302,6 +301,8 @@ def write_csv(docs: List[Dict[str, Any]]) -> io.StringIO:

def write_spreadsheet(title: str, docs: List[Dict[str, Any]], emails: List[str]) -> str:
"""upload local array of flat yamls to url, share with email list"""
import gspread # lazy: Google Sheets write is CLI/local only

gc = gspread.oauth() # oauth config,
# TODO (northdpole): make this configurable
sh = gc.create("0." + title)
Expand Down
Loading
Loading