Skip to content

Commit 4fa19fc

Browse files
chg: [publish] Improvements to the publish module for the notifications about new vulnerabilities on Mastodon.
1 parent 1093a1d commit 4fa19fc

File tree

9 files changed

+95
-60
lines changed

9 files changed

+95
-60
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Release 1.1.0 (2025-03-07)
4+
5+
Improvements to the publish module for the notifications about new vulnerabilities
6+
on Mastodon.
7+
8+
39
## Release 1.0.0 (2025-02-13)
410

511
This release introduces the capability to report errors, warnings,

fedivuln/conf_sample.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
# Templates used when publishing status
1717
templates = {
1818
"vulnerability": "You can now share your thoughts on vulnerability "
19-
"<VULNID> in Vulnerability-Lookup:\n<LINK>\n\n#VulnerabilityLookup #Vulnerability #Cybersecurity #bot",
19+
"<VULNID> in Vulnerability-Lookup:\n<LINK>\n\n<VENDOR> - <PRODUCT>\n\n#VulnerabilityLookup #Vulnerability #Cybersecurity #bot",
2020
"comment": "Vulnerability <VULNID> has received a comment on "
2121
"Vulnerability-Lookup:\n\n<TITLE>\n<LINK>\n\n#VulnerabilityLookup #Vulnerability #Cybersecurity #bot",
2222
"bundle": "A new bundle, <BUNDLETITLE>, has been published "

fedivuln/config.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#! /usr/bin/env python
22

3-
"""This module is responsible for loading the configuration variables.
4-
"""
3+
"""This module is responsible for loading the configuration variables."""
54

65
import importlib.util
76
import os

fedivuln/monitoring.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import time
2+
3+
import valkey
4+
5+
from fedivuln import config
6+
7+
if config.heartbeat_enabled:
8+
valkey_client = valkey.Valkey(config.valkey_host, config.valkey_port)
9+
10+
11+
def heartbeat(process_name) -> None:
12+
"""Sends a heartbeat in the Valkey datastore."""
13+
if not config.heartbeat_enabled:
14+
return
15+
try:
16+
valkey_client.set(process_name, time.time(), ex=config.expiration_period)
17+
except Exception as e:
18+
print(f"Heartbeat error: {e}")
19+
raise # Propagate the error to stop the process
20+
21+
22+
def log(level="warning", message="", key="process_logs_FediVuln") -> None:
23+
"""Reports an error or warning in the Valkey datastore."""
24+
timestamp = time.time()
25+
log_entry = {"timestamp": timestamp, "level": level, "message": message}
26+
try:
27+
# Add the log entry to a list, so multiple messages are preserved
28+
valkey_client.rpush(key, str(log_entry))
29+
valkey_client.expire(key, 86400) # Expire after 24 hours
30+
except Exception as e:
31+
print(f"Error reporting failure: {e}")
32+
raise

fedivuln/publish.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from mastodon import Mastodon
88

99
from fedivuln import config
10-
from fedivuln.utils import heartbeat, report_error
10+
from fedivuln.monitoring import heartbeat, log
11+
from fedivuln.utils import get_vendor_product_cve
1112

1213
# Set up your Mastodon instance with access credentials
1314
if config.mastodon_clientcred_push and config.mastodon_usercred_push:
@@ -30,24 +31,46 @@ def create_status_content(event_data: str, topic: str) -> str:
3031
status = config.templates.get(topic, "")
3132
match topic:
3233
case "vulnerability":
33-
try: # CVE
34+
# CVE
35+
try:
36+
if (
37+
event_dict["cveMetadata"]["datePublished"]
38+
!= event_dict["cveMetadata"]["dateUpdated"]
39+
):
40+
return ""
41+
vendor, product = get_vendor_product_cve(event_dict)[0]
3442
status = status.replace("<VULNID>", event_dict["cveMetadata"]["cveId"])
3543
status = status.replace(
3644
"<LINK>",
3745
f"https://vulnerability.circl.lu/vuln/{event_dict['cveMetadata']['cveId']}",
3846
)
47+
status = status.replace("<VENDOR>", vendor)
48+
status = status.replace("<PRODUCT>", product)
3949
return status
4050
except Exception:
4151
pass
42-
try: # GHSA, PySec
52+
53+
# GHSA, PySec
54+
try:
55+
if event_dict["published"] != event_dict["modified"]:
56+
return ""
4357
status = status.replace("<VULNID>", event_dict["id"])
4458
status = status.replace(
4559
"<LINK>", f"https://vulnerability.circl.lu/vuln/{event_dict['id']}"
4660
)
61+
status = status.replace("<VENDOR>", "")
62+
status = status.replace("<PRODUCT>", "")
4763
return status
4864
except Exception:
4965
pass
50-
try: # CSAF
66+
67+
# CSAF
68+
try:
69+
if (
70+
event_dict["document"]["tracking"]["initial_release_date"]
71+
!= event_dict["document"]["tracking"]["current_release_date"]
72+
):
73+
return ""
5174
try:
5275
vuln_id = event_dict["document"]["tracking"]["id"].replace(":", "_")
5376
except Exception:
@@ -56,6 +79,8 @@ def create_status_content(event_data: str, topic: str) -> str:
5679
status = status.replace(
5780
"<LINK>", f"https://vulnerability.circl.lu/vuln/{vuln_id}"
5881
)
82+
status = status.replace("<VENDOR>", "")
83+
status = status.replace("<PRODUCT>", "")
5984
return status
6085
except Exception:
6186
return ""
@@ -67,7 +92,7 @@ def create_status_content(event_data: str, topic: str) -> str:
6792
status = status.replace("<BUNDLETITLE>", event_dict["payload"]["name"])
6893
status = status.replace("<LINK>", event_dict["uri"])
6994
case _:
70-
pass
95+
status = ""
7196
return status
7297

7398

@@ -117,13 +142,13 @@ def listen_to_http_event_stream(url, headers=None, params=None, topic="comment")
117142

118143
except requests.exceptions.RequestException as req_err:
119144
print(f"Request error: {req_err}")
120-
report_error("error", f"Request error with HTTP event stream: {req_err}")
145+
log("error", f"Request error with HTTP event stream: {req_err}")
121146
except KeyboardInterrupt:
122147
print("\nStream interrupted by user. Closing connection.")
123-
report_error("error", "Stream interrupted by user. Closing connection.")
148+
log("error", "Stream interrupted by user. Closing connection.")
124149
except Exception as e:
125150
print(f"Unexpected error: {e}")
126-
report_error("error", f"Unexpected error in listen_to_http_event_stream: {e}")
151+
log("error", f"Unexpected error in listen_to_http_event_stream: {e}")
127152

128153

129154
def listen_to_valkey_stream(topic="comment"):

fedivuln/stream.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pyvulnerabilitylookup import PyVulnerabilityLookup
88

99
from fedivuln import config
10-
from fedivuln.utils import heartbeat, report_error
10+
from fedivuln.monitoring import heartbeat, log
1111

1212

1313
# Custom encoder for datetime
@@ -71,7 +71,7 @@ def on_direct_message(self, message):
7171
# Handle any errors in streaming
7272
def on_abort(self, err):
7373
print("Stream aborted with error:", err)
74-
report_error("error", f"Stream aborted with error: {err}")
74+
log("error", f"Stream aborted with error: {err}")
7575

7676
def handle_heartbeat(self):
7777
heartbeat(process_name="process_heartbeat_FediVuln")
@@ -105,7 +105,7 @@ def push_sighting_to_vulnerability_lookup(status_uri, vulnerability_ids):
105105
print(
106106
f"Error when sending POST request to the Vulnerability-Lookup server:\n{e}"
107107
)
108-
report_error(
108+
log(
109109
"error",
110110
f"Error when sending POST request to the Vulnerability-Lookup server: {e}",
111111
)

fedivuln/utils.py

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,5 @@
1-
import time
2-
3-
import valkey
4-
5-
from fedivuln import config
6-
7-
if config.heartbeat_enabled:
8-
valkey_client = valkey.Valkey(config.valkey_host, config.valkey_port)
9-
10-
11-
def heartbeat(process_name) -> None:
12-
"""Sends a heartbeat in the Valkey datastore."""
13-
if not config.heartbeat_enabled:
14-
return
15-
try:
16-
valkey_client.set(process_name, time.time(), ex=config.expiration_period)
17-
except Exception as e:
18-
print(f"Heartbeat error: {e}")
19-
raise # Propagate the error to stop the process
20-
21-
22-
def report_error(level="warning", message="", key="process_logs_FediVuln") -> None:
23-
"""Reports an error or warning in the Valkey datastore."""
24-
timestamp = time.time()
25-
log_entry = {"timestamp": timestamp, "level": level, "message": message}
26-
try:
27-
# Add the log entry to a list, so multiple messages are preserved
28-
valkey_client.rpush(key, str(log_entry))
29-
valkey_client.expire(key, 86400) # Expire after 24 hours
30-
except Exception as e:
31-
print(f"Error reporting failure: {e}")
32-
raise
1+
def get_vendor_product_cve(data):
2+
return [
3+
(entry["vendor"], entry["product"])
4+
for entry in data["containers"]["cna"]["affected"]
5+
]

poetry.lock

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
55

66
[project]
77
name = "FediVuln"
8-
version = "1.0.0"
8+
version = "1.1.0"
99
description = "A client to gather vulnerability-related information from the Fediverse."
1010
authors = [
1111
{name = "Cédric Bonhomme", email = "[email protected]"}
@@ -20,7 +20,6 @@ requires-python = ">=3.10,<4.0"
2020
dependencies = [
2121
"pyvulnerabilitylookup (>=2.2.0)",
2222
"mastodon-py (>=1.8.1)",
23-
"pyvulnerabilitylookup (>=2.1.0)",
2423
"valkey (>=6.0.2)"
2524
]
2625

@@ -50,6 +49,7 @@ classifiers = [
5049
"Programming Language :: Python :: 3.10",
5150
"Programming Language :: Python :: 3.11",
5251
"Programming Language :: Python :: 3.12",
52+
"Programming Language :: Python :: 3.13",
5353
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)"
5454
]
5555
include = [
@@ -85,7 +85,7 @@ warn_unreachable = true
8585
show_error_context = true
8686
pretty = true
8787

88-
exclude = "build|dist|docs|fedivuln.egg-info"
88+
exclude = "build|dist|docs"
8989

9090
[tool.isort]
9191
profile = "black"

0 commit comments

Comments
 (0)