Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
bin/__pycache__/
lib/__pycache__/
lib/cpeimport/__pycache__/
**/__pycache__
data/*.gz
data/*.tar
data/*.json
Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uv should be able to manage Python versions without requiring the repository to pin an exact version.

@qjerome qjerome Feb 11, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version pinning is admitted as a good practice as it is supposed to guarantee at least a working setup for the tools shipped by the project. If the version is not wanted, one is free to ignore it. Moreover Python version pinning is not used when using the package as dependency. Rather the package manager relies on the requires-python field in the pyproject.toml file. I can remove it, yet I don't really understand the motivation behind this removal.

@oh2fih oh2fih Feb 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file appears to follow a documented approach, as outlined in UV’s Python version files guide, so it doesn’t seem inherently problematic. My perspective may differ, as I tend to prioritize portability across environments rather than a fixed, containerized setup. This is more of a library-development mindset, whereas the cybersecurity community often focuses on a narrower, deployment-specific usage of these tools.

45 changes: 36 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
# syntax=docker/dockerfile:1

FROM python:3.8-slim-buster
FROM alpine:latest

COPY . /app

RUN <<EOF
apk update
apk add py3-pip
pip install pipx
pipx install --global uv
EOF

# Disable development dependencies
ENV UV_NO_DEV=1

WORKDIR /app

COPY REQUIREMENTS REQUIREMENTS
RUN pip3 install -r REQUIREMENTS
RUN uv sync --locked

# configuration
COPY <<EOF /app/config/settings.yaml
server:
port: 8000
valkey:
host: valkey
port: 6379
db: 8
cpe:
path: '/data/nvdcpe-2.0.tar'
source: 'https://nvd.nist.gov/feeds/json/cpe/2.0/nvdcpe-2.0.tar.gz'
EOF

# entrypoint script
COPY <<EOF entrypoint.sh
#!/bin/ash
set -e

COPY bin bin
COPY etc /etc
COPY lib lib
COPY docker/entrypoint.sh entrypoint.sh
uv run cpe-import

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs the import only once at startup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't already the case in the previous implementation ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussion moved to #20 (comment)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I am missing something here but AFAIK docker only runs the entrypoint, there is no systemd scheduling tasks.

uv run cpe-server
EOF

RUN mkdir /app/config
RUN chmod u+x entrypoint.sh

ENTRYPOINT ["/app/entrypoint.sh"]
ENTRYPOINT ["/app/entrypoint.sh"]
2 changes: 0 additions & 2 deletions docker/docker-compose.yml → docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
version: "3"
services:
server:
container_name: cpe-guesser
image: cpe-guesser:1.0
build: ..
volumes:
- ../data/:/data/:rw
- ./settings.yaml:/app/config/settings.yaml
ports:
- 8000:8000
depends_on:
Expand Down
5 changes: 0 additions & 5 deletions docker/entrypoint.sh

This file was deleted.

9 changes: 0 additions & 9 deletions docker/settings.yaml

This file was deleted.

10 changes: 0 additions & 10 deletions etc/logrotate.d/cpeguesser

This file was deleted.

11 changes: 0 additions & 11 deletions etc/systemd/system/cpeguesser.service

This file was deleted.

9 changes: 0 additions & 9 deletions etc/systemd/system/cpeguesser.timer

This file was deleted.

18 changes: 0 additions & 18 deletions etc/systemd/system/cpeweb.service

This file was deleted.

22 changes: 22 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[project]
name = "cpe-guesser"
version = "0.1.0"
description = "Add your description here"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be a placeholder. Was it copied from a template or generated automatically?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix description here: 6e605a9

readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"dynaconf>=3.2.12",
"falcon>=4.2.0",
"pydantic>=2.12.5",
"spectree>=2.0.1",
"valkey[libvalkey]>=6.1.1",
]

[build-system]
requires = ["uv_build>=0.9.26,<0.10.0"]
build-backend = "uv_build"

[project.scripts]
cpe-import = "cpe_guesser.bin.importer:main"
cpe-lookup = "cpe_guesser.bin.lookup:main"
cpe-server = "cpe_guesser.bin.server:main"
7 changes: 3 additions & 4 deletions lib/cpeguesser.py → src/cpe_guesser/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import List, Tuple

import valkey
from dynaconf import Dynaconf
Expand All @@ -20,7 +20,7 @@ def __init__(self):
decode_responses=True,
)

def guessCpe(self, words):
def guessCpe(self, words) -> List[Tuple[int, str]]:
k = []
for keyword in words:
k.append(f"w:{keyword.lower()}")
Expand All @@ -29,13 +29,12 @@ def guessCpe(self, words):
cpes = []
for x in reversed(range(maxinter)):
ret = self.rdb.sinter(k[x])
cpes.append(list(ret))
cpes.append(list(ret)) # ty:ignore[invalid-argument-type]
result = set(cpes[0]).intersection(*cpes)

ranked = []

for cpe in result:
rank = self.rdb.zrank("rank:cpe", cpe)
ranked.append((rank, cpe))

return sorted(ranked, reverse=True)
51 changes: 31 additions & 20 deletions bin/import.py → src/cpe_guesser/bin/importer.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
#!/usr/bin/env python3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prevents running the importer directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix there: f649298

# -*- coding: utf-8 -*-

import argparse
import sys
import os
import valkey
import sys
import urllib.error

import valkey
from dynaconf import Dynaconf
from valkey.client import Valkey

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from lib.cpeimport import CPEDownloader, NVDCPEHandler, XMLCPEHandler
from cpe_guesser.cpeimport import CPEDownloader, NVDCPEHandler, XMLCPEHandler

# Configuration
settings = Dynaconf(settings_files=["../config/settings.yaml"])
cpe_path = settings.get("cpe.path", "./data/nvdcpe-2.0.tar")
cpe_source = settings.get(
"cpe.source",
"https://nvd.nist.gov/feeds/json/cpe/2.0/nvdcpe-2.0.tar.gz",
)
valkey_host = settings.get("valkey.host", "127.0.0.1")
valkey_port = settings.get("valkey.port", 6379)
valkey_db = settings.get("valkey.db", 8)

rdb = valkey.Valkey(host=valkey_host, port=valkey_port, db=valkey_db)
def dbsize(rdb: Valkey) -> int:
dbsize = rdb.dbsize()
if isinstance(dbsize, int):
return dbsize
return 0


if __name__ == "__main__":
def main():
argparser = argparse.ArgumentParser(
description="Initializes the Redis database with CPE dictionary."
)
Expand All @@ -45,7 +38,20 @@
)
args = argparser.parse_args()

if not args.replace and rdb.dbsize() > 0:
# Configuration
settings = Dynaconf(settings_files=["../config/settings.yaml"])
cpe_path = settings.get("cpe.path", "./data/nvdcpe-2.0.tar")
cpe_source = settings.get(
"cpe.source",
"https://nvd.nist.gov/feeds/json/cpe/2.0/nvdcpe-2.0.tar.gz",
)
valkey_host = settings.get("valkey.host", "127.0.0.1")
valkey_port = settings.get("valkey.port", 6666)
valkey_db = settings.get("valkey.db", 8)

rdb = valkey.Valkey(host=valkey_host, port=valkey_port, db=valkey_db)

if not args.replace and dbsize(rdb) > 0:
print(f"Warning! The Redis database already has {rdb.dbsize()} keys.")
print("Use --replace if you want to flush the database and repopulate it.")
sys.exit(0)
Expand All @@ -60,13 +66,14 @@
FileNotFoundError,
PermissionError,
) as e:
print(f"Error: {e}")
sys.exit(1)

elif os.path.isfile(cpe_path):
print(f"Using existing file {cpe_path} ...")
cpe_file = cpe_path

if rdb.dbsize() > 0 and args.replace:
if rdb.dbsize() > 0 and args.replace: # ty:ignore[unsupported-operator]
print(f"Flushing {rdb.dbsize()} keys from the database...")
rdb.flushdb()

Expand All @@ -87,3 +94,7 @@
handler.parse_file(cpe_file, label=label)

print(f"Done! {rdb.dbsize()} keys inserted.")


if __name__ == "__main__":
main()
28 changes: 15 additions & 13 deletions bin/lookup.py → src/cpe_guesser/bin/lookup.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
#!/usr/bin/env python3

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prevents running the lookup directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix there: f649298

# -*- coding: utf-8 -*-

import argparse
import os
import sys
import json

runPath = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join(runPath, ".."))
from lib.cpeguesser import CPEGuesser
from cpe_guesser import CPEGuesser

if __name__ == "__main__":

def main():
parser = argparse.ArgumentParser(
description="Find potential CPE names from a list of keyword(s) and return a JSON of the results"
)
Expand All @@ -30,12 +26,18 @@
args = parser.parse_args()

cpeGuesser = CPEGuesser()
r = cpeGuesser.guessCpe(args.word)
cpes = cpeGuesser.guessCpe(args.word)

if not args.unique:
print(json.dumps(r))
print(json.dumps(cpes))
else:
try:
r = r[:1][0][1]
except:
r = []
r = []
if len(cpes) > 0:
if len(cpes[0]) >= 2:
cpes = cpes[0][1]

print(json.dumps(r))


if __name__ == "__main__":
main()
30 changes: 17 additions & 13 deletions bin/server.py → src/cpe_guesser/bin/server.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import json
import sys
import falcon
from wsgiref.simple_server import make_server
import json

import falcon
from dynaconf import Dynaconf

from cpe_guesser import CPEGuesser

# Configuration
settings = Dynaconf(settings_files=["../config/settings.yaml"])
port = settings.get("server.port", 8000)

runPath = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join(runPath, ".."))

from lib.cpeguesser import CPEGuesser


class Search:
def on_post(self, req, resp):
Expand Down Expand Up @@ -59,14 +56,17 @@ def on_post(self, req, resp):
return

cpeGuesser = CPEGuesser()
try:
r = cpeGuesser.guessCpe(q["query"])[:1][0][1]
except:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While broadly catching exceptions was not ideal, this change removes exception handling entirely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are already lots of changes in the pipe for that part of the code so I will not include direct fixes.

@rg-atte rg-atte Feb 11, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the intent was to remove the [:1][0][1] from the guessCpe result like in the lookup.py change but now it seems like it does both the length checks and cpeGuesser.guessCpe(q["query"])[:1][0][1] (now without try except)which to me seems like it would break the intended functionality entirely even if it didn't cause an exception. at least based on the diff and what the change in lookup.py did.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While broadly catching exceptions was not ideal, this change removes exception handling entirely, which may introduce new failure modes.

r = []
cpes = cpeGuesser.guessCpe(q["query"])[:1][0][1]

r = []
if len(cpes) > 0:
if len(cpes[0]) >= 2:
r = cpes[0][1]

resp.media = r


if __name__ == "__main__":
def main():
app = falcon.App()
app.add_route("/search", Search())
app.add_route("/unique", Unique())
Expand All @@ -80,3 +80,7 @@ def on_post(self, req, resp):
sys.exit(1)
except KeyboardInterrupt:
sys.exit(0)


if __name__ == "__main__":
main()
File renamed without changes.
Loading