Skip to content

Commit f369577

Browse files
authored
Merge pull request #784 from jacob-masse/add-flowtriq-modules
Add Flowtriq expansion and export modules for DDoS threat intelligence
2 parents 01ac8c6 + b0bd84b commit f369577

2 files changed

Lines changed: 302 additions & 0 deletions

File tree

documentation/logos/flowtriq.png

6.61 KB
Loading
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
"""
2+
Flowtriq DDoS Intelligence Module for MISP
3+
4+
Enriches IP addresses with DDoS attack data from Flowtriq.
5+
When given an IP, queries the Flowtriq API to check if this IP
6+
has been observed as a DDoS attack source across Flowtriq's
7+
network of monitored infrastructure.
8+
9+
Returns attack context including attack families, severity,
10+
peak traffic rates, geographic origin, and threat intel matches.
11+
"""
12+
13+
import json
14+
import requests
15+
16+
misperrors = {'error': 'Error'}
17+
mispattributes = {
18+
'input': ['ip-src', 'ip-dst'],
19+
'format': 'misp_standard',
20+
}
21+
moduleinfo = {
22+
'version': '1.0',
23+
'author': 'Flowtriq',
24+
'description': 'Query Flowtriq for DDoS attack intelligence on IP addresses',
25+
'module-type': ['expansion', 'hover'],
26+
'name': 'Flowtriq DDoS Intelligence',
27+
'logo': 'flowtriq.png',
28+
'requirements': ['Flowtriq API key and API URL'],
29+
'features': (
30+
'Queries the Flowtriq IP threat lookup API to check whether an IP '
31+
'has been observed as a DDoS attack source. Returns structured '
32+
'enrichment data including attack families, severity breakdown, '
33+
'peak PPS, ASN, country, risk score, and related attacker IPs.'
34+
),
35+
'references': ['https://flowtriq.com'],
36+
'input': 'An IP address attribute (ip-src or ip-dst).',
37+
'output': (
38+
'MISP attributes and objects describing DDoS attack activity '
39+
'associated with the queried IP: attack types, timestamps, '
40+
'severity, peak traffic, related IPs, and threat intel matches.'
41+
),
42+
}
43+
moduleconfig = ['api_key', 'api_url']
44+
45+
_DEFAULT_API_URL = 'https://flowtriq.com'
46+
_TIMEOUT = 15
47+
48+
49+
def handler(q=False):
50+
if q is False:
51+
return False
52+
53+
request = json.loads(q)
54+
55+
# Validate config
56+
config = request.get('config', {})
57+
api_key = config.get('api_key', '').strip()
58+
api_url = config.get('api_url', '').strip().rstrip('/') or _DEFAULT_API_URL
59+
60+
if not api_key:
61+
misperrors['error'] = 'Flowtriq API key is required. Set it in the module configuration.'
62+
return misperrors
63+
64+
# Extract IP from the request
65+
ip = None
66+
attribute = request.get('attribute', {})
67+
for attr_type in ('ip-src', 'ip-dst'):
68+
if attr_type in request:
69+
ip = request[attr_type]
70+
break
71+
if not ip and attribute.get('value'):
72+
ip = attribute['value']
73+
74+
if not ip:
75+
misperrors['error'] = 'No IP address provided in the request.'
76+
return misperrors
77+
78+
# Query Flowtriq IP threat lookup
79+
try:
80+
response = requests.post(
81+
f'{api_url}/api/ip-lookup.php',
82+
json={'ip': ip},
83+
headers={
84+
'Authorization': f'Bearer {api_key}',
85+
'Content-Type': 'application/json',
86+
'Accept': 'application/json',
87+
'User-Agent': 'MISP-Flowtriq/1.0',
88+
},
89+
timeout=_TIMEOUT,
90+
verify=True,
91+
)
92+
except requests.exceptions.ConnectionError:
93+
misperrors['error'] = f'Cannot connect to Flowtriq API at {api_url}'
94+
return misperrors
95+
except requests.exceptions.Timeout:
96+
misperrors['error'] = f'Flowtriq API request timed out after {_TIMEOUT}s'
97+
return misperrors
98+
except requests.exceptions.RequestException as e:
99+
misperrors['error'] = f'Flowtriq API request failed: {e}'
100+
return misperrors
101+
102+
if response.status_code != 200:
103+
misperrors['error'] = f'Flowtriq API returned HTTP {response.status_code}'
104+
return misperrors
105+
106+
try:
107+
data = response.json()
108+
except (ValueError, json.JSONDecodeError):
109+
misperrors['error'] = 'Flowtriq API returned invalid JSON'
110+
return misperrors
111+
112+
if not data.get('ok'):
113+
misperrors['error'] = data.get('error', 'Flowtriq API returned an error')
114+
return misperrors
115+
116+
if not data.get('found'):
117+
return {'results': []}
118+
119+
# Build enrichment results
120+
results = _build_results(ip, data, attribute)
121+
return {'results': results}
122+
123+
124+
def _build_results(ip, data, attribute):
125+
"""Build MISP-standard enrichment results from Flowtriq API response."""
126+
results = []
127+
128+
risk_score = data.get('risk_score', 0)
129+
reputation = data.get('reputation')
130+
incidents = data.get('incidents', {})
131+
threat_intel = data.get('threat_intel', [])
132+
related_ips = data.get('related_ips', {})
133+
ioc_matches = data.get('ioc_matches', {})
134+
135+
# -- Summary comment on the original attribute --
136+
summary_parts = []
137+
summary_parts.append(f'Flowtriq risk score: {risk_score}/100')
138+
139+
total_incidents = incidents.get('total', 0)
140+
if total_incidents:
141+
summary_parts.append(f'Seen in {total_incidents} DDoS incident(s)')
142+
143+
if reputation:
144+
attack_count = reputation.get('attack_count', 0)
145+
tenants_seen = reputation.get('tenants_seen', 0)
146+
if attack_count:
147+
summary_parts.append(f'{attack_count} attacks across {tenants_seen} network(s)')
148+
if reputation.get('top_attack_family'):
149+
summary_parts.append(f'Primary vector: {reputation["top_attack_family"]}')
150+
if reputation.get('country'):
151+
summary_parts.append(f'Country: {reputation["country"]}')
152+
if reputation.get('asn'):
153+
summary_parts.append(f'ASN: {reputation["asn"]}')
154+
if reputation.get('peak_pps'):
155+
summary_parts.append(f'Peak PPS: {reputation["peak_pps"]:,}')
156+
157+
families = incidents.get('attack_families', {})
158+
if families:
159+
top_families = ', '.join(list(families.keys())[:5])
160+
summary_parts.append(f'Attack families: {top_families}')
161+
162+
severity = incidents.get('severity', {})
163+
sev_parts = []
164+
for level in ('critical', 'high', 'medium', 'low'):
165+
count = severity.get(level, 0)
166+
if count:
167+
sev_parts.append(f'{count} {level}')
168+
if sev_parts:
169+
summary_parts.append(f'Severity: {", ".join(sev_parts)}')
170+
171+
if threat_intel:
172+
sources = set()
173+
for ti in threat_intel:
174+
sources.add(ti.get('source', 'unknown'))
175+
summary_parts.append(f'Threat intel sources: {", ".join(sorted(sources))}')
176+
177+
if ioc_matches:
178+
summary_parts.append(f'IOC matches: {", ".join(list(ioc_matches.keys())[:5])}')
179+
180+
summary_text = '. '.join(summary_parts) + '.'
181+
182+
results.append({
183+
'types': ['text'],
184+
'categories': ['External analysis'],
185+
'values': [summary_text],
186+
'comment': f'Flowtriq DDoS intelligence for {ip}',
187+
})
188+
189+
# -- Reputation text attribute --
190+
if reputation:
191+
rep_lines = [f'Flowtriq IP Reputation for {ip}:']
192+
rep_lines.append(f' Risk Score: {risk_score}/100')
193+
rep_lines.append(f' Attack Count: {reputation.get("attack_count", 0)}')
194+
rep_lines.append(f' Networks Seen: {reputation.get("tenants_seen", 0)}')
195+
rep_lines.append(f' First Seen: {reputation.get("first_seen", "N/A")}')
196+
rep_lines.append(f' Last Seen: {reputation.get("last_seen", "N/A")}')
197+
rep_lines.append(f' Top Attack Family: {reputation.get("top_attack_family", "N/A")}')
198+
rep_lines.append(f' Top Protocol: {reputation.get("top_protocol", "N/A")}')
199+
rep_lines.append(f' Country: {reputation.get("country", "N/A")}')
200+
rep_lines.append(f' ASN: {reputation.get("asn", "N/A")}')
201+
rep_lines.append(f' Peak PPS: {reputation.get("peak_pps", 0):,}')
202+
tags = reputation.get('tags', [])
203+
if tags:
204+
rep_lines.append(f' Tags: {", ".join(tags)}')
205+
206+
results.append({
207+
'types': ['text'],
208+
'categories': ['External analysis'],
209+
'values': ['\n'.join(rep_lines)],
210+
'comment': 'Flowtriq reputation data',
211+
})
212+
213+
# -- ASN attribute --
214+
if reputation and reputation.get('asn'):
215+
results.append({
216+
'types': ['AS'],
217+
'categories': ['Network activity'],
218+
'values': [str(reputation['asn'])],
219+
'comment': f'ASN of {ip} per Flowtriq',
220+
})
221+
222+
# -- First/last seen timestamps --
223+
if reputation:
224+
if reputation.get('first_seen'):
225+
results.append({
226+
'types': ['datetime'],
227+
'categories': ['Network activity'],
228+
'values': [reputation['first_seen']],
229+
'comment': f'Flowtriq first seen {ip}',
230+
})
231+
if reputation.get('last_seen'):
232+
results.append({
233+
'types': ['datetime'],
234+
'categories': ['Network activity'],
235+
'values': [reputation['last_seen']],
236+
'comment': f'Flowtriq last seen {ip}',
237+
})
238+
239+
# -- Incident records as text --
240+
records = incidents.get('records', [])
241+
if records:
242+
inc_lines = [f'Flowtriq DDoS Incidents involving {ip} (last 90 days):']
243+
for i, rec in enumerate(records[:10], 1):
244+
inc_lines.append(f' [{i}] {rec.get("date", "?")} - {rec.get("attack_family", "?")} '
245+
f'({rec.get("severity", "?")}) - '
246+
f'Peak: {rec.get("peak_pps", 0):,} pps / '
247+
f'{rec.get("peak_bps", 0):,} bps - '
248+
f'Duration: {rec.get("duration_sec", 0)}s - '
249+
f'{rec.get("source_ip_count", 0)} source IPs')
250+
if rec.get('spoofing'):
251+
inc_lines.append(' Spoofing detected')
252+
if rec.get('botnet'):
253+
inc_lines.append(' Botnet indicators')
254+
255+
results.append({
256+
'types': ['text'],
257+
'categories': ['External analysis'],
258+
'values': ['\n'.join(inc_lines)],
259+
'comment': 'Flowtriq incident history',
260+
})
261+
262+
# -- Related attacker IPs --
263+
if related_ips:
264+
for related_ip, co_occurrence in list(related_ips.items())[:10]:
265+
results.append({
266+
'types': ['ip-src'],
267+
'categories': ['Network activity'],
268+
'values': [related_ip],
269+
'comment': f'Co-attacker with {ip} in {co_occurrence} Flowtriq incident(s)',
270+
'tags': ['flowtriq:related-attacker'],
271+
})
272+
273+
# -- Threat intel feed matches --
274+
if threat_intel:
275+
ti_lines = [f'Threat Intel Matches for {ip}:']
276+
for ti in threat_intel:
277+
ti_lines.append(
278+
f' - {ti.get("source", "?")} ({ti.get("threat_type", "?")}) '
279+
f'confidence={ti.get("confidence", "?")} '
280+
f'seen={ti.get("times_seen", "?")}x '
281+
f'[{ti.get("first_seen", "?")} to {ti.get("last_seen", "?")}]'
282+
)
283+
if ti.get('description'):
284+
ti_lines.append(f' {ti["description"]}')
285+
286+
results.append({
287+
'types': ['text'],
288+
'categories': ['External analysis'],
289+
'values': ['\n'.join(ti_lines)],
290+
'comment': 'Flowtriq threat intel feed matches',
291+
})
292+
293+
return results
294+
295+
296+
def introspection():
297+
return mispattributes
298+
299+
300+
def version():
301+
moduleinfo['config'] = moduleconfig
302+
return moduleinfo

0 commit comments

Comments
 (0)