Skip to content
Closed
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
53 changes: 53 additions & 0 deletions common/manager/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
load("@rules_python//python:defs.bzl", "py_binary")
load(":crate_metadata.bzl", "selenium_manager_notice", "selenium_manager_sbom")

package(
default_visibility = [
"//dotnet/src/webdriver:__pkg__",
Expand All @@ -9,6 +12,56 @@ package(
],
)

py_binary(
name = "sbom_generator",
srcs = ["generate_sbom.py"],
main = "generate_sbom.py",
visibility = ["//visibility:private"],
)

py_binary(
name = "notice_generator",
srcs = ["generate_notice.py"],
main = "generate_notice.py",
visibility = ["//visibility:private"],
)
Comment on lines +15 to +27

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.

Remediation recommended

1. Sbom/notice generators untested πŸ“˜ Rule violation ☼ Reliability

New Python generators for selenium-manager.cdx.json and selenium-manager-THIRD-PARTY-NOTICES.txt
are introduced without accompanying small/unit tests, increasing regression risk for license/SBOM
artifact correctness. This conflicts with the requirement to cover new behavior with unit tests
where feasible.
Agent Prompt
## Issue description
The newly-added SBOM and NOTICE generator scripts include non-trivial parsing/formatting logic but no corresponding small/unit tests were added in this PR.

## Issue Context
These scripts generate compliance artifacts (CycloneDX SBOM + THIRD-PARTY-NOTICES). Without unit tests, changes to Cargo.lock parsing, manifest parsing, dependency resolution, and license-text deduplication can silently break artifact correctness.

## Fix Focus Areas
- common/manager/BUILD.bazel[15-27]
- common/manager/generate_sbom.py[34-167]
- common/manager/generate_notice.py[43-118]

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


# CycloneDX SBOM for the bundled Selenium Manager binary. Fully cargo-free: the
# component list and dependency graph come from rust/Cargo.lock, and licenses
# are harvested off the crate dependency graph via crate_manifests_aspect (the
# vendored Cargo.toml manifests crate_universe already fetched). Every binding
# copies this artifact next to the binary it ships.
selenium_manager_sbom(
name = "selenium-manager-sbom",
out = "selenium-manager.cdx.json",
crates = ["//rust:selenium-manager"],
lockfile = "//rust:cargo-lock",
visibility = [
"//dotnet/src/webdriver:__pkg__",
"//java/src/org/openqa/selenium/manager:__pkg__",
"//javascript/selenium-webdriver:__pkg__",
"//py:__pkg__",
"//rb:__pkg__",
],
)

# Third-party attribution NOTICE for the bundled Selenium Manager binary. Cargo-free
# companion to the SBOM: crate_manifests_aspect harvests each crate's LICENSE/NOTICE
# text off the dependency graph so the copyright notices that MIT/BSD/Apache require
# in binary distributions travel with the binary. Every binding copies it alongside.
selenium_manager_notice(
name = "selenium-manager-notice",
out = "selenium-manager-THIRD-PARTY-NOTICES.txt",
crates = ["//rust:selenium-manager"],
visibility = [
"//dotnet/src/webdriver:__pkg__",
"//java/src/org/openqa/selenium/manager:__pkg__",
"//javascript/selenium-webdriver:__pkg__",
"//py:__pkg__",
"//rb:__pkg__",
],
)

alias(
name = "selenium-manager-linux",
actual = select({
Expand Down
219 changes: 219 additions & 0 deletions common/manager/crate_metadata.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""Harvest vendored crate license metadata off the Rust dependency graph.

crate_universe has already fetched every crate's source, so each crate's
``Cargo.toml`` (its SPDX ``license``) and its ``LICENSE``/``NOTICE`` text files
are on disk and reachable as Bazel inputs via the crate rule's ``compile_data``.
This aspect walks the dependency graph and collects both, so the SBOM's licenses
and the third-party attribution NOTICE are a hermetic, cargo-free function of
what Bazel already downloaded.
"""

CrateMetadataInfo = provider(
doc = "Transitive vendored crate manifests and license-text files.",
fields = {
"manifests": "depset of root Cargo.toml File objects",
"license_files": "depset of root LICENSE/NOTICE text File objects",
},
)

# rust_binary/rust_library attributes that carry crate edges to follow.
_CRATE_EDGES = ["deps", "proc_macro_deps"]

# The binary links a different crate subset per OS/arch (e.g. winapi only on
# Windows). Seed the aspect under every shipped target platform and union the
# results so the SBOM/NOTICE cover all of them. Only the crates' vendored
# source files (Cargo.toml, LICENSE) are consumed, so nothing is cross-compiled.
_TARGET_PLATFORMS = [
"@rules_rs//rs/platforms:aarch64-apple-darwin",
"@rules_rs//rs/platforms:x86_64-apple-darwin",
"@rules_rs//rs/platforms:aarch64-unknown-linux-gnu",
"@rules_rs//rs/platforms:x86_64-unknown-linux-gnu",
"@rules_rs//rs/platforms:aarch64-pc-windows-msvc",
"@rules_rs//rs/platforms:x86_64-pc-windows-msvc",
]

def _platforms_split_impl(_settings, _attr):
return {platform: {"//command_line_option:platforms": platform} for platform in _TARGET_PLATFORMS}

_platforms_split = transition(
implementation = _platforms_split_impl,
inputs = [],
outputs = ["//command_line_option:platforms"],
)

# Root filenames (uppercased) that hold reproducible license/attribution text.
_LICENSE_PREFIXES = ["LICENSE", "LICENCE", "COPYING", "COPYRIGHT", "NOTICE", "UNLICENSE"]

def _is_repo_root(file):
"""True when the file sits at a vendored crate's repo root.

Filters out nested fixtures like ``crates__foo-1.0/tests/bar/LICENSE``.
"""
tail = file.path.rsplit("crates__", 1)
return len(tail) == 2 and tail[1].count("/") == 1

def _is_license_file(name):
upper = name.upper()
for prefix in _LICENSE_PREFIXES:
if upper.startswith(prefix):
return True
return False

def _collect(ctx):
manifests = []
license_files = []
for attr in ["compile_data", "data"]:
if not hasattr(ctx.rule.attr, attr):
continue
for dep in getattr(ctx.rule.attr, attr):
for file in dep.files.to_list():
if not _is_repo_root(file):
Comment on lines +82 to +87

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.

Action required

2. Aspect crashes on file labels 🐞 Bug ≑ Correctness

_collect() in crate_manifests_aspect unconditionally calls dep.files.to_list() for
compile_data/data entries, which can fail when those attributes contain direct file labels (not
targets with .files). This can break analysis/build of //common/manager:selenium-manager-sbom
and //common/manager:selenium-manager-notice because //rust:selenium_manager uses compile_data
with file labels.
Agent Prompt
## Issue description
The aspect helper `_collect(ctx)` assumes every element of `ctx.rule.attr.compile_data` / `ctx.rule.attr.data` has a `.files` field and calls `dep.files.to_list()`. When those attributes contain file labels (which can happen in this repo), the aspect can crash during analysis, preventing SBOM/NOTICE generation.

## Issue Context
In `//rust:selenium_manager`, `compile_data` is populated with direct file labels (e.g., markdown resources). The SBOM/NOTICE rules seed the aspect from `//rust:selenium-manager`, which depends on `//rust:selenium_manager`, so the aspect will evaluate `_collect()` on a rule with file labels in `compile_data`.

## Fix Focus Areas
- common/manager/crate_metadata.bzl[79-93]

### Suggested implementation direction
Update `_collect()` to handle both cases:
- If an attribute entry is a file-like object, treat it as a single file.
- If it is a target, iterate `target.files`.

For example (Starlark sketch):
```starlark
for dep in getattr(ctx.rule.attr, attr):
    files = []
    if hasattr(dep, "files"):
        files = dep.files.to_list()
    else:
        # file label
        files = [dep]
    for file in files:
        ...
```
Alternatively, prefer `ctx.rule.files.<attr>` when present, to directly obtain `File` objects for file labels, and separately handle target entries if needed.

β“˜ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

continue
if file.basename == "Cargo.toml":
manifests.append(file)
elif _is_license_file(file.basename):
license_files.append(file)
return manifests, license_files

def _aspect_impl(_target, ctx):
manifests, license_files = _collect(ctx)
manifest_deps = []
license_deps = []
for attr in _CRATE_EDGES:
if hasattr(ctx.rule.attr, attr):
for dep in getattr(ctx.rule.attr, attr):
if CrateMetadataInfo in dep:
manifest_deps.append(dep[CrateMetadataInfo].manifests)
license_deps.append(dep[CrateMetadataInfo].license_files)
return [CrateMetadataInfo(
manifests = depset(direct = manifests, transitive = manifest_deps),
license_files = depset(direct = license_files, transitive = license_deps),
)]

crate_manifests_aspect = aspect(
implementation = _aspect_impl,
attr_aspects = _CRATE_EDGES,
doc = "Collects transitive vendored crate Cargo.toml and license-text files.",
)

def _crate_targets(crates_attr):
"""Flatten the crates attr, whether a plain list or a split-transition dict.

A split transition makes a label_list surface as ``dict[str, list[Target]]``
(one entry per target platform); each platform contributes its own crate
closure, which we merge.
"""
if type(crates_attr) == "dict":
targets = []
for per_platform in crates_attr.values():
targets.extend(per_platform)
return targets
return crates_attr

def _transitive(crates_attr, field):
return depset(transitive = [
getattr(dep[CrateMetadataInfo], field)
for dep in _crate_targets(crates_attr)
if CrateMetadataInfo in dep
])

def _sbom_impl(ctx):
manifests = _transitive(ctx.attr.crates, "manifests")
out = ctx.actions.declare_file(ctx.attr.out or (ctx.label.name + ".cdx.json"))

args = ctx.actions.args()
args.add("--lockfile", ctx.file.lockfile)
args.add("--output", out)
args.add_all(manifests, before_each = "--manifest")
args.use_param_file("@%s", use_always = True)
args.set_param_file_format("multiline")

ctx.actions.run(
outputs = [out],
inputs = depset([ctx.file.lockfile], transitive = [manifests]),
executable = ctx.executable._generator,
arguments = [args],
mnemonic = "SeleniumManagerSbom",
progress_message = "Generating CycloneDX SBOM %{output}",
)
return [DefaultInfo(files = depset([out]))]

selenium_manager_sbom = rule(
implementation = _sbom_impl,
doc = "Generate a cargo-free CycloneDX SBOM for the Selenium Manager binary.",
attrs = {
"crates": attr.label_list(
aspects = [crate_manifests_aspect],
cfg = _platforms_split,
doc = "Seed target(s) whose transitive crate graph supplies licenses.",
),
"lockfile": attr.label(
allow_single_file = True,
mandatory = True,
doc = "Cargo.lock defining the component list and dependency graph.",
),
"out": attr.string(doc = "Output filename; defaults to <name>.cdx.json."),
"_generator": attr.label(
default = "//common/manager:sbom_generator",
executable = True,
cfg = "exec",
),
},
)

def _notice_impl(ctx):
manifests = _transitive(ctx.attr.crates, "manifests")
license_files = _transitive(ctx.attr.crates, "license_files")
out = ctx.actions.declare_file(ctx.attr.out or (ctx.label.name + ".txt"))

args = ctx.actions.args()
args.add("--output", out)
args.add_all(manifests, before_each = "--manifest")
args.add_all(license_files, before_each = "--license-file")
args.use_param_file("@%s", use_always = True)
args.set_param_file_format("multiline")

ctx.actions.run(
outputs = [out],
inputs = depset(transitive = [manifests, license_files]),
executable = ctx.executable._generator,
arguments = [args],
mnemonic = "SeleniumManagerNotice",
progress_message = "Generating third-party notices %{output}",
)
return [DefaultInfo(files = depset([out]))]

selenium_manager_notice = rule(
implementation = _notice_impl,
doc = "Generate a cargo-free third-party attribution NOTICE for Selenium Manager.",
attrs = {
"crates": attr.label_list(
aspects = [crate_manifests_aspect],
cfg = _platforms_split,
doc = "Seed target(s) whose transitive crate graph supplies attributions.",
),
"out": attr.string(doc = "Output filename; defaults to <name>.txt."),
"_generator": attr.label(
default = "//common/manager:notice_generator",
executable = True,
cfg = "exec",
),
},
)
Loading
Loading