Compare commits

...

3 Commits

Author SHA1 Message Date
Guillaume BOURGEOIS 381b42b6c7 feat(feed): add CINS Army feed connector (CI Army bad-guys list) 2026-07-12 22:59:20 +02:00
Guillaume BOURGEOIS 5715f21603 feat(feed): add Blocklist.de feed connector (fail2ban attacker lists) 2026-07-12 22:59:19 +02:00
Guillaume BOURGEOIS 7543a95711 feat(feed): add DShield block list feed connector (SANS ISC) 2026-07-12 22:59:18 +02:00
9 changed files with 447 additions and 0 deletions
@@ -0,0 +1,40 @@
id: feed_blocklist_de
name: Blocklist.de Feed
version: 1.0.0
description: "Blocklist.de feed connector — pull the community fail2ban attacker lists (all attackers, or per-service subsets: ssh, mail, apache, ftp, sip, bots, strongips, ...) and emit normalized IOCs (IP + type) for import into the Threat Indicator Manager. Free, no authentication required; stdlib-only, no extra Python dependencies."
changelog: "1.0.0 — Initial release: fetch a blocklist.de attacker list."
category: feed
# Blocklist.de publishes free plain-text IP lists over HTTPS. No key needed.
config_schema:
properties:
list:
type: string
description: "Which list: all, ssh, mail, apache, imap, ftp, sip, bots, strongips, ircbot, bruteforcelogin. Default all"
default: "all"
insecure:
type: boolean
description: "Trust any TLS certificate (not secure)"
default: false
required: []
commands:
- id: fetch_indicators
name: feed-blocklist-de-fetch-indicators
description: "Fetch a blocklist.de attacker list and return normalized indicators."
risk: read
inputs_schema:
properties:
list: { type: string, description: "all | ssh | mail | apache | imap | ftp | sip | bots | strongips | ircbot | bruteforcelogin (overrides config)" }
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
required: []
outputs_schema: { properties: {} }
- id: test_connection
name: feed-blocklist-de-test-connection
description: "Verify the blocklist.de feed is reachable (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,63 @@
import json, os, sys, ssl, urllib.request, urllib.error
BASE = "https://lists.blocklist.de/lists/"
ALLOWED = {
"all", "ssh", "mail", "apache", "imap", "ftp", "sip",
"bots", "strongips", "ircbot", "bruteforcelogin",
}
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _ctx(cfg):
if cfg.get("insecure"):
c = ssl.create_default_context()
c.check_hostname = False
c.verify_mode = ssl.CERT_NONE
return c
return None
def _get(url, cfg):
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
return r.read()
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
key = str(inputs.get("list") or cfg.get("list") or "all").lower()
if key not in ALLOWED:
key = "all"
raw = _get(BASE + key + ".txt", cfg).decode("utf-8", "replace")
maxn = int(inputs.get("max_indicators") or 0)
out = []
for line in raw.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
out.append({"value": line, "type": "ip", "list": key})
if maxn and len(out) >= maxn:
break
return {"source": "blocklist.de:" + key, "count": len(out), "indicators": out}
_run(main)
@@ -0,0 +1,47 @@
import json, os, sys, ssl, urllib.request, urllib.error
BASE = "https://lists.blocklist.de/lists/"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _ctx(cfg):
if cfg.get("insecure"):
c = ssl.create_default_context()
c.check_hostname = False
c.verify_mode = ssl.CERT_NONE
return c
return None
def _get(url, cfg):
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
return r.read()
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
key = str(cfg.get("list") or "all").lower()
raw = _get(BASE + key + ".txt", cfg).decode("utf-8", "replace")
n = sum(1 for ln in raw.splitlines() if ln.strip() and not ln.startswith("#"))
return {"ok": True, "sample_count": n, "list": key}
_run(main)
+38
View File
@@ -0,0 +1,38 @@
id: feed_cins_army
name: CINS Army Feed
version: 1.0.0
description: "CINS Score (CI Army) feed connector — pull the free 'CI Army' bad-guys list (poorly-rated IPs from the CINS threat-intel system that have not yet been seen attacking your own network) and emit normalized IOCs (IP + type) for import into the Threat Indicator Manager. Free, no authentication required; stdlib-only, no extra Python dependencies."
changelog: "1.0.0 — Initial release: fetch the CI Army bad-guys list."
category: feed
# The CI Army list is a free CINS Score feed. No key is required.
config_schema:
properties:
feed_url:
type: string
description: "Override the list URL (default https://cinsscore.com/list/ci-badguys.txt)"
insecure:
type: boolean
description: "Trust any TLS certificate (not secure)"
default: false
required: []
commands:
- id: fetch_indicators
name: feed-cins-army-fetch-indicators
description: "Fetch the CI Army bad-guys list and return normalized indicators."
risk: read
inputs_schema:
properties:
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
required: []
outputs_schema: { properties: {} }
- id: test_connection
name: feed-cins-army-test-connection
description: "Verify the CI Army feed is reachable (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,57 @@
import json, os, sys, ssl, urllib.request, urllib.error
DEFAULT_URL = "https://cinsscore.com/list/ci-badguys.txt"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _ctx(cfg):
if cfg.get("insecure"):
c = ssl.create_default_context()
c.check_hostname = False
c.verify_mode = ssl.CERT_NONE
return c
return None
def _get(url, cfg):
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
return r.read()
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
url = cfg.get("feed_url") or DEFAULT_URL
raw = _get(url, cfg).decode("utf-8", "replace")
maxn = int(inputs.get("max_indicators") or 0)
out = []
for line in raw.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
out.append({"value": line, "type": "ip", "tags": ["ci-army"]})
if maxn and len(out) >= maxn:
break
return {"source": "ci-army", "count": len(out), "indicators": out}
_run(main)
@@ -0,0 +1,47 @@
import json, os, sys, ssl, urllib.request, urllib.error
DEFAULT_URL = "https://cinsscore.com/list/ci-badguys.txt"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _ctx(cfg):
if cfg.get("insecure"):
c = ssl.create_default_context()
c.check_hostname = False
c.verify_mode = ssl.CERT_NONE
return c
return None
def _get(url, cfg):
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
return r.read()
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
url = cfg.get("feed_url") or DEFAULT_URL
raw = _get(url, cfg).decode("utf-8", "replace")
n = sum(1 for ln in raw.splitlines() if ln.strip() and not ln.startswith("#"))
return {"ok": True, "sample_count": n}
_run(main)
+38
View File
@@ -0,0 +1,38 @@
id: feed_dshield
name: DShield Block List Feed
version: 1.0.0
description: "SANS Internet Storm Center DShield feed connector — pull the recommended block list (networks responsible for the most attacks seen by the DShield sensor network) and emit normalized IOCs (CIDR + type, with country) for import into the Threat Indicator Manager. Free, no authentication required; stdlib-only, no extra Python dependencies."
changelog: "1.0.0 — Initial release: fetch the DShield recommended block list."
category: feed
# The DShield block list is a free SANS ISC feed served over HTTPS. No key needed.
config_schema:
properties:
feed_url:
type: string
description: "Override the block-list URL (default https://feeds.dshield.org/block.txt)"
insecure:
type: boolean
description: "Trust any TLS certificate (not secure)"
default: false
required: []
commands:
- id: fetch_indicators
name: feed-dshield-fetch-indicators
description: "Fetch the DShield block list and return normalized indicators."
risk: read
inputs_schema:
properties:
max_indicators: { type: number, description: "Max indicators to return (0 = no limit, default 0)" }
required: []
outputs_schema: { properties: {} }
- id: test_connection
name: feed-dshield-test-connection
description: "Verify the DShield feed is reachable (used by the Test button)."
risk: read
inputs_schema:
properties: {}
required: []
outputs_schema: { properties: {} }
@@ -0,0 +1,70 @@
import json, os, sys, ssl, urllib.request, urllib.error
DEFAULT_URL = "https://feeds.dshield.org/block.txt"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _ctx(cfg):
if cfg.get("insecure"):
c = ssl.create_default_context()
c.check_hostname = False
c.verify_mode = ssl.CERT_NONE
return c
return None
def _get(url, cfg):
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
with urllib.request.urlopen(req, timeout=120, context=_ctx(cfg)) as r:
return r.read()
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
url = cfg.get("feed_url") or DEFAULT_URL
raw = _get(url, cfg).decode("utf-8", "replace")
maxn = int(inputs.get("max_indicators") or 0)
out = []
for line in raw.splitlines():
line = line.rstrip()
if not line or line.lstrip().startswith("#"):
continue
# tab-separated: Start End Netmask Attacks Name Country Email
cols = line.split("\t")
if len(cols) < 3:
cols = line.split()
if len(cols) < 3:
continue
start = cols[0].strip()
netmask = cols[2].strip()
if start.count(".") != 3 or not netmask.isdigit():
continue
item = {"value": start + "/" + netmask, "type": "cidr", "start": start, "netmask": netmask}
if len(cols) > 5 and cols[5].strip():
item["country"] = cols[5].strip()
out.append(item)
if maxn and len(out) >= maxn:
break
return {"source": "dshield:block", "count": len(out), "indicators": out}
_run(main)
@@ -0,0 +1,47 @@
import json, os, sys, ssl, urllib.request, urllib.error
DEFAULT_URL = "https://feeds.dshield.org/block.txt"
def _cfg():
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
def _inputs():
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
def _ctx(cfg):
if cfg.get("insecure"):
c = ssl.create_default_context()
c.check_hostname = False
c.verify_mode = ssl.CERT_NONE
return c
return None
def _get(url, cfg):
req = urllib.request.Request(url, headers={"User-Agent": "Riposte-SOAR"})
with urllib.request.urlopen(req, timeout=60, context=_ctx(cfg)) as r:
return r.read()
def _run(fn):
try:
print(json.dumps(fn(_cfg(), _inputs())))
except urllib.error.HTTPError as e:
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
sys.exit(1)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
def main(cfg, inputs):
url = cfg.get("feed_url") or DEFAULT_URL
raw = _get(url, cfg).decode("utf-8", "replace")
n = sum(1 for ln in raw.splitlines() if ln and not ln.lstrip().startswith("#"))
return {"ok": True, "sample_lines": n}
_run(main)