vulnix integration

This commit is contained in:
Alex Tau 2025-05-09 15:27:22 +03:00
parent 758438382d
commit 436855d8c1
11 changed files with 172 additions and 2 deletions

View file

@ -65,6 +65,15 @@
buildInputs = old.buildInputs or [ ] ++ [ _prev.setuptools ]; buildInputs = old.buildInputs or [ ] ++ [ _prev.setuptools ];
} }
); );
lego-monitoring = _prev.lego-monitoring.overrideAttrs (
old: {
postPatch = ''
substituteInPlace src/lego_monitoring/core/const.py \
--replace-fail 'VULNIX_PATH: str = ...' 'VULNIX_PATH = "${lib.getExe pkgs.vulnix}"'
'';
}
);
}; };
# This example is only using x86_64-linux # This example is only using x86_64-linux

View file

@ -9,6 +9,7 @@ package:
let let
tempSensorOptions = (import ./submodules/tempSensorOptions.nix) { inherit lib; }; tempSensorOptions = (import ./submodules/tempSensorOptions.nix) { inherit lib; };
vulnixWhitelistRule = (import ./submodules/vulnixWhitelistRule.nix) { inherit lib; };
in in
{ {
options.services.lego-monitoring = { options.services.lego-monitoring = {
@ -19,6 +20,7 @@ in
"start" "start"
"stop" "stop"
"temp" "temp"
"vulnix"
]); ]);
default = [ ]; default = [ ];
description = "List of enabled check sets. Each check set is a module which checks something and generates alerts based on check results."; description = "List of enabled check sets. Each check set is a module which checks something and generates alerts based on check results.";
@ -63,12 +65,40 @@ in
''; '';
}; };
}; };
vulnix = {
whitelist = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule vulnixWhitelistRule);
default = { };
description = "Whitelist rules for vulnix. Attr name is package with version, package name, or `*`.";
example = lib.literalExpression ''{
"ffmpeg-3.4.2" = {
cve = [ "CVE-2018-6912" "CVE-2018-7557" ];
until = "2018-05-01";
issueUrl = "https://issues.example.com/29952";
};
}'';
};
};
}; };
}; };
config = let config = let
cfg = config.services.lego-monitoring; cfg = config.services.lego-monitoring;
json = pkgs.formats.json {}; json = pkgs.formats.json {};
toml = pkgs.formats.toml {};
# This monstrous incantation has the effect of converting the options to snake_case
# and removing those that are null (because TOML does not support null values)
vulnixWhitelistFile = toml.generate "vulnix-whitelist.toml" (lib.attrsets.filterAttrsRecursive (
k: v: v != null
) (
lib.mapAttrs (_: rule: {
inherit (rule) cve until;
issue_url = rule.issueUrl;
}) cfg.checks.vulnix.whitelist
));
serviceConfigFile = json.generate "config.json" { serviceConfigFile = json.generate "config.json" {
enabled_check_sets = cfg.enabledCheckSets; enabled_check_sets = cfg.enabledCheckSets;
telegram = with cfg.telegram; { telegram = with cfg.telegram; {
@ -88,6 +118,8 @@ in
}) sensorCfg.readings; }) sensorCfg.readings;
}) cfg.checks.temp.sensors; }) cfg.checks.temp.sensors;
vulnix.whitelist_path = vulnixWhitelistFile;
}; };
}; };
in lib.mkIf cfg.enable { in lib.mkIf cfg.enable {

View file

@ -0,0 +1,27 @@
{
lib,
}:
{
options = {
cve = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = ''
List of CVE identifiers to match. The whitelist rule is valid as long as the detected CVEs are a subset of the CVEs listed here.
If additional CVEs are detected, this whitelist rule is not effective anymore. If null, all CVEs are matched.'';
};
until = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Date in the form "YYYY-MM-DD" which confines this rule's lifetime. Null means forever.
On the specified date and later, this whitelist rule is not effective anymore.'';
};
issueUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "URL or list of URLs that point to any issue tracker. Informational only.";
};
};
}

View file

@ -56,6 +56,7 @@ async def async_main():
], ],
"stop": [], # this is checked later "stop": [], # this is checked later
"temp": [interval_checker(checks.temp_check, datetime.timedelta(minutes=5))], "temp": [interval_checker(checks.temp_check, datetime.timedelta(minutes=5))],
"vulnix": [interval_checker(checks.vulnix_check, datetime.timedelta(days=3))],
} }
checkers = [] checkers = []

View file

@ -5,10 +5,10 @@ class AlertType(StrEnum):
BOOT = "BOOT" BOOT = "BOOT"
TEMP = "TEMP" TEMP = "TEMP"
TEST = "TEST" TEST = "TEST"
# ERROR = "ERROR" VULN = "VULN"
ERROR = "ERROR"
# RAM = "RAM" # RAM = "RAM"
# CPU = "CPU" # CPU = "CPU"
# VULN = "VULN"
# LOGIN = "LOGIN" # LOGIN = "LOGIN"
# SMART = "SMART" # TODO # SMART = "SMART" # TODO
# RAID = "RAID" # RAID = "RAID"

View file

@ -1 +1,2 @@
from .temp import temp_check from .temp import temp_check
from .vulnix import vulnix_check

View file

@ -0,0 +1,48 @@
from lego_monitoring.alerting import alerts
from lego_monitoring.alerting.enum import AlertType, Severity
from .vulnix import get_vulnix_output
IS_TESTING = False
def vulnix_check() -> list[alerts.Alert]:
alert_list = []
try:
vulnix_output = get_vulnix_output(IS_TESTING)
except Exception as e:
alerts.send_alert(
alerts.Alert(
alert_type=AlertType.ERROR,
message=f"Exception {type(e).__name__} while calling vulnix: {e}",
severity=Severity.CRITICAL,
)
)
return []
for finding in vulnix_output:
if not IS_TESTING:
non_whitelisted_cves = [k for k in finding.description if k not in finding.whitelisted]
else:
non_whitelisted_cves = finding.description.keys()
if len(non_whitelisted_cves) == 0:
continue
message = f"New findings in derivation <code>{finding.derivation}</code>:"
for cve in non_whitelisted_cves:
if cve in finding.cvssv3_basescore:
score_str = f"(CVSSv3 = {finding.cvssv3_basescore[cve]})"
else:
score_str = "(not scored by CVSSv3)"
message += f'\n* <a href="https://nvd.nist.gov/vuln/detail/{cve}">{cve}</a> - {finding.description[cve]} {score_str}'
alert = alerts.Alert(
alert_type=AlertType.VULN,
message=message,
severity=Severity.WARNING,
)
alert_list.append(alert)
if IS_TESTING:
alert_list[0].message += "\n(just testing)"
return [alert_list[0]]
else:
return alert_list

View file

@ -0,0 +1,41 @@
import json
import logging
import subprocess
from dataclasses import dataclass
from lego_monitoring.core import cvars
from lego_monitoring.core.const import VULNIX_PATH
@dataclass
class VulnixPackageFindings:
pname: str
version: str
derivation: str
whitelisted: list[str]
cvssv3_basescore: dict[str, float]
description: dict[str, float]
def get_vulnix_output(is_testing=False) -> list[VulnixPackageFindings]:
whitelist_path = cvars.config.get().checks.vulnix.whitelist_path
cmd = [VULNIX_PATH, "--system", "-w", whitelist_path, "--json"]
if is_testing:
cmd.append("-s")
vulnix_run = subprocess.run(cmd, capture_output=True, check=False)
if vulnix_run.returncode not in range(0, 3):
logging.error(f"Vulnix returned error code {vulnix_run.returncode}, stderr: {vulnix_run.stderr}")
raise Exception(f"vulnix return error code {vulnix_run.returncode}, check logs")
vulnix_findings_json = json.loads(vulnix_run.stdout)
vulnix_findings = [
VulnixPackageFindings(
pname=f["pname"],
version=f["version"],
derivation=f["derivation"],
whitelisted=f["whitelisted"],
cvssv3_basescore=f["cvssv3_basescore"],
description=f["description"],
)
for f in vulnix_findings_json
]
return vulnix_findings

View file

@ -4,11 +4,13 @@ from dataclasses import dataclass
from alt_utils import NestedDeserializableDataclass from alt_utils import NestedDeserializableDataclass
from .checks.temp import TempCheckConfig from .checks.temp import TempCheckConfig
from .checks.vulnix import VulnixCheckConfig
@dataclass @dataclass
class ChecksConfig(NestedDeserializableDataclass): class ChecksConfig(NestedDeserializableDataclass):
temp: TempCheckConfig temp: TempCheckConfig
vulnix: VulnixCheckConfig
@dataclass @dataclass

View file

@ -0,0 +1,8 @@
from dataclasses import dataclass
from alt_utils import NestedDeserializableDataclass
@dataclass
class VulnixCheckConfig(NestedDeserializableDataclass):
whitelist_path: str

View file

@ -0,0 +1 @@
VULNIX_PATH: str = ... # path to vulnix executable