ThreatIntel / scripts /clean_prepared_data.py
Malek98's picture
Deploy FastAPI backend via automated upload
bca5172 verified
Raw
History Blame Contribute Delete
12.6 kB
#!/usr/bin/env python3
"""
scripts/clean_prepared_data.py
Cleans the outputs in `prepared_data/` using heuristic filters to remove noisy / unwanted examples.
Produces:
- prepared_data/cleaned_extracted_text.jsonl
- prepared_data/cleaned_labeled_dataset.csv
- prepared_data/cleaned_instruction_tuning.jsonl
- prepared_data/cleaning_report.json
Usage examples (run from project root):
# Dry-run (no writes), print summary
python scripts/clean_prepared_data.py --prepared-dir prepared_data --dry-run
# Run and write cleaned outputs with defaults
python scripts/clean_prepared_data.py --prepared-dir prepared_data --apply
# More aggressive: drop pages with >10 BTC addresses or >30 [URL] markers
python scripts/clean_prepared_data.py --prepared-dir prepared_data --apply --url-threshold 30 --btc-threshold 10
The heuristics are intentionally conservative; tweak thresholds as needed.
"""
from pathlib import Path
import argparse
import json
import re
import hashlib
from collections import Counter, defaultdict
import csv
import sys
# Regexes
BTC_RE = re.compile(r"\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b")
HEX_RE = re.compile(r"\b0x[a-fA-F0-9]{10,}\b")
LONG_HEX_RE = re.compile(r"\b[A-Fa-f0-9]{40,}\b")
URL_TOKEN = "[URL]"
def sha256_text(s: str) -> str:
return hashlib.sha256(s.encode('utf-8', errors='ignore')).hexdigest()
def load_jsonl(path: Path):
with path.open(encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
try:
yield json.loads(line)
except Exception:
# try to be tolerant to bad lines
continue
def write_jsonl(path: Path, objs):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open('w', encoding='utf-8') as f:
for o in objs:
f.write(json.dumps(o, ensure_ascii=False) + "\n")
def read_csv_as_rows(path: Path):
with path.open(encoding='utf-8', newline='') as f:
reader = csv.DictReader(f)
for r in reader:
yield r
def write_csv(path: Path, rows, fieldnames):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open('w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for r in rows:
writer.writerow(r)
class Cleaner:
def __init__(self, url_threshold=20, min_chars=60, btc_threshold=8, hex_threshold=3, repeat_chunk_thresh=3, blacklist=None, blacklist_enabled=False, max_chars=20000):
self.url_threshold = int(url_threshold)
self.min_chars = int(min_chars)
self.btc_threshold = int(btc_threshold)
self.hex_threshold = int(hex_threshold)
self.repeat_chunk_thresh = int(repeat_chunk_thresh)
self.blacklist = set([b.lower() for b in (blacklist or [])])
self.blacklist_enabled = blacklist_enabled
self.max_chars = int(max_chars)
def _count_repeated_chunks(self, text, chunk_size=60):
# simple repeated-chunk heuristic: count duplicate substrings of length chunk_size
if len(text) < chunk_size * 2:
return 0
chunks = [text[i:i+chunk_size] for i in range(0, len(text)-chunk_size+1, chunk_size)]
c = Counter(chunks)
# how many chunks repeat more than once
repeats = sum(1 for v in c.values() if v > 1)
return repeats
def is_noisy(self, text: str) -> (bool, str):
"""Return (is_noisy, reason) using heuristics observed in your dataset."""
if not text or not text.strip():
return True, 'empty'
t = text
# very short
if len(t) < self.min_chars:
return True, f'too_short:{len(t)}'
# too many [URL] tokens (common in References blocks)
url_tokens = t.count(URL_TOKEN) + len(re.findall(r'https?://', t)) + t.count('file://')
if url_tokens >= self.url_threshold:
return True, f'tool_many_urls:{url_tokens}'
# btc address dumps
btc_count = len(BTC_RE.findall(t))
if btc_count >= self.btc_threshold:
return True, f'btc_dump:{btc_count}'
# hex-like tokens
hex_count = len(HEX_RE.findall(t)) + len(LONG_HEX_RE.findall(t))
if hex_count >= self.hex_threshold:
return True, f'hex_tokens:{hex_count}'
# long reference lists by word heuristics
if 'compteur de visite' in t.lower() or ('references' in t.lower() and url_tokens > 5):
return True, 'references_block'
# repeated chunk heuristic
repeats = self._count_repeated_chunks(t, chunk_size=80)
if repeats >= self.repeat_chunk_thresh:
return True, f'repeated_chunks:{repeats}'
# blacklist
if self.blacklist_enabled:
low = t.lower()
for b in self.blacklist:
if b and b in low:
return True, f'blacklist_match:{b}'
# too long single example: truncate instead of drop
if len(t) > self.max_chars:
# not noisy per se, but will be truncated by the cleaner
return False, 'too_long_truncate'
return False, ''
def clean_text(self, text: str) -> str:
# basic post-processing: collapse whitespace and truncate to max_chars
s = re.sub(r"\s+", " ", text).strip()
if len(s) > self.max_chars:
s = s[:self.max_chars] + "\n\n[TRUNCATED]"
return s
def main():
p = argparse.ArgumentParser()
p.add_argument('--prepared-dir', type=Path, default=Path('prepared_data'))
p.add_argument('--apply', action='store_true', help='Write cleaned outputs. Without this flag runs a dry-run and prints summary')
p.add_argument('--url-threshold', type=int, default=20)
p.add_argument('--min-chars', type=int, default=60)
p.add_argument('--btc-threshold', type=int, default=8)
p.add_argument('--hex-threshold', type=int, default=3)
p.add_argument('--repeat-chunk-thresh', type=int, default=3)
p.add_argument('--blacklist', type=str, default='', help='Comma-separated blacklist terms to drop (optional)')
p.add_argument('--enable-blacklist', action='store_true', help='Enable blacklist matching')
p.add_argument('--max-chars', type=int, default=20000)
args = p.parse_args()
prepared = Path(args.prepared_dir)
if not prepared.exists():
print('prepared_data dir not found:', prepared)
sys.exit(2)
extracted_path = prepared / 'extracted_text.jsonl'
labeled_csv = prepared / 'labeled_dataset.csv'
instruction_j = prepared / 'instruction_tuning.jsonl'
if not extracted_path.exists():
print('extracted_text.jsonl missing at', extracted_path)
sys.exit(2)
cleaner = Cleaner(
url_threshold=args.url_threshold,
min_chars=args.min_chars,
btc_threshold=args.btc_threshold,
hex_threshold=args.hex_threshold,
repeat_chunk_thresh=args.repeat_chunk_thresh,
blacklist=[t.strip() for t in args.blacklist.split(',')] if args.blacklist else None,
blacklist_enabled=args.enable_blacklist,
max_chars=args.max_chars,
)
cleaned_objs = []
stats = defaultdict(int)
reasons = Counter()
kept_sources = set()
seen_hashes = set()
# iterate and filter
for obj in load_jsonl(extracted_path):
text = obj.get('text','')
is_noise, reason = cleaner.is_noisy(text)
if is_noise:
stats['dropped'] += 1
reasons[reason] += 1
continue
# clean text
new_text = cleaner.clean_text(text)
# dedupe by text hash
h = sha256_text(new_text)[:32]
if h in seen_hashes:
stats['duplicate'] += 1
reasons['duplicate'] += 1
continue
seen_hashes.add(h)
# update object
obj['text'] = new_text
cleaned_objs.append(obj)
kept_sources.add(obj.get('source_path') or obj.get('filename') or '')
stats['kept'] += 1
stats['total'] = stats.get('kept',0) + stats.get('dropped',0) + stats.get('duplicate',0)
# prepare output paths
out_extracted = prepared / 'cleaned_extracted_text.jsonl'
out_csv = prepared / 'cleaned_labeled_dataset.csv'
out_instr = prepared / 'cleaned_instruction_tuning.jsonl'
report_path = prepared / 'cleaning_report.json'
# dry-run: print summary
if not args.apply:
print('DRY RUN - no files written')
print('Prepared dir:', prepared)
print('Total input examples:', stats['total'])
print('Kept:', stats['kept'], 'Dropped:', stats['dropped'], 'Duplicates:', stats['duplicate'])
print('Top drop reasons:')
for r, c in reasons.most_common(12):
print(' ', r, c)
print('\nTo write cleaned outputs add --apply')
return
# write cleaned extracted JSONL
write_jsonl(out_extracted, cleaned_objs)
# filter labeled CSV to keep only rows that are in cleaned set
if labeled_csv.exists():
kept_rows = []
fieldnames = None
for row in read_csv_as_rows(labeled_csv):
# match by source_path
sp = row.get('source_path') or ''
if sp in kept_sources:
kept_rows.append(row)
else:
# there may be entries where source_path is just a filename; try filename match
fn = row.get('source_path','')
if kept_rows:
fieldnames = list(kept_rows[0].keys())
write_csv(out_csv, kept_rows, fieldnames)
else:
# fallback: regenerate labeled CSV minimally from cleaned_objs using Unknown label
fieldnames = ['source_path','text','label']
regen = []
for o in cleaned_objs:
regen.append({'source_path': o.get('source_path',''), 'text': o.get('text',''), 'label': 'Unknown'})
write_csv(out_csv, regen, fieldnames)
else:
# No labeled CSV original: create minimal one
fieldnames = ['source_path','text','label']
regen = []
for o in cleaned_objs:
regen.append({'source_path': o.get('source_path',''), 'text': o.get('text',''), 'label': 'Unknown'})
write_csv(out_csv, regen, fieldnames)
# regenerate instruction JSONL from cleaned CSV
# Use same basic prompt as original pipeline to keep format consistent
PROMPT = (
"""Below is an instruction that describes a task, paired with an input that provides further context. \nWrite a response that appropriately completes the request. \nBefore answering, think carefully about the question ensure an accurate response.\n\n### Instruction:\nYou are an expert in cybersecurity and threat intelligence.\nYour role is to provide precise classification of threats from the description provided by the user.\n\n### Description : \n{} \n\n### Response:\n{}"""
)
written = 0
with out_instr.open('w', encoding='utf-8') as outf:
# read from cleaned CSV
for row in read_csv_as_rows(out_csv):
text = row.get('text','')
label = row.get('label','Unknown')
prompt = PROMPT.format(text, '')
combined = prompt + "\n" + label + " <EOS>"
outf.write(json.dumps({'text': combined, 'label': label, 'source': row.get('source_path','')}, ensure_ascii=False) + "\n")
written += 1
# write report
report = {
'input_total': stats['total'],
'kept': stats['kept'],
'dropped': stats['dropped'],
'duplicates': stats['duplicate'],
'drop_reasons': dict(reasons.most_common()),
'out_extracted': str(out_extracted),
'out_csv': str(out_csv),
'out_instruction': str(out_instr),
}
with report_path.open('w', encoding='utf-8') as f:
json.dump(report, f, indent=2)
print('Wrote cleaned extracted JSONL ->', out_extracted)
print('Wrote cleaned labeled CSV ->', out_csv)
print('Wrote cleaned instruction JSONL ->', out_instr)
print('Wrote cleaning report ->', report_path)
print('Summary: kept', stats['kept'], 'dropped', stats['dropped'], 'duplicates', stats['duplicate'])
if __name__ == '__main__':
main()