CyberIntelCollector is a Python 3 application for defensive cyber-intelligence research. It collects configured RSS feeds, normalizes article metadata, and stores immutable source material in SQLite.
The initial release deliberately contains no AI or LLM integration.
- RSS sources defined in YAML rather than source code
- Dedicated CISA KEV/advisory, MITRE ATT&CK STIX 2.1, and DC3/DCISE document collectors
- SQLite storage with an append-only
articlestable - SHA-256 duplicate detection
- Per-source failure isolation
- Structured JSON Lines logging
- Repeatable command-line collection summary
- Versioned, deterministic article enrichment and daily delta reports
- Optional macOS
launchdautomation using project-owned templates
From the project directory:
cd ~/Desktop/PARA/01_Projects/GitHub/CyberIntelCollector
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txtAll dependencies are installed inside .venv; no global installation is
required.
Edit config/sources.yaml:
sources:
- name: Example Security Feed
url: https://example.org/security/feed.xml
category: news
enabled: trueSupported categories are government, vendors, news, and frameworks.
Set enabled: false to retain a definition without collecting it.
The same YAML file also configures dedicated collectors:
collectors:
cisa:
enabled: true
kev_url: https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json
advisory_feeds:
- name: CISA Cybersecurity Advisories
url: https://www.cisa.gov/cybersecurity-advisories/all.xml
mitre:
enabled: true
enterprise_url: https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/enterprise-attack/enterprise-attack.json
dc3:
enabled: true
pages: []
documents:
- title: DIB-Reported Cyber Threats CY 2026 Q1
url: https://www.dc3.mil/Portals/100/Documents/DC3/Missions/DCISE/DCISE%20Slick%20Sheets/DIB%20Cyber%20Threats/2026/DCISE-Quarterly-Sheet-CY2026-Q1-Final.pdfNSA's official cybersecurity publication page was evaluated but is disabled because it rejects automated requests with HTTP 403 and no verified official RSS or Atom endpoint was found.
Activate the environment and run:
source .venv/bin/activate
python main.py collectThe command reports:
- sources attempted
- sources successful
- sources failed
- new items
- duplicates skipped
Default paths:
| Purpose | Path |
|---|---|
| Source definitions | config/sources.yaml |
| SQLite database | data/cyber_intel.db |
| Structured log | logs/collector.jsonl |
Override them when testing or operating another environment:
python main.py collect \
--config /path/to/sources.yaml \
--database /path/to/cyber_intel.db \
--log-file /path/to/collector.jsonlEach log line is a JSON object with a UTC timestamp, level, event, source, URL, and exception details when applicable.
The articles table is the immutable evidence layer. It contains:
id, title, url, source, published_at, collected_at,
summary, content, category, content_hash
content_hash is a unique SHA-256 digest of the immutable collected content.
When that digest already exists, the collector skips the duplicate. SQLite
triggers reject direct updates and deletions, ensuring previously collected
source material cannot be overwritten.
Enrichment never writes annotations into an article or changes its content hash. It creates replaceable, versioned derived records that reference the article ID instead:
tag_definitionsis a sharedtag_type/canonical-name lookup, whilearticle_tagsretains the rules version and exact matched text/field evidence for every article match.tag_definitionsis not a historical copy of each YAML rules version.article_cvesretains normalized CVE matches, KEV status, and KEVdate_addedat correlation time.article_attack_matchesretains exact MITRE ATT&CK object matches and their match basis.event_clustersandevent_cluster_articlesretain derived event grouping and its score/basis evidence.rules_versionsstores the immutable canonical JSON snapshot and SHA-256 digest for every accepted rules version. Reusing a version for different tags, enabled flags, ATT&CK safety settings, scoring, or clustering settings is rejected. An upgrade reserves historical versions that predate this registry aslegacy-unverified, so operators must choose a new version instead of silently attaching new semantics to old derived rows.enrichment_runs,article_enrichment_state, andreport_runsrecord the rules version, timing, status, counts, reporting window, and output path. Each enrichment run also records the highest source article ID visible when the run begins.
Derived rows are correctable without changing source evidence. The current
pipeline processes only articles that lack article_enrichment_state for the
active rules version, so operators must bump the YAML rules version to
re-enrich already processed articles after a tagging-rule, ATT&CK matching, or
authoritative-catalog change. A catalog refresh alone does not recompute stored
ATT&CK or CVE evidence for completed articles. The daily report dynamically
joins the current KEV catalog when it renders CVE/KEV status, but the stored
per-article correlation evidence can therefore be stale until that versioned
re-enrichment occurs. The original articles rows must remain unchanged; they
are the source evidence for all derived results.
Normalized authoritative data is stored separately:
kev_vulnerabilitiescontains the latest searchable CISA KEV facts.attack_objectscontains Enterprise ATT&CK groups, software, campaigns, techniques, tactics, and other STIX objects.attack_relationshipscontains MITRE STIX relationships.
The raw KEV catalog and ATT&CK bundle are also retained as immutable article records, preserving provenance when normalized records are refreshed.
The DC3/DCISE collector discovers public PDFs on the configured official pages, downloads the original bytes, and stores them as immutable government records. Revised documents with different bytes receive new SHA-256 hashes; unchanged files are skipped.
DC3's Akamai configuration currently returns HTTP 403 for automated listing
page requests. The current configuration therefore uses verified direct
official PDF URLs. Page crawling remains supported through pages if DC3
permits automated listing access in the future.
Tagging, scoring, and clustering configuration lives in
config/tagging.yaml. The document is strictly validated and has five
top-level keys:
version: "2026-07-28.3"
attack:
aliases:
Volt Typhoon:
- Vanguard Panda
safe_canonical_names: []
minimum_canonical_alphanumeric_length: 8
ambiguous_canonical_names:
- Windows
tags:
- type: actor
canonical_name: Volt Typhoon
aliases: []
enabled: true
scoring_weights:
government_source: 4
# all eight documented criteria are required
clustering:
threshold: 3
max_days: 7
weights:
cve: 5
# attack, actor, malware, sector, and title_similarity are also requiredSupported tag types are country, actor, malware, sector, and
priority. Review and edit this YAML to add a tag or toggle its optional
enabled flag. ATT&CK aliases and safety decisions live only under attack;
ordinary tag aliases do not authorize ATT&CK matches. Bump version before
re-enriching existing articles after any tagging, ATT&CK matching,
authoritative-catalog, scoring, or clustering change. Once a version has been
used, its complete canonical snapshot is immutable.
Run deterministic enrichment with:
.venv/bin/python main.py enrichOptional paths allow an isolated operation or a reviewed rules file:
.venv/bin/python main.py enrich \
--database /path/to/cyber_intel.db \
--rules /path/to/tagging.yamlThe engine searches title, summary, and textual content. Configured tag
phrases use canonical Unicode normalization (so NFC/NFD-equivalent text
matches), case-insensitive word/phrase boundaries, and retain the exact
original source substring and field as evidence. CVE extraction is intentionally
an ASCII CVE-YYYY-NNNN... pattern and normalizes its matched identifier to
uppercase; it does not claim general Unicode phrase semantics. The engine
prefers longer configured phrases. It indexes non-revoked, supported ATT&CK
objects by their catalog canonical names when those names are long and
unambiguous, plus aliases reviewed under attack.aliases. Short or common
canonical names are excluded unless explicitly approved under
attack.safe_canonical_names; the minimum length and
attack.ambiguous_canonical_names form the versioned safety baseline.
Unsupported or revoked ATT&CK objects are excluded.
Binary content (including PDFs) is deliberately skipped; there is no PDF text
extraction, probabilistic entity recognition, semantic model, or inferred
relationship analysis.
Each pending article is handled in its own transaction, so one malformed row is recorded as an isolated failure instead of stopping the rest of a run. A rerun with no pending article for the active rules version is idempotent; it does not automatically revisit completed rows when authoritative inputs change. The run captures its maximum visible article ID in the same transaction that records the run and applies that cutoff to enrichment and clustering queries, including the authoritative catalog source articles used for KEV and ATT&CK correlation. Articles and catalog updates collected concurrently above the cutoff remain pending for the next run.
The daily brief ranks its window's articles by the explicit
scoring_weights: government source, CVE, KEV match, KEV newly added in the
report date, priority tag, named actor tag, critical-infrastructure sector tag,
and multi-publisher cluster corroboration. Equal scores are ordered by
published time, then collection time, then article ID.
Clusters are evidence links, not merged or deduplicated articles. Candidate
pairs must be no more than clustering.max_days apart by publication time (or
collection time if publication time is absent). A pair receives the full
configured weight for every shared CVE, ATT&CK ID, actor, malware, and sector.
Its title contribution is the Jaccard similarity of normalized, stop-word-free
title-token sets multiplied by title_similarity. Only pairs meeting
clustering.threshold qualify. Qualifying pairs are considered in descending
score order; a merge is accepted only when the resulting component's entire
publication/collection span remains within clustering.max_days. A stable
cluster key includes the rules version, cluster settings, article IDs, and
match basis. The result is useful for transparent corroboration, but it is not
a semantic determination that two reports describe the same incident.
Generate a report for the previous local day (by default America/Chicago):
.venv/bin/python main.py report dailyFor a reproducible operator run, specify both the date and, if needed, the timezone:
.venv/bin/python main.py report daily \
--report-date 2026-07-28 \
--timezone America/ChicagoThe report date is a local calendar date. The program converts local midnight
boundaries to an explicit UTC half-open window, [start, end), and selects
articles by collected_at in that window. Consequently, daylight-saving days
are represented correctly. “New KEV” instead uses CISA's calendar
date_added in the same local report date; it is not a permanent article tag.
The default output root is reports; a run writes both:
reports/daily/YYYY-MM-DD/daily-brief.md
reports/daily/YYYY-MM-DD/daily-brief.json
The Markdown brief is readable for operators and the JSON companion contains
the same explicit window, rules version, scoring weights, delta, corpus totals,
source links, tags, CVE/KEV data, ATT&CK matches, clusters, and top-development
evidence. The brief is a deterministic report, not an AI-generated assessment.
Use --report-root /path/to/reports to direct output elsewhere.
Every report verifies the immutable rules snapshot and reports enrichment
coverage for the selected delta, including exact IDs missing enrichment or
still pending clustering. Such a report is written with partial status
instead of silently claiming completion. The status is present in Markdown,
JSON, command output, and report_runs.
For an ordered manual workflow, run collection, enrichment, and reporting
separately. run-daily is the equivalent convenience command and stops at the
first failed stage:
.venv/bin/python main.py run-daily --report-date 2026-07-28The project supplies source templates under config/launchd/; they are not
generated files. The templates schedule the three jobs in local macOS time:
| Label | Command | Schedule |
|---|---|---|
com.cyberintel.collect |
main.py collect |
06:00 |
com.cyberintel.enrich |
main.py enrich |
06:10 |
com.cyberintel.report-daily |
main.py report daily |
06:20 |
Install or reinstall only those known labels for the current GUI user:
.venv/bin/python scripts/install_launchd.pyThe installer validates the project paths and every template's exact label,
schedule, log paths, and explicit command arguments before changing anything.
For each known label it preserves the prior plist, writes the expanded
replacement atomically, checks bootout, bootstrap, and final service
status, and restores and re-loads the prior job if replacement fails. Labels
are replaced independently, and unrelated LaunchAgents are never altered.
Inspect a job and its logs with:
launchctl print "gui/$UID/com.cyberintel.collect"
launchctl print "gui/$UID/com.cyberintel.enrich"
launchctl print "gui/$UID/com.cyberintel.report-daily"
tail -n 100 logs/launchd/collect.stdout.log
tail -n 100 logs/launchd/collect.stderr.logThe enrich and report labels use matching enrich.* and report-daily.* log
names. To exercise the installed sequence without waiting for its schedule,
kick start the labels in collection, enrichment, then reporting order:
launchctl kickstart -k "gui/$UID/com.cyberintel.collect"
launchctl kickstart -k "gui/$UID/com.cyberintel.enrich"
launchctl kickstart -k "gui/$UID/com.cyberintel.report-daily"The standalone collectors.source_evaluation evaluator is intentionally not
called during collection. It accepts only HTTPS endpoints on official
nsa.gov and fbi.gov domains, with no embedded credentials and only the
default HTTPS port. It follows at most five same-official-domain redirects and
rejects any protocol downgrade. It records the status, redirect chain,
declared and observed content type, structured format, entry count, and
required title/link/date fields. It recommends enabling only nonempty,
complete RSS, Atom, JSON, or STIX responses; HTML-only, empty, unofficial,
blocked, or invalid endpoints remain disabled.
The dated evidence is in
docs/source-evaluations/2026-07-28-nsa-fbi.md.
As evaluated on 2026-07-29 UTC, NSA's official advisory page returned blocked
HTML and had no verified machine-readable endpoint, so it remains disabled.
FBI National Press Releases RSS 2.0 was valid and enabled in
config/sources.yaml; the FBI News Blog RSS 2.0 endpoint was validly served
but empty, so it remains disabled. Do not add HTML scraping or unofficial
mirrors merely to claim coverage.
Before operating against a live database, create a timestamped backup with
SQLite's backup API, record the article count, and compute a SHA-256 digest of
ordered id:content_hash values. This uses only the immutable evidence-layer
identity:
.venv/bin/python - <<'PY'
from datetime import datetime, timezone
from hashlib import sha256
from pathlib import Path
import sqlite3
database = Path("data/cyber_intel.db")
backup = Path("data/backups") / (
"cyber_intel-" + datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + ".db"
)
backup.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(database) as source, sqlite3.connect(backup) as target:
source.backup(target)
with sqlite3.connect(database) as connection:
rows = connection.execute(
"SELECT id, content_hash FROM articles ORDER BY id"
)
digest = sha256(
"".join(f"{article_id}:{content_hash}\\n" for article_id, content_hash in rows)
.encode("utf-8")
).hexdigest()
count = connection.execute("SELECT COUNT(*) FROM articles").fetchone()[0]
print(f"backup={backup} articles={count} digest={digest}")
PYThen follow this evidence-preserving practice run:
- Save the backup path, count, and digest.
- Run
.venv/bin/python main.py collectand record its collection summary. - Record the post-collection digest; enrichment and reporting must not change that immutable-evidence digest.
- Run
.venv/bin/python main.py enrichand record tags, CVEs, KEVs, ATT&CK matches, clusters, and failures. - Run
.venv/bin/python main.py report daily --report-date YYYY-MM-DDand save both reported output paths. - Install and manually kick-start the three LaunchAgents in order, then check
launchctl printoutput and the corresponding launchd logs. - Run SQLite integrity and foreign-key checks, then recompute only the ordered digest. It must match the post-collection digest:
.venv/bin/python - <<'PY'
import sqlite3
with sqlite3.connect("data/cyber_intel.db") as connection:
print(connection.execute("PRAGMA integrity_check").fetchall())
print(connection.execute("PRAGMA foreign_key_check").fetchall())
PY.venv/bin/python - <<'PY'
from hashlib import sha256
import sqlite3
with sqlite3.connect("data/cyber_intel.db") as connection:
rows = connection.execute("SELECT id, content_hash FROM articles ORDER BY id")
print(sha256(
"".join(f"{article_id}:{content_hash}\\n" for article_id, content_hash in rows)
.encode("utf-8")
).hexdigest())
PY- Run the final offline regression suite after the practice run:
.venv/bin/python -m unittest discover -s tests -vKeep the backup and hand off the collection/enrichment summaries, report
paths, launchd labels/status/log paths, endpoint conclusions, integrity result,
and immutable digest comparison. Runtime databases, backups, logs, and reports
are ignored by Git; templates, source code, documentation, and tests remain
tracked. data/reports/ is retained only as a legacy/custom-output compatibility
location; new default reports are written below reports/daily/.
The test suite uses temporary databases and local feed fixtures:
.venv/bin/python -m unittest discover -s tests -vThe tests do not require internet access.