Skip to content

Commit dd37680

Browse files
committed
Search by sbom
Signed-off-by: Prabhu Subramanian <[email protected]>
1 parent ce6eb90 commit dd37680

File tree

4 files changed

+84
-22
lines changed

4 files changed

+84
-22
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,10 @@ vdb --search "npm:gitblame:0.0.1"
123123
vdb --search CVE-2024-25169
124124

125125
# Search by git url
126-
vdb --search "https://github.com/electron/electron
126+
vdb --search "https://github.com/electron/electron"
127+
128+
# Search by CycloneDX SBOM
129+
vdb --bom bom.json
127130
```
128131

129132
## License

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "appthreat-vulnerability-db"
3-
version = "6.0.0rc2"
3+
version = "6.0.0rc3"
44
description = "AppThreat's vulnerability database and package search library with a built-in sqlite based storage. OSV, CVE, GitHub, npm are the primary sources of vulnerabilities."
55
authors = [
66
{name = "Team AppThreat", email = "[email protected]"},

vdb/cli.py

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55
import logging
66
import os
77
import shutil
8+
import types
89

910
import orjson
1011
from rich.console import Console
11-
from rich.syntax import Syntax
12+
from rich.live import Live
13+
from rich.markdown import Markdown
1214
from rich.table import Table
1315

1416
from vdb.lib import config, db6 as db_lib, search
1517
from vdb.lib.aqua import AquaSource
18+
from vdb.lib.cve_model import CVE
1619
from vdb.lib.gha import GitHubSource
1720
from vdb.lib.osv import OSVSource
1821

@@ -85,27 +88,62 @@ def build_args():
8588
parser.add_argument(
8689
"--search",
8790
dest="search",
88-
help="Search for the package or CVE ID in the database. Use purl, cpe, or colon-separated values.",
91+
help="Search for the package or CVE ID in the database. Use purl, cpe, or git http url.",
92+
)
93+
parser.add_argument(
94+
"--bom",
95+
dest="bom_file",
96+
help="Search for packages in the CycloneDX BOM file.",
8997
)
9098
return parser.parse_args()
9199

92100

101+
def add_table_row(table: Table, res: dict, added_row_keys: dict):
102+
# matched_by is the purl or cpe string
103+
row_key = f"""{res["matched_by"]}|res.get("source_data_hash")"""
104+
# Filter duplicate rows from getting printed
105+
if added_row_keys.get(row_key):
106+
return
107+
source_data: CVE = res.get("source_data")
108+
description = ""
109+
if (
110+
source_data.root.containers.cna
111+
and source_data.root.containers.cna.descriptions
112+
and source_data.root.containers.cna.descriptions.root
113+
):
114+
description = (
115+
source_data.root.containers.cna.descriptions.root[0]
116+
.value.replace("\\n", "\n")
117+
.replace("\\t", " ")
118+
)
119+
table.add_row(
120+
res.get("cve_id"),
121+
res.get("matched_by"),
122+
Markdown(description, justify="left", hyperlinks=True),
123+
)
124+
added_row_keys[row_key] = True
125+
126+
93127
def print_results(results):
94-
table = Table(title="VDB Results")
128+
added_row_keys = {}
129+
table = Table(title="VDB Results", show_lines=True)
95130
table.add_column("CVE", justify="left")
96-
table.add_column("Type")
97-
table.add_column("Namespace")
98-
table.add_column("Name")
99-
table.add_column("Hash")
100-
table.add_column("Source Data")
101-
for res in results:
102-
table.add_row(res.get("cve_id"), res.get("type"),
103-
res.get("namespace", ""), res.get("name"),
104-
res.get("source_data_hash"),
105-
Syntax(orjson.dumps(
106-
res.get("source_data").model_dump(mode="json", exclude_none=True),
107-
option=orjson.OPT_INDENT_2 | orjson.OPT_APPEND_NEWLINE).decode("utf-8", errors="ignore"), "json", word_wrap=True))
108-
console.print(table)
131+
table.add_column("Locator")
132+
table.add_column("Description")
133+
if isinstance(results, types.GeneratorType):
134+
with Live(
135+
table, console=console, refresh_per_second=4, vertical_overflow="visible"
136+
):
137+
for result_gen in results:
138+
if isinstance(result_gen, dict):
139+
add_table_row(table, result_gen, added_row_keys)
140+
if isinstance(result_gen, types.GeneratorType):
141+
for res in result_gen:
142+
add_table_row(table, res, added_row_keys)
143+
elif isinstance(results, list):
144+
for res in results:
145+
add_table_row(table, res)
146+
console.print(table)
109147

110148

111149
def main():
@@ -133,12 +171,18 @@ def main():
133171
LOG.info("Refreshing %s", s.__class__.__name__)
134172
s.refresh()
135173
cve_data_count, cve_index_count = db_lib.stats()
136-
console.print("cve_data_count", cve_data_count, "cve_index_count", cve_index_count)
174+
console.print(
175+
"cve_data_count", cve_data_count, "cve_index_count", cve_index_count
176+
)
137177
db_lib.optimize_and_close_all()
138178
if args.search:
139179
if args.search.startswith("pkg:"):
140180
results = search.search_by_purl_like(args.search, with_data=True)
141-
elif args.search.startswith("CVE-") or args.search.startswith("GHSA-") or args.search.startswith("MAL-"):
181+
elif (
182+
args.search.startswith("CVE-")
183+
or args.search.startswith("GHSA-")
184+
or args.search.startswith("MAL-")
185+
):
142186
results = search.search_by_cve(args.search, with_data=True)
143187
elif args.search.startswith("http"):
144188
results = search.search_by_url(args.search, with_data=True)
@@ -148,6 +192,10 @@ def main():
148192
print_results(results)
149193
else:
150194
console.print("No results found!")
195+
elif args.bom_file:
196+
if os.path.exists(args.bom_file):
197+
results_generator = search.search_by_cdx_bom(args.bom_file, with_data=True)
198+
print_results(results_generator)
151199

152200

153201
if __name__ == "__main__":

vdb/lib/search.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Any, Generator
22

33
import orjson
44

@@ -27,7 +27,7 @@ def _filter_hits(raw_hits: list, compare_ver: str) -> list:
2727

2828
def get_cve_data(
2929
db_conn, index_hits: list[dict, Any], search_str: str
30-
) -> list[dict[str, str | CVE | None]]:
30+
) -> Generator | list[dict[str, str | CVE | None]]:
3131
"""Get CVE data for the index results
3232
3333
Args:
@@ -163,6 +163,17 @@ def search_by_url(url: str, with_data=False) -> list | None:
163163
return search_by_purl_like(purl_str, with_data)
164164

165165

166+
def search_by_cdx_bom(bom_file: str, with_data=False) -> Generator:
167+
"""Search by CycloneDX BOM file"""
168+
with open(bom_file, encoding="utf-8", mode="r") as fp:
169+
cdx_obj = orjson.loads(fp.read())
170+
for component in cdx_obj.get("components"):
171+
if component.get("purl"):
172+
yield search_by_purl_like(component.get("purl"), with_data)
173+
if component.get("cpe"):
174+
yield search_by_cpe_like(component.get("cpe"), with_data)
175+
176+
166177
def exec_query(conn, query: str, args: tuple[str, ...]) -> list:
167178
res = conn.execute(query, args)
168179
return res.fetchall()

0 commit comments

Comments
 (0)