Spaces:
Running on Zero
Running on Zero
| """ | |
| Stage 3 - where this window sits in the corpus Part A measured. | |
| Part A's bake-off picked `nomic-ai/nomic-embed-text-v1.5` over MiniLM and BGE, embedding the | |
| `payload_window` column with the `search_document: ` prefix its model card specifies for indexing, | |
| unit-normalised, 768 dimensions. Those four facts are recorded in `part_a_results.json` under | |
| `winner`, and `check_provenance()` asserts them against that file rather than trusting this | |
| docstring - because a query embedded with a different model, or without the prefix, lands in a | |
| different space and every neighbour returned is meaningless, silently, with no error anywhere. | |
| What it is worth: **precision@5 of 35.6%** against a 6.8% random baseline. Fewer than 2 of the 5 | |
| files returned are the same kind of attack as the query. That is far better than chance and it is | |
| not good, which is why this is labelled "nearest files in the corpus" and never "the same attack". | |
| """ | |
| import json | |
| import re | |
| import urllib.request | |
| import numpy as np | |
| import pandas as pd | |
| REPO = "Cyber-security-final-project/Evaluation_of_OpenSource_Models_for_PDF_Injection_Recognition" | |
| BASE = f"https://huggingface.co/datasets/{REPO}/resolve/main/" | |
| INDEX_URL = BASE + "Part_A_Outputs/corpus_embeddings.parquet" | |
| RESULTS_URL = BASE + "Part_A_Outputs/part_a_results.json" | |
| # Part A's winner. Every field here is asserted against the results file before use. | |
| EMBED_REPO = "nomic-ai/nomic-embed-text-v1.5" | |
| PREFIX = "search_document: " | |
| DIMS = 768 | |
| # The corpus filename encodes its own attack family ("javascript_injection_OWASP_0004.pdf"), and | |
| # the clean controls are hash-named. That is a label on a known corpus file, not a model input - | |
| # nothing here ever shows a filename to a model. | |
| FAMILY_RE = re.compile(r"^([a-z_]+?)_(?:AMTSO|WICAR|OWASP|AtomicRedTeam|Metasploit|Glasswall|" | |
| r"mindcrypt|RanSim|RANSIM|custom)_\d+\.pdf$") | |
| _index = None | |
| _model = None | |
| def family_of(file_id: str) -> str: | |
| """The attack family a corpus file carries, read off its name. Clean files are hash-named.""" | |
| m = FAMILY_RE.match(file_id or "") | |
| return m.group(1) if m else "clean (control)" | |
| def check_provenance() -> dict: | |
| """Assert this module embeds queries the way the index was built. Cheap, and load-bearing.""" | |
| with urllib.request.urlopen(RESULTS_URL) as r: | |
| winner = json.loads(r.read().decode())["winner"] | |
| assert winner["repo"] == EMBED_REPO, f"index built by {winner['repo']}, app uses {EMBED_REPO}" | |
| assert winner["prefix"] == PREFIX, "prefix differs from the one the index was built with" | |
| assert winner["dims"] == DIMS, "dimension mismatch" | |
| assert winner["normalised"], "index is not unit-normalised; the dot product is not cosine" | |
| assert winner["input_column"] == "payload_window", "index was not built on payload windows" | |
| return winner | |
| def load_index(): | |
| """The 1,100 x 768 index Part A exported, straight from the dataset repo. ~3 MB.""" | |
| global _index | |
| if _index is None: | |
| df = pd.read_parquet(INDEX_URL) | |
| _index = {"ids": df["file_id"].to_numpy(), | |
| "matrix": df.drop(columns="file_id").to_numpy(dtype=np.float32)} | |
| return _index | |
| def load_model(): | |
| """Nomic-embed-v1.5 on the CPU. ~550 MB, and a 3,000-character window embeds in a second.""" | |
| global _model | |
| if _model is None: | |
| from sentence_transformers import SentenceTransformer | |
| _model = SentenceTransformer(EMBED_REPO, trust_remote_code=True, device="cpu") | |
| return _model | |
| def embed(text: str) -> np.ndarray: | |
| """Embed with the prefix and normalisation Part A used. Anything else is a different space.""" | |
| return load_model().encode([PREFIX + text], normalize_embeddings=True, | |
| show_progress_bar=False)[0] | |
| def neighbours(text: str, k: int = 5) -> list: | |
| """ | |
| The k corpus files nearest one window. | |
| Vectors are unit-normalised, so the dot product is the cosine similarity and the whole lookup | |
| against 1,100 files is one matrix-vector product - no index structure needed at this size. | |
| """ | |
| idx = load_index() | |
| sims = idx["matrix"] @ embed(text) | |
| top = np.argsort(-sims)[:k] | |
| return [{"file_id": str(idx["ids"][i]), | |
| "family": family_of(str(idx["ids"][i])), | |
| "similarity": round(float(sims[i]), 3)} for i in top] | |
| NEIGHBOUR_COLUMNS = ["corpus file", "its attack family", "cosine similarity"] | |
| def neighbour_rows(text: str, k: int = 5) -> list: | |
| return [[n["file_id"], n["family"], n["similarity"]] for n in neighbours(text, k)] | |