Skip to content
Draft
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
15 changes: 14 additions & 1 deletion api/bazel/external_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ DEPENDENCY_ANNOTATIONS = [
"extensions",

# Additional dependencies loaded transitively via this dependency that are not tracked in
# Envoy (see the external dependency at the given version for information).
# Envoy (see the external dependency at the given version for information). Names listed
# here carry no metadata of their own; a transitive dependency that *is* tracked instead
# gets its own top-level entry with a used_by list, and must not appear in any
# implied_untracked_deps list.
"implied_untracked_deps",

# Project metadata.
Expand All @@ -35,6 +38,16 @@ DEPENDENCY_ANNOTATIONS = [
# This attribute is mandatory for each dependecy.
"use_category",

# For a dependency that is only reached transitively (it is not a direct
# dependency), the list of tracked dependencies through which it is loaded.
# Such entries are tracked in their own right — they carry their own
# metadata (cpe, use_category, extensions, ...) — as opposed to
# implied_untracked_deps, which declares transitives we carry no metadata
# for. A name must never appear both as a top-level entry and in any
# implied_untracked_deps list, and every name listed in used_by must be a
# top-level metadata key.
"used_by",

# The dependency version. This may be either a tagged release (preferred)
# or git SHA (as an exception when no release tagged version is suitable).
"version",
Expand Down
9 changes: 7 additions & 2 deletions bazel/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -964,16 +964,21 @@ json_data(
json_merge(
name = "legacy_all_repository_locations",
srcs = [":legacy_all_repository_locations_version_info"],
# Union of version-carrying deps and metadata-only entries: tracked
# transitive deps (annotated with `used_by`) have no entry in
# repository_locations.bzl but must still surface in the merged output so
# that their metadata (cpe, use_category, ...) reaches validation and CVE
# scanning.
filter = """
.[0] as $versions
| .[1] as $metadata1
| .[2] as $metadata2
| ($metadata1 * $metadata2) as $metadata
| $versions
| ($versions + $metadata)
| keys
| reduce .[] as $k ({};
. + {($k): (
($versions[$k] + ($metadata[$k] // {}))
((($versions[$k] // {}) + ($metadata[$k] // {})))
| .version as $ver
| if .license_url and $ver then
.license_url |= (
Expand Down
31 changes: 20 additions & 11 deletions tools/dependency/cve_utils.jq
Original file line number Diff line number Diff line change
Expand Up @@ -106,23 +106,32 @@ def get_severity(metrics):
##

def parse_deps(deps):
# A dep carrying a cpe but lacking a resolvable version cannot be
# meaningfully matched against version-ranged CVEs - a silent skip would
# make false negatives look identical to "clean", so it is a hard failure.
.
| deps
| with_entries(
select(.value.cpe != null and .value.cpe != "N/A")
| .key as $name
| .value
|= parse_cpe_tag(.cpe) as $cc
| {
release_date,
version: Version::parse(.version),
cpe: {
match: .cpe,
part: $cc.part,
vendor: $cc.vendor,
product: $cc.product,
version: $cc.version
|= (Version::parse(.version) as $version
| if $version == null then
error("Dependency '\($name)' declares a cpe but has no resolvable version, CVE matching is not possible")
else . end
| parse_cpe_tag(.cpe) as $cc
| {
release_date,
version: $version,
cpe: {
match: .cpe,
part: $cc.part,
vendor: $cc.vendor,
product: $cc.product,
version: $cc.version
}
}
}
)
);

def iterate_cves(cves; deps; ignored):
Expand Down
41 changes: 41 additions & 0 deletions tools/dependency/validate_reachability_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,39 @@ def check_apparent_name_uniqueness(metadata):
return errors


def check_tracked_implied_disjointness(metadata):
"""Verify tracked entries and implied_untracked_deps names are disjoint.

A dependency name is either tracked (a top-level metadata entry, possibly
reached only transitively and annotated with ``used_by``) or untracked
(listed in some entry's ``implied_untracked_deps``), never both. The
metadata must not be able to express the ambiguity.

Rules:
- A name in any ``implied_untracked_deps`` list MUST NOT be a top-level
metadata key.
- A name in any ``used_by`` list MUST be a top-level metadata key.

Returns a list of error strings (empty when the sets are disjoint and all
``used_by`` references resolve).
"""
errors = []
for key, meta in metadata.items():
for untracked in meta.get("implied_untracked_deps", []):
if untracked in metadata:
errors.append(
"%r is a tracked entry - remove it from %r's implied_untracked_deps"
% (untracked, key)
)
for parent in meta.get("used_by", []):
if parent not in metadata:
errors.append(
"%r lists %r in used_by but %r is not a top-level metadata key"
% (key, parent, parent)
)
return errors


# ---------------------------------------------------------------------------
# Validation logic (mirrors validate.py check-by-check)
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -435,6 +468,14 @@ def test_apparent_name_uniqueness(self):
"apparent_name uniqueness violations:\n" + "\n".join(errors)
)

def test_tracked_implied_disjointness(self):
errors = check_tracked_implied_disjointness(self.metadata)
if errors:
raise AssertionError(
"tracked/implied_untracked_deps disjointness violations:\n"
+ "\n".join(errors)
)

def test_dep_names_resolved(self):
validate_dep_names_resolved(
self.deps, self.metadata, self.apparent_lookup, self.revmap
Expand Down