Compare commits
2
Commits
3123263cf0
...
02cfb39486
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02cfb39486 | ||
|
|
d460364590 |
@@ -97,6 +97,37 @@ def _resolve_os(os_name: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def _os_family(os_name: str) -> Optional[str]:
|
||||
"""Map an asset OS string to a CPE target_sw token."""
|
||||
n = (os_name or "").lower()
|
||||
if "windows" in n:
|
||||
return "windows"
|
||||
if "ipad" in n:
|
||||
return "ipados"
|
||||
if "iphone" in n or "ios" in n:
|
||||
return "iphone_os"
|
||||
if "android" in n:
|
||||
return "android"
|
||||
if "mac" in n or "darwin" in n or "os x" in n:
|
||||
return "macos"
|
||||
if any(x in n for x in ("linux", "ubuntu", "debian", "centos", "red hat",
|
||||
"rhel", "fedora", "suse", "alma", "rocky")):
|
||||
return "linux"
|
||||
return None
|
||||
|
||||
|
||||
def _platform_ok(tsws, plat: Optional[str]) -> bool:
|
||||
"""A CVE's matching cpeMatch target_sw must be platform-neutral (*) or
|
||||
name the asset's OS. Stops desktop Firefox-on-Windows matching the
|
||||
Firefox-for-iOS CPE (target_sw=iphone_os) and similar cross-platform FPs.
|
||||
"""
|
||||
if not tsws:
|
||||
return True # unknown (e.g. pre-fix cache) → keep
|
||||
if any(t in ("*", "-", "") for t in tsws):
|
||||
return True
|
||||
return bool(plat) and plat in tsws
|
||||
|
||||
|
||||
def resolve_product(name: str) -> Optional[dict]:
|
||||
n = (name or "").strip().lower()
|
||||
if not n:
|
||||
@@ -207,7 +238,7 @@ def _query_nvd_cpe(cpe: str, version: str) -> List[dict]:
|
||||
url = f"{NVD_CVE_API}?virtualMatchString={cpe}:{version}&resultsPerPage=200"
|
||||
sleep = NVD_SLEEP_WITH_KEY if api_key else NVD_SLEEP_NO_KEY
|
||||
items = None
|
||||
for attempt in range(3):
|
||||
for attempt in range(4):
|
||||
try:
|
||||
with httpx.Client(timeout=HTTP_TIMEOUT) as c:
|
||||
r = c.get(url, headers=headers)
|
||||
@@ -224,7 +255,8 @@ def _query_nvd_cpe(cpe: str, version: str) -> List[dict]:
|
||||
items = [] # permanent (e.g. 404/400) → genuinely empty, cacheable
|
||||
break
|
||||
logger.debug("NVD %s for %s@%s (try %d)", r.status_code, cpe, version, attempt + 1)
|
||||
time.sleep(sleep * (attempt + 1))
|
||||
# NVD 2.0 throws 503 under load even with a key → back off generously.
|
||||
time.sleep(max(3.0, sleep) * (attempt + 1))
|
||||
time.sleep(sleep)
|
||||
if items is None:
|
||||
raise _TransientNVD(f"NVD unavailable for {cpe}@{version}")
|
||||
@@ -238,20 +270,25 @@ def _query_nvd_cpe(cpe: str, version: str) -> List[dict]:
|
||||
continue
|
||||
matched = False
|
||||
fixed = None
|
||||
tsws: set = set()
|
||||
for cfg in cve_obj.get("configurations", []) or []:
|
||||
for node in cfg.get("nodes", []) or []:
|
||||
for m in node.get("cpeMatch", []) or []:
|
||||
if prod_token not in (m.get("criteria") or ""):
|
||||
crit = m.get("criteria") or ""
|
||||
if prod_token not in crit:
|
||||
continue
|
||||
if not m.get("vulnerable", True):
|
||||
continue
|
||||
if _in_range(version, m):
|
||||
matched = True
|
||||
parts = crit.split(":")
|
||||
tsws.add(parts[9] if len(parts) > 9 else "*") # target_sw
|
||||
fixed = fixed or m.get("versionEndExcluding")
|
||||
if not matched:
|
||||
continue
|
||||
cvss, sev = _nvd_cvss(cve_obj)
|
||||
out.append({"cve": cve_id.upper(), "cvss": cvss, "severity": sev, "fixed": fixed})
|
||||
out.append({"cve": cve_id.upper(), "cvss": cvss, "severity": sev,
|
||||
"fixed": fixed, "tsw": sorted(tsws)})
|
||||
return out
|
||||
|
||||
|
||||
@@ -300,9 +337,12 @@ def _cache_put(db: Session, product_key: str, version: str, cves: List[dict]) ->
|
||||
db.commit()
|
||||
|
||||
|
||||
CACHE_PREFIX = "v2:" # bump → ignores pre-target_sw cache rows (stale FPs)
|
||||
|
||||
|
||||
def lookup_cves(db: Session, entry: dict, version: str) -> List[dict]:
|
||||
"""Cached (product, version) → CVE list."""
|
||||
key = entry["key"]
|
||||
key = CACHE_PREFIX + entry["key"]
|
||||
cached = _cache_get(db, key, version)
|
||||
if cached is not None:
|
||||
return cached
|
||||
@@ -389,7 +429,10 @@ def scan_asset_packages(db: Session, asset, packages: list, new_ids: Optional[li
|
||||
except Exception as e:
|
||||
logger.debug("app-cve lookup failed for %s %s: %s", name, version, e)
|
||||
continue
|
||||
plat = _os_family(asset.operating_system or "")
|
||||
for c in cves:
|
||||
if not _platform_ok(c.get("tsw"), plat):
|
||||
continue # CVE is for a different OS platform (e.g. Firefox-iOS)
|
||||
try:
|
||||
before = len(new_ids)
|
||||
_upsert(db, asset, name, version, c, new_ids)
|
||||
@@ -413,8 +456,11 @@ def scan_asset_os(db: Session, asset, new_ids: list) -> int:
|
||||
except Exception as ex:
|
||||
logger.debug("app-cve OS lookup failed for %s: %s", asset.hostname, ex)
|
||||
return 0
|
||||
plat = _os_family(asset.operating_system or "")
|
||||
count = 0
|
||||
for c in cves:
|
||||
if not _platform_ok(c.get("tsw"), plat):
|
||||
continue
|
||||
before = len(new_ids)
|
||||
try:
|
||||
_upsert(db, asset, e["label"], asset.os_version or cver, c, new_ids)
|
||||
@@ -430,6 +476,8 @@ def run_app_cve_scan(db: Session, asset_id: Optional[int] = None) -> dict:
|
||||
from app.models.asset import Asset, AssetSource
|
||||
stats = {"assets": 0, "findings": 0, "new": 0, "errors": []}
|
||||
new_ids: list = []
|
||||
logger.info("App CVE scan starting: NVD key %s",
|
||||
"present" if os.getenv("NVD_API_KEY", "").strip() else "MISSING (keyless = frequent 503)")
|
||||
|
||||
wazuh = None
|
||||
graph = None
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function AssetsPage() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [showInactive, setShowInactive] = useState(false);
|
||||
const [sourceFilter, setSourceFilter] = useState('');
|
||||
// Table sort state
|
||||
const [sortBy, setSortBy] = useState<string>('hostname');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
|
||||
@@ -56,6 +57,7 @@ export default function AssetsPage() {
|
||||
const params: any = {};
|
||||
if (searchText) params.search = searchText;
|
||||
if (showInactive) params.include_inactive = true;
|
||||
if (sourceFilter) params.source = sourceFilter;
|
||||
params.sort_by = sortBy;
|
||||
params.sort_order = sortOrder;
|
||||
params.limit = pageSize;
|
||||
@@ -114,14 +116,14 @@ export default function AssetsPage() {
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchText, showInactive, sortBy, sortOrder, pageSize]);
|
||||
}, [searchText, showInactive, sortBy, sortOrder, pageSize, sourceFilter]);
|
||||
|
||||
// Fetch on page / filter / search change (debounced for typing).
|
||||
useEffect(() => {
|
||||
const t = setTimeout(fetchAssets, searchText ? 300 : 0);
|
||||
return () => clearTimeout(t);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page, pageSize, sortBy, sortOrder, showInactive, searchText]);
|
||||
}, [page, pageSize, sortBy, sortOrder, showInactive, searchText, sourceFilter]);
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
|
||||
@@ -332,6 +334,18 @@ export default function AssetsPage() {
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<select
|
||||
value={sourceFilter}
|
||||
onChange={(e) => setSourceFilter(e.target.value)}
|
||||
title="Filter by the source that first registered the asset"
|
||||
className="rounded-sm border-0 py-1.5 pl-3 pr-8 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-vulncheck-blue sm:text-sm font-mono"
|
||||
>
|
||||
<option value="">All sources</option>
|
||||
<option value="WAZUH">Wazuh</option>
|
||||
<option value="NESSUS">Nessus</option>
|
||||
<option value="INTUNE">Intune / Defender</option>
|
||||
<option value="MANUAL">Manual</option>
|
||||
</select>
|
||||
<label className="flex items-center gap-1.5 text-xs font-mono text-gray-600 cursor-pointer whitespace-nowrap" title="INACTIVE assets are always shown (amber badge). Tick to also show operator-retired DECOMMISSIONED assets.">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
Reference in New Issue
Block a user