Maximuz23 commited on
Commit
f57882e
·
verified ·
1 Parent(s): fb6701f
Files changed (5) hide show
  1. Dockerfile +24 -13
  2. README.md +38 -11
  3. app.py +179 -0
  4. examples.json +7 -0
  5. requirements.txt +8 -3
Dockerfile CHANGED
@@ -1,20 +1,31 @@
1
- FROM python:3.13.5-slim
 
 
 
 
2
 
3
- WORKDIR /app
4
-
5
- RUN apt-get update && apt-get install -y \
6
- build-essential \
7
- curl \
8
- git \
9
  && rm -rf /var/lib/apt/lists/*
10
 
11
- COPY requirements.txt ./
12
- COPY src/ ./src/
 
 
 
 
13
 
14
- RUN pip3 install -r requirements.txt
15
 
16
- EXPOSE 8501
 
 
17
 
18
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
 
 
 
19
 
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
 
 
1
+ # Text OSINT AI demo — Docker Space (CPU). Runs the Streamlit app.
2
+ # For a GPU Space, swap the base for an NVIDIA CUDA image (e.g.
3
+ # nvidia/cuda:12.4.1-runtime-ubuntu22.04 + python), install bitsandbytes,
4
+ # and the app's USE_GPU path loads the 4-bit base automatically.
5
+ FROM python:3.12-slim
6
 
7
+ # curl is used by the Streamlit healthcheck below
8
+ RUN apt-get update && apt-get install -y --no-install-recommends curl \
 
 
 
 
9
  && rm -rf /var/lib/apt/lists/*
10
 
11
+ # Non-root user (HF Spaces best practice — keeps the HF model cache writable)
12
+ RUN useradd -m -u 1000 user
13
+ USER user
14
+ ENV HOME=/home/user \
15
+ PATH=/home/user/.local/bin:$PATH \
16
+ HF_HOME=/home/user/.cache/huggingface
17
 
18
+ WORKDIR $HOME/app
19
 
20
+ COPY --chown=user requirements.txt ./
21
+ RUN pip install --no-cache-dir --upgrade pip \
22
+ && pip install --no-cache-dir -r requirements.txt
23
 
24
+ COPY --chown=user . .
25
+
26
+ EXPOSE 8501
27
+ HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1
28
 
29
+ ENTRYPOINT ["streamlit", "run", "app.py", \
30
+ "--server.port=8501", "--server.address=0.0.0.0", \
31
+ "--server.headless=true"]
README.md CHANGED
@@ -1,20 +1,47 @@
1
  ---
2
- title: TextScout
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
  sdk: docker
7
  app_port: 8501
8
- tags:
9
- - streamlit
10
  pinned: false
11
- short_description: ' Fine-tuned Llama 3.2 3B for red team Text OSINT.'
12
  license: bigscience-openrail-m
13
  ---
14
 
15
- # Welcome to Streamlit!
16
 
17
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
 
 
 
18
 
19
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Text OSINT AI
3
+ emoji: 🛡️
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: docker
7
  app_port: 8501
 
 
8
  pinned: false
 
9
  license: bigscience-openrail-m
10
  ---
11
 
12
+ # Text OSINT AI — demo
13
 
14
+ Live demo of the fine-tuned **Text OSINT AI** model: a Llama-3.2-3B LoRA adapter
15
+ (`Maximuz23/Text-OSINT`) for red-team threat intelligence. It extracts IOCs,
16
+ profiles threat actors, and maps MITRE ATT&CK from a supplied record — and
17
+ **refuses to fabricate** when the lookup is empty (evidence-gated honesty).
18
 
19
+ This is a **Docker** Space. Files: `Dockerfile`, `app.py`, `requirements.txt`,
20
+ `examples.json`. The container installs deps and runs
21
+ `streamlit run app.py` on port 8501.
22
+
23
+ ## Pushing updates
24
+ From the `osint-project` root, with the `hf` CLI authenticated:
25
+ ```bash
26
+ hf upload <user>/<space> demo . --type space
27
+ ```
28
+ If the adapter repo `Maximuz23/Text-OSINT` is **private**, add a Space secret
29
+ `HF_TOKEN` (Settings → Variables and secrets) with a read token.
30
+
31
+ ## CPU vs GPU
32
+ - **Free CPU** runs the 3B model; the first generation is ~30–60s (model load),
33
+ then faster. Lower *Max new tokens* in the sidebar for a snappier live demo.
34
+ - For instant responses, switch the Space hardware to **T4 small** (~$0.40/hr),
35
+ change the `Dockerfile` base to an NVIDIA CUDA image, and uncomment
36
+ `bitsandbytes` in `requirements.txt` (the GPU path uses the 4-bit base).
37
+ **Pause the Space after the demo** so you stop paying.
38
+
39
+ ## Contract — do not drift
40
+ `app.py` mirrors `ai-test.ipynb` exactly: the system prompt, chat template,
41
+ greedy decoding (`do_sample=False`), and 384 max new tokens. If you retrain and
42
+ the eval contract changes, update both together or the model goes off-distribution.
43
+
44
+ ## Phase 2 (optional)
45
+ Add live-API enrichment: user types a CVE id / actor name → fetch NVD / CISA KEV /
46
+ MITRE → build the record in this same format → feed the model. Keeps the demo on
47
+ "current ground truth" instead of pasted records. Needs NVD/OTX keys as secrets.
app.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Text OSINT AI — live demo (Streamlit, for Hugging Face Spaces).
3
+
4
+ Loads the fine-tuned LoRA adapter `Maximuz23/Text-OSINT` on Llama-3.2-3B-Instruct
5
+ and reproduces the EXACT inference contract from ai-test.ipynb (system prompt,
6
+ chat template, greedy decoding, 384 new tokens). Keep these in sync with the
7
+ notebook — any drift puts the model off-distribution and the demo looks broken.
8
+
9
+ The point of the demo is the honesty differentiator: the model extracts/structures
10
+ intel from a real record, and *refuses* (instead of hallucinating) when the lookup
11
+ is empty — fake CVE id, unknown actor, etc.
12
+ """
13
+ import os
14
+ import json
15
+ import time
16
+ import pathlib
17
+ import re
18
+
19
+ import streamlit as st
20
+ import torch
21
+ from transformers import AutoModelForCausalLM, AutoTokenizer
22
+ from peft import PeftModel
23
+
24
+ # --- contract (mirror ai-test.ipynb cells 1 & 3) --------------------------------
25
+ HF_REPO = "Maximuz23/Text-OSINT"
26
+ USE_GPU = torch.cuda.is_available()
27
+ # GPU Space -> the 4-bit base used in training (needs bitsandbytes).
28
+ # Free CPU Space -> the 16-bit base (ungated); the adapter applies to either.
29
+ BASE_MODEL = "unsloth/Llama-3.2-3B-Instruct-bnb-4bit" if USE_GPU else "unsloth/Llama-3.2-3B-Instruct"
30
+ MAX_NEW_TOKENS_DEFAULT = 384
31
+ HF_TOKEN = os.environ.get("HF_TOKEN") # set as a Space secret if the adapter repo is private
32
+
33
+ SYSTEM_PROMPT = (
34
+ "You are an expert cybersecurity analyst specializing in Text OSINT and threat "
35
+ "intelligence for red team operations. You analyze unstructured text to extract "
36
+ "threat indicators, profile threat actors, map TTPs to MITRE ATT&CK, reconstruct "
37
+ "attack timelines, and produce actionable intelligence for offensive security "
38
+ "engagements. Work only from the record provided: extract and analyze what is present, "
39
+ "and when a lookup is empty or no record is given, say so plainly instead of inventing "
40
+ "details. Judge by the evidence in the input, not by whether a name looks familiar."
41
+ )
42
+
43
+ # Light heuristic only for the on-screen badge (NOT the eval scorer).
44
+ REFUSAL_HINTS = re.compile(
45
+ r"no record (?:for|of|found)|won'?t fabricat|returns? no data|not indexed|"
46
+ r"no authoritative record|cannot produce an assessment|no matching group",
47
+ re.I,
48
+ )
49
+
50
+ EXAMPLES_PATH = pathlib.Path(__file__).parent / "examples.json"
51
+ EXAMPLE_LABELS = {
52
+ "real_cve": "Real CVE — Log4Shell → expect: structured assessment",
53
+ "fake_cve": "Fake CVE — CVE-9999-987654 → expect: refusal",
54
+ "real_actor": "Real actor — APT28 → expect: threat profile",
55
+ "fake_actor": "Fake actor — APT-Lyrebird-77 → expect: refusal",
56
+ "raw_report": "Raw report — abuse.ch malware URL → expect: IOC extraction",
57
+ }
58
+
59
+
60
+ @st.cache_resource(show_spinner="Loading Llama-3.2-3B + Text-OSINT adapter (first load is slow)…")
61
+ def load_model():
62
+ tok = AutoTokenizer.from_pretrained(BASE_MODEL, token=HF_TOKEN)
63
+ if tok.pad_token is None:
64
+ tok.pad_token = tok.eos_token
65
+ tok.padding_side = "left"
66
+ kwargs = dict(token=HF_TOKEN)
67
+ if USE_GPU:
68
+ kwargs.update(device_map={"": 0}, torch_dtype=torch.float16)
69
+ else:
70
+ # bf16 halves RAM vs fp32 (~6GB for 3B, fits the free 16GB CPU Space) and,
71
+ # unlike fp16, runs on CPU. low_cpu_mem_usage avoids a 2x spike at load.
72
+ kwargs.update(torch_dtype=torch.bfloat16, low_cpu_mem_usage=True)
73
+ base = AutoModelForCausalLM.from_pretrained(BASE_MODEL, **kwargs)
74
+ base.config.use_cache = True
75
+ model = PeftModel.from_pretrained(base, HF_REPO, token=HF_TOKEN)
76
+ model.eval()
77
+ return tok, model
78
+
79
+
80
+ def _device(model):
81
+ return next(model.parameters()).device
82
+
83
+
84
+ def generate(prompt, use_adapter=True, max_new_tokens=MAX_NEW_TOKENS_DEFAULT):
85
+ tok, model = load_model()
86
+ messages = [
87
+ {"role": "system", "content": SYSTEM_PROMPT},
88
+ {"role": "user", "content": prompt},
89
+ ]
90
+ inputs = tok.apply_chat_template(
91
+ messages, tokenize=True, add_generation_prompt=True,
92
+ return_tensors="pt", return_dict=True,
93
+ )
94
+ inputs = {k: v.to(_device(model)) for k, v in inputs.items()}
95
+ gen_kwargs = dict(max_new_tokens=max_new_tokens, do_sample=False, pad_token_id=tok.eos_token_id)
96
+ with torch.no_grad():
97
+ if use_adapter:
98
+ out = model.generate(**inputs, **gen_kwargs)
99
+ else:
100
+ with model.disable_adapter():
101
+ out = model.generate(**inputs, **gen_kwargs)
102
+ in_len = inputs["input_ids"].shape[1]
103
+ return tok.decode(out[0][in_len:], skip_special_tokens=True).strip()
104
+
105
+
106
+ def badge(text):
107
+ if REFUSAL_HINTS.search(text):
108
+ st.warning("🛡️ **Refused** — no source record (honesty guardrail held)")
109
+ else:
110
+ st.success("✅ **Extracted** from the record")
111
+
112
+
113
+ # --- UI -------------------------------------------------------------------------
114
+ st.set_page_config(page_title="Text OSINT AI", page_icon="🛡️", layout="wide")
115
+ st.title("🛡️ Text OSINT AI — red-team threat-intel assistant")
116
+ st.caption(
117
+ "Fine-tuned Llama-3.2-3B (LoRA adapter `Maximuz23/Text-OSINT`). It extracts IOCs, "
118
+ "profiles actors, and maps MITRE ATT&CK from a supplied record — and **refuses to "
119
+ "fabricate** when the lookup is empty (fake CVE, unknown actor). Evidence-gated honesty."
120
+ )
121
+
122
+ with st.sidebar:
123
+ st.subheader("Model")
124
+ st.markdown(
125
+ f"- **Base:** `{BASE_MODEL}`\n"
126
+ f"- **Adapter:** `{HF_REPO}`\n"
127
+ f"- **Device:** `{'GPU' if USE_GPU else 'CPU (free)'}`"
128
+ )
129
+ st.divider()
130
+ max_tokens = st.slider(
131
+ "Max new tokens", 128, 512, MAX_NEW_TOKENS_DEFAULT, 32,
132
+ help="384 matches the eval. Lower it for faster CPU demos.",
133
+ )
134
+ show_base = st.checkbox(
135
+ "Also run the BASE model (show what fine-tuning fixed)", value=False,
136
+ help="Doubles latency. Best on a fake input: base waffles, fine-tune refuses.",
137
+ )
138
+ st.caption("Inputs use the same record format the model was trained on — load an example to see it.")
139
+
140
+ examples = json.loads(EXAMPLES_PATH.read_text())
141
+ st.session_state.setdefault("input_text", examples["fake_cve"])
142
+
143
+ col_in, col_pick = st.columns([3, 1])
144
+ with col_pick:
145
+ choice = st.selectbox("Try an example", list(EXAMPLE_LABELS), format_func=lambda k: EXAMPLE_LABELS[k])
146
+ if st.button("⤵ Load example", use_container_width=True):
147
+ st.session_state.input_text = examples[choice]
148
+ with col_in:
149
+ prompt = st.text_area("Record / report to analyze", key="input_text", height=240)
150
+
151
+ if st.button("🔍 Analyze", type="primary"):
152
+ if not prompt.strip():
153
+ st.error("Paste a record or load an example first.")
154
+ st.stop()
155
+
156
+ if show_base:
157
+ col_ft, col_base = st.columns(2)
158
+ with col_ft:
159
+ st.markdown("#### Fine-tuned (Text-OSINT)")
160
+ with st.spinner("Generating…"):
161
+ t0 = time.time()
162
+ out = generate(prompt, use_adapter=True, max_new_tokens=max_tokens)
163
+ badge(out)
164
+ st.code(out, language="markdown")
165
+ st.caption(f"{time.time() - t0:.1f}s")
166
+ with col_base:
167
+ st.markdown("#### Base Llama-3.2-3B (no adapter)")
168
+ with st.spinner("Generating…"):
169
+ t0 = time.time()
170
+ out_b = generate(prompt, use_adapter=False, max_new_tokens=max_tokens)
171
+ st.code(out_b, language="markdown")
172
+ st.caption(f"{time.time() - t0:.1f}s")
173
+ else:
174
+ with st.spinner("Generating…"):
175
+ t0 = time.time()
176
+ out = generate(prompt, use_adapter=True, max_new_tokens=max_tokens)
177
+ badge(out)
178
+ st.code(out, language="markdown")
179
+ st.caption(f"{time.time() - t0:.1f}s")
examples.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "real_cve": "[CVE Record]\nCVE: CVE-2021-44228\nName: Apache Log4j2 Remote Code Execution Vulnerability\nVendor/Product: Apache Log4j2\nDescription: Apache Log4j2 contains a vulnerability where JNDI features do not protect against attacker-controlled JNDI-related endpoints, allowing for remote code execution.\nCISA KEV: listed (confirmed exploited in the wild)\nRequired action: For all affected software assets for which updates exist, the only acceptable remediation actions are: 1) Apply updates; OR 2) remove affected assets from agency networks. Temporary mitigations using one of the measures provided at https://www.cisa.gov/uscert/ed-22-02-apache-log4j-recommended-mitigation-measures are only acceptable until updates are available.\n\nAssess this CVE for offensive relevance.",
3
+ "fake_cve": "[CVE Record]\nCVE: CVE-9999-987654\nNVD: this identifier is not indexed.\nKEV: not present.\n\nAssess this CVE for offensive relevance.",
4
+ "real_actor": "[MITRE ATT&CK Group lookup]\nName: APT28 (G0007)\nAliases: APT28, IRON TWILIGHT, SNAKEMACKEREL, Swallowtail, Group 74, Sednit, Sofacy, Pawn Storm, Fancy Bear, STRONTIUM, Tsar Team, Threat Group-4127, TG-4127, Forest Blizzard, FROZENLAKE, GruesomeLarch\nAttributed techniques: T1001.001, T1003, T1003.001, T1003.003, T1005, T1014, T1021.002, T1025, T1027.013, T1030, T1036, T1036.005\nDescription: APT28 is a threat group that has been attributed to Russia's General Staff Main Intelligence Directorate (GRU) 85th Main Special Service Center (GTsSS) military unit 26165. This group has been active since at least 2004. APT28 reportedly compromised the Hillary Clinton campaign, the Democratic National Committee, and the Democratic Congressional Campaign Committee in 2016 in an attempt to interfere with the U.S. presidential election. In 2018, the US indicted five GRU Unit 26165 officers associated with APT28 for cyber operations (including close-access operations) conducted between 2014 and 2018 against the World Anti-Doping Agency (WADA), the US Anti-Doping Agency, a US nuclear facility, t\n\nProfile this threat actor and summarize how they operate.",
5
+ "fake_actor": "[MITRE ATT&CK Group lookup]\nQuery: APT-Lyrebird-77\nResult: query returned no results.\n\nProfile this threat actor.",
6
+ "raw_report": "This URL was reported to abuse.ch as a malware distribution point. Extract IOCs and explain its threat relevance:\n\nhxxps://wash8siteview.felo7wave[.]surf/software-distribution-dxnp2c7/meta-verify.index malware_download"
7
+ }
requirements.txt CHANGED
@@ -1,3 +1,8 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
1
+ streamlit>=1.40
2
+ torch>=2.2
3
+ transformers>=4.45
4
+ peft>=0.13
5
+ accelerate>=0.34
6
+ sentencepiece
7
+ # --- GPU Space only (the 4-bit base needs this) ---
8
+ # bitsandbytes>=0.43