| 1 | """ |
| 2 | Cyclops v2 - fresh-target feed (anti "beaten to the punch"). |
| 3 | |
| 4 | Hunt code that is DAYS old, before anyone else has looked. Watches GitHub |
| 5 | releases/tags for a curated set of high-value repos per archetype and enqueues |
| 6 | freshly-released versions for analysis. New code = unexplored sinks = novel CVEs, |
| 7 | not sibling-CVE rediscovery. |
| 8 | |
| 9 | No auth needed for public release feeds (unauthenticated GitHub API, low rate). |
| 10 | If GH_TOKEN is set it's used to raise the rate limit. |
| 11 | """ |
| 12 | import os, sys, json, time, urllib.request |
| 13 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "memory")) |
| 14 | from memory import Memory |
| 15 | |
| 16 | WATCH = { |
| 17 | "ml-model-loaders": [ |
| 18 | "ggml-org/llama.cpp", "microsoft/onnxruntime", "huggingface/safetensors", |
| 19 | "pytorch/pytorch", "comfyanonymous/ComfyUI", "gradio-app/gradio", |
| 20 | "huggingface/transformers", "vllm-project/vllm", |
| 21 | ], |
| 22 | "ai-agent-mcp": [ |
| 23 | "open-webui/open-webui", "FlowiseAI/Flowise", "n8n-io/n8n", |
| 24 | "langchain-ai/langchain", "run-llama/llama_index", |
| 25 | ], |
| 26 | "devops-infra": [ |
| 27 | "grafana/grafana", "go-gitea/gitea", "jenkinsci/jenkins", |
| 28 | "argoproj/argo-cd", "minio/minio", "metabase/metabase", |
| 29 | ], |
| 30 | "memory-unsafe-parsers": [ |
| 31 | "DaveGamble/cJSON", "libarchive/libarchive", "madler/zlib", |
| 32 | "nlohmann/json", "tafia/quick-xml", |
| 33 | ], |
| 34 | } |
| 35 | |
| 36 | API = "https://api.github.com/repos/{}/releases/latest" |
| 37 | |
| 38 | def _get(url): |
| 39 | req = urllib.request.Request(url, headers={"User-Agent": "cyclops-v2"}) |
| 40 | tok = os.environ.get("GH_TOKEN") |
| 41 | if tok: |
| 42 | req.add_header("Authorization", f"Bearer {tok}") |
| 43 | with urllib.request.urlopen(req, timeout=20) as r: |
| 44 | return json.loads(r.read().decode()) |
| 45 | |
| 46 | def poll(max_age_days=21): |
| 47 | m = Memory() |
| 48 | now = time.time() |
| 49 | fresh = [] |
| 50 | for archetype, repos in WATCH.items(): |
| 51 | for repo in repos: |
| 52 | try: |
| 53 | rel = _get(API.format(repo)) |
| 54 | except Exception as e: |
| 55 | print(f"[feed] {repo}: {e}") |
| 56 | continue |
| 57 | tag = rel.get("tag_name", "") |
| 58 | pub = rel.get("published_at", "") |
| 59 | if not pub: |
| 60 | continue |
| 61 | age = (now - time.mktime(time.strptime(pub, "%Y-%m-%dT%H:%M:%SZ"))) / 86400 |
| 62 | key = f"feed:seen:{repo}:{tag}" |
| 63 | if m.r.sismember("cyclops:feed_seen", key): |
| 64 | continue |
| 65 | m.r.sadd("cyclops:feed_seen", key) |
| 66 | if age <= max_age_days: |
| 67 | job = {"repo": f"github.com/{repo}", "archetype": archetype, |
| 68 | "tag": tag, "released_at": pub, "age_days": round(age, 1)} |
| 69 | m.enqueue("targets", job) |
| 70 | fresh.append(job) |
| 71 | print(f"[feed] FRESH {repo} {tag} ({age:.1f}d old) -> queued") |
| 72 | print(f"[feed] {len(fresh)} fresh targets queued") |
| 73 | return fresh |
| 74 | |
| 75 | if __name__ == "__main__": |
| 76 | poll(int(sys.argv[1]) if len(sys.argv) > 1 else 21) |