Skip to content

Commit c5312e9

Browse files
authored
Rank OSV.dev findings by severity (#61)
1 parent bdd01a2 commit c5312e9

File tree

7 files changed

+256
-37
lines changed

7 files changed

+256
-37
lines changed

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
name = "scfw"
33
dynamic = ["version"]
44
dependencies = [
5+
"cvss",
56
"datadog-api-client",
67
"inquirer",
78
"packaging",
@@ -26,7 +27,9 @@ requires = ["setuptools"]
2627
build-backend = "setuptools.build_meta"
2728

2829
[tool.setuptools]
29-
packages = ["scfw", "scfw.commands", "scfw.configure", "scfw.loggers", "scfw.verifiers"]
30+
packages = [
31+
"scfw", "scfw.commands", "scfw.configure", "scfw.loggers", "scfw.verifiers", "scfw.verifiers.osv_verifier"
32+
]
3033

3134
[tool.setuptools.dynamic]
3235
version = {attr = "scfw.__version__"}

requirements.txt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ charset-normalizer==3.4.1 ; python_version >= "3.10" and python_version < "4" \
9797
--hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \
9898
--hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \
9999
--hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616
100+
cvss==3.4 ; python_version >= "3.10" and python_version < "4" \
101+
--hash=sha256:632353244ba3c58b53355466677edc968b9d7143c317b66271f9fd7939951ee8 \
102+
--hash=sha256:d9950613758e60820f7fac37ca5f35158712f8f2ea4f6629858a60c4984fe4ef
100103
datadog-api-client==2.33.1 ; python_version >= "3.10" and python_version < "4" \
101104
--hash=sha256:26e6b44fccff5d7bc9eaeaae1d3ce4fc4c0587400d869a53c81e4cc272dc63b4 \
102105
--hash=sha256:9b56521fa8ea7b743024759c624804cf0435dca110f111f31f08dd68d640737a
@@ -115,9 +118,9 @@ packaging==24.2 ; python_version >= "3.10" and python_version < "4" \
115118
python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4" \
116119
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
117120
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
118-
python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4" \
119-
--hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \
120-
--hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a
121+
python-dotenv==1.1.0 ; python_version >= "3.10" and python_version < "4" \
122+
--hash=sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5 \
123+
--hash=sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d
121124
readchar==4.2.1 ; python_version >= "3.10" and python_version < "4" \
122125
--hash=sha256:91ce3faf07688de14d800592951e5575e9c7a3213738ed01d394dcc949b79adb \
123126
--hash=sha256:a769305cd3994bb5fa2764aa4073452dc105a4ec39068ffe6efd3c20c60acc77
@@ -130,9 +133,9 @@ runs==1.2.2 ; python_version >= "3.10" and python_version < "4" \
130133
six==1.17.0 ; python_version >= "3.10" and python_version < "4" \
131134
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
132135
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
133-
typing-extensions==4.12.2 ; python_version >= "3.10" and python_version < "4" \
134-
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
135-
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
136+
typing-extensions==4.13.0 ; python_version >= "3.10" and python_version < "4" \
137+
--hash=sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b \
138+
--hash=sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5
136139
urllib3==2.3.0 ; python_version >= "3.10" and python_version < "4" \
137140
--hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \
138141
--hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d

scfw/logger.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from abc import (ABCMeta, abstractmethod)
77
from enum import Enum
8+
from typing_extensions import Self
89

910
from scfw.ecosystem import ECOSYSTEM
1011
from scfw.target import InstallTarget
@@ -15,9 +16,9 @@ class FirewallAction(Enum):
1516
The various actions the firewall may take in response to inspecting a
1617
package manager command.
1718
"""
18-
ALLOW = "ALLOW"
19-
ABORT = "ABORT"
20-
BLOCK = "BLOCK"
19+
ALLOW = 0
20+
ABORT = 1
21+
BLOCK = 2
2122

2223
def __lt__(self, other) -> bool:
2324
"""
@@ -38,15 +39,7 @@ def __lt__(self, other) -> bool:
3839
f"'<' not supported between instances of '{self.__class__}' and '{other.__class__}'"
3940
)
4041

41-
match self.name, other.name:
42-
case "ALLOW", "ABORT":
43-
return True
44-
case "ALLOW", "BLOCK":
45-
return True
46-
case "ABORT", "BLOCK":
47-
return True
48-
case _:
49-
return False
42+
return self.value < other.value
5043

5144
def __str__(self) -> str:
5245
"""
@@ -55,7 +48,26 @@ def __str__(self) -> str:
5548
Returns:
5649
A `str` representing the given `FirewallAction` suitable for printing.
5750
"""
58-
return self.value
51+
return self.name
52+
53+
@classmethod
54+
def from_string(cls, s: str) -> Self:
55+
"""
56+
Convert a string into a `FirewallAction`.
57+
58+
Args:
59+
s: The `str` to be converted.
60+
61+
Returns:
62+
The `FirewallAction` referred to by the given string.
63+
64+
Raises:
65+
ValueError: The given string does not refer to a valid `FirewallAction`.
66+
"""
67+
mappings = {f"{action}".lower(): action for action in cls}
68+
if (action := mappings.get(s.lower())):
69+
return action
70+
raise ValueError(f"Invalid firewall action '{s}'")
5971

6072

6173
class FirewallLogger(metaclass=ABCMeta):

scfw/loggers/dd_logger.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,13 @@ def __init__(self, logger: logging.Logger):
6666
logger: A configured log handle to which logs will be written.
6767
"""
6868
self._logger = logger
69+
self._level = _DD_LOG_LEVEL_DEFAULT
6970

7071
try:
71-
self._level = FirewallAction(os.getenv(DD_LOG_LEVEL_VAR))
72+
if (dd_log_level := os.getenv(DD_LOG_LEVEL_VAR)) is not None:
73+
self._level = FirewallAction.from_string(dd_log_level)
7274
except ValueError:
7375
_log.warning(f"Undefined or invalid Datadog log level: using default level {_DD_LOG_LEVEL_DEFAULT}")
74-
self._level = _DD_LOG_LEVEL_DEFAULT
7576

7677
def log(
7778
self,

scfw/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def _configure_logging(level: int):
4949
"""
5050
handler = logging.StreamHandler()
5151
handler.addFilter(logging.Filter(name="scfw"))
52-
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
52+
handler.setFormatter(logging.Formatter("[SCFW] %(levelname)s: %(message)s"))
5353

5454
log = logging.getLogger()
5555
log.addHandler(handler)
Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
and malicious open source software packages.
44
"""
55

6+
import functools
67
import logging
78

89
import requests
910

1011
from scfw.ecosystem import ECOSYSTEM
1112
from scfw.target import InstallTarget
1213
from scfw.verifier import FindingSeverity, InstallTargetVerifier
14+
from scfw.verifiers.osv_verifier.osv_advisory import OsvAdvisory
1315

1416
_log = logging.getLogger(__name__)
1517

@@ -53,16 +55,12 @@ def verify(self, target: InstallTarget) -> list[tuple[FindingSeverity, str]]:
5355
requests.HTTPError:
5456
An error occurred while querying an installation target against the OSV.dev API.
5557
"""
56-
def mal_finding(id: str) -> str:
58+
def finding(osv: OsvAdvisory) -> str:
59+
kind = "malicious package " if osv.id.startswith("MAL") else ""
60+
severity_tag = f"[{osv.severity}] " if osv.severity else ""
5761
return (
58-
f"An OSV.dev malicious package disclosure exists for package {target}:\n"
59-
f" * {_OSV_DEV_VULN_URL_PREFIX}/{id}"
60-
)
61-
62-
def non_mal_finding(id: str) -> str:
63-
return (
64-
f"An OSV.dev disclosure exists for package {target}:\n"
65-
f" * {_OSV_DEV_VULN_URL_PREFIX}/{id}"
62+
f"An OSV.dev {kind}disclosure exists for package {target}:\n"
63+
f" * {severity_tag}{_OSV_DEV_VULN_URL_PREFIX}/{osv.id}"
6664
)
6765

6866
def error_message(e: str) -> str:
@@ -102,19 +100,27 @@ def error_message(e: str) -> str:
102100
if not vulns:
103101
return []
104102

105-
osv_ids = set(filter(lambda id: id is not None, map(lambda vuln: vuln.get("id"), vulns)))
106-
mal_ids = set(filter(lambda id: id.startswith("MAL"), osv_ids))
107-
non_mal_ids = osv_ids - mal_ids
103+
osvs = set(map(OsvAdvisory.from_json, filter(lambda vuln: vuln.get("id"), vulns)))
104+
mal_osvs = set(filter(lambda osv: osv.id.startswith("MAL"), osvs))
105+
non_mal_osvs = osvs - mal_osvs
106+
107+
osv_sort_key = functools.cmp_to_key(OsvAdvisory.compare_severities)
108+
sorted_mal_osvs = sorted(mal_osvs, reverse=True, key=osv_sort_key)
109+
sorted_non_mal_osvs = sorted(non_mal_osvs, reverse=True, key=osv_sort_key)
108110

109111
return (
110-
[(FindingSeverity.CRITICAL, mal_finding(id)) for id in mal_ids]
111-
+ [(FindingSeverity.WARNING, non_mal_finding(id)) for id in non_mal_ids]
112+
[(FindingSeverity.CRITICAL, finding(osv)) for osv in sorted_mal_osvs]
113+
+ [(FindingSeverity.WARNING, finding(osv)) for osv in sorted_non_mal_osvs]
112114
)
113115

114116
except requests.exceptions.RequestException as e:
115117
_log.warning(f"Failed to query OSV.dev API: returning WARNING finding for target {target}")
116118
return [(FindingSeverity.WARNING, error_message(str(e)))]
117119

120+
except Exception as e:
121+
_log.warning(f"Target verification failed: returning WARNING finding for target {target}")
122+
return [(FindingSeverity.WARNING, error_message(str(e)))]
123+
118124

119125
def load_verifier() -> InstallTargetVerifier:
120126
"""

0 commit comments

Comments
 (0)