import hashlib
import json
import os
from pathlib import Path
import requests
URL = "https://fullhunt.io/api/v1/oem/vulnerability-intelligence/feed"
API_KEY = os.environ["FULLHUNT_API_KEY"]
ECOSYSTEM = os.environ["FULLHUNT_PACKAGE_ECOSYSTEM"]
PACKAGE = os.environ["FULLHUNT_PACKAGE_NAME"]
TENANT = os.environ["FULLHUNT_PACKAGE_TENANT"]
DAYS = int(os.environ.get("FULLHUNT_ADVISORY_DAYS", "2"))
STATE_PATH = Path(os.environ.get("FULLHUNT_PACKAGE_STATE", "package-advisory-state.json"))
HEADERS = {"X-API-KEY": API_KEY, "Content-Type": "application/json"}
PER_PAGE = 100
MAX_PAGES = 100
def advisory_key(advisory):
identifier = advisory.get("source_id") or advisory.get("cve_id")
if identifier:
return str(identifier).upper()
material = json.dumps(
{
"source": advisory.get("source"),
"title": advisory.get("title"),
"aliases": advisory.get("aliases"),
},
sort_keys=True,
)
return f"sha256:{hashlib.sha256(material.encode('utf-8')).hexdigest()}"
def fetch_feed(session):
records = []
for page in range(1, MAX_PAGES + 1):
response = session.post(
URL,
headers=HEADERS,
json={
"days": DAYS,
"page": page,
"per_page": PER_PAGE,
"type": "advisories",
"ecosystem": ECOSYSTEM,
"package": PACKAGE,
"query_tags": {
"tenant_id": TENANT,
"workflow": "package-vulnerability-monitoring",
},
},
timeout=60,
)
response.raise_for_status()
payload = response.json()
items = payload.get("advisories")
if not isinstance(items, list):
raise ValueError("Unexpected OEM advisory feed response")
records.extend(items)
total = int(payload.get("metadata", {}).get("total_advisories") or len(records))
if len(items) < PER_PAGE or len(records) >= total:
return records, payload.get("metadata", {})
raise RuntimeError("Advisory feed reached its page guard")
def load_seen():
if not STATE_PATH.exists():
return set()
return set(json.loads(STATE_PATH.read_text(encoding="utf-8")).get("seen", []))
def save_seen(seen, cutoff):
temporary = STATE_PATH.with_suffix(STATE_PATH.suffix + ".tmp")
temporary.write_text(
json.dumps({"seen": sorted(seen), "last_cutoff_epoch": cutoff}, indent=2) + "\n",
encoding="utf-8",
)
temporary.replace(STATE_PATH)
def normalize(advisory):
aliases = {str(value).upper() for value in advisory.get("aliases") or []}
if advisory.get("cve_id"):
aliases.add(str(advisory["cve_id"]).upper())
return {
"advisory_id": advisory.get("source_id"),
"aliases": sorted(aliases),
"title": advisory.get("title"),
"severity": advisory.get("severity"),
"affected": advisory.get("affected") or [],
"is_kev": advisory.get("is_kev"),
"is_exploit_available": advisory.get("is_exploit_available"),
"epss_score": advisory.get("epss_score"),
"published_date": advisory.get("published_date"),
"modified_date": advisory.get("last_modified_date"),
"reference_url": advisory.get("reference_url"),
}
def main():
if DAYS < 1 or DAYS > 7:
raise ValueError("FULLHUNT_ADVISORY_DAYS must be between 1 and 7")
seen = load_seen()
with requests.Session() as session:
advisories, metadata = fetch_feed(session)
current = {advisory_key(item): item for item in advisories}
new_records = [normalize(item) for key, item in current.items() if key not in seen]
save_seen(seen | set(current), metadata.get("cutoff_epoch"))
print(json.dumps({"ecosystem": ECOSYSTEM, "package": PACKAGE, "new_advisories": new_records}, indent=2))
if __name__ == "__main__":
main()