Skip to content
DSACMSPublic

About

Attempt at implementing CHAOSS metric libyears as a standalone python library

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Libyears

About the Project

Libyears is a local Python CLI that measures the age of direct Python, npm, and RubyGems dependencies using release timestamps from their public registries. It uses only the Python standard library at runtime.

Project website: https://dsacms.github.io/libyears

Agency Mission

CMS is working to modernize the nation's digital health ecosystem, including through public infrastructure, improved data sharing and interoperability, and collaboration that empowers people and improves care.

See the CMS Health Technology Ecosystem overview.

Team Mission

The function of the CMS Open Source Program Office is to:

Establish and maintain guidance, policies, practices, and talent pipelines that advance access, build trust, and amplify impact across CMS, HHS, and Federal Open Source Ecosystems by working and sharing openly.

Project Mission

Measure the age of direct dependencies using release timestamps from their public registries through a small local CLI.

Project Vision

Keep dependency freshness measurement simple, transparent, reproducible, and maintainable, with human-readable evidence rather than guessed versions or dates.

Core Team

See Project Members for project contacts and roles.

What libyears measures

For each measurable dependency, libyears compares the release timestamp of its exact used version with the timestamp of the latest stable release:

libyears = max(latest stable release time - used release time, 0) / 365.25 days

The report includes total, mean, median, and maximum libyears, plus measurement coverage. High libyears describes age; by itself, it does not mean a dependency is vulnerable or unsafe.

Important concept: version evidence

A dependency declaration says what a project requests, but it may not establish the exact version used. For example, neither of these identifies an exact version:

requests
urllib3>=2.6.3

Libyears does not silently resolve those declarations against today's registry: that would measure what might install now, not what the repository evidence records. An exact repository pin or a resolved snapshot can provide version evidence.

Basic usage

Scan a directory (or a requirements.txt file directly):

python3 -m libyears PATH
python3 -m libyears PATH --json

Exact-pinned requirements

Exact pins in requirements.txt can be measured directly:

requests==2.34.2
pandas==2.3.3

Resolved snapshot workflow

Create a snapshot and use it while scanning the repository:

python3 -m pip freeze > resolved.txt
python3 -m libyears PATH --resolved resolved.txt

The repository declarations still define direct dependency scope; the snapshot supplies exact version evidence for those declared dependencies. Snapshot-only transitive dependencies are ignored. Libyears does not invoke pip or install dependencies itself. A freshly generated snapshot describes that environment now, not historical repository state.

TLS and enterprise environments

Registry requests use HTTPS with certificate verification enabled. Libyears follows Python/OpenSSL defaults, including SSL_CERT_FILE and SSL_CERT_DIR. If the active interpreter has no usable default CA source, it can fall back to an existing pip-bundled CA file. An explicit bundle takes precedence:

python3 -m libyears PATH \
  --resolved resolved.txt \
  --ca-bundle /path/to/approved-ca-bundle.pem

Python's standard environment override is also supported:

SSL_CERT_FILE=/path/to/approved-ca-bundle.pem \
python3 -m libyears PATH --resolved resolved.txt

An explicit --ca-bundle is authoritative and validated before scanning. If no trusted CA source is available, requests fail with verification enabled.

Never use an unverified TLS context merely to make registry requests succeed.

Verbose diagnostics

python3 -m libyears PATH --verbose

Verbose output can identify certificate verification failures, HTTP failures, timeouts, DNS/network errors, malformed registry responses, and the selected TLS trust source. Stable JSON reason identifiers remain unchanged.

JSON output

JSON schema version 3 reports ecosystem-keyed version evidence, repository and per-ecosystem summaries, and source discovery errors separately from dependency-level failures. The schema v2 to v3 change is intentional. A compact Python-only example:

{
  "schema_version": 3,
  "metric": "libyears",
  "dependency_scope": "direct",
  "ecosystems": ["pypi"],
  "version_evidence": {
    "pypi": {
      "declaration_source": "requirements.txt",
      "snapshot_source": null
    }
  },
  "summary": {
    "dependencies_discovered": 1,
    "dependencies_measured": 1,
    "coverage": 1.0,
    "total_libyears": 0.999315537303217,
    "mean_libyears": 0.999315537303217,
    "median_libyears": 0.999315537303217,
    "max_libyears": 0.999315537303217,
    "by_ecosystem": {
      "pypi": {
        "dependencies_discovered": 1,
        "dependencies_measured": 1,
        "coverage": 1.0,
        "total_libyears": 0.999315537303217,
        "mean_libyears": 0.999315537303217,
        "median_libyears": 0.999315537303217,
        "max_libyears": 0.999315537303217
      }
    }
  },
  "dependencies": [
    {
      "ecosystem": "pypi",
      "name": "requests",
      "used_version": "2.34.2",
      "used_release_time": "2024-06-01T00:00:00Z",
      "latest_stable_version": "2.35.0",
      "latest_stable_release_time": "2025-06-01T00:00:00Z",
      "libyears": 0.999315537303217,
      "timestamp_anomaly": false,
      "version_source": "requirements_exact"
    }
  ],
  "unmeasurable": [],
  "source_errors": []
}

Measurement coverage

Coverage is the number measured divided by the number discovered. 7 discovered / 0 measured means no dependency age could be calculated; 7 discovered / 7 measured means the aggregate covers every discovered dependency. Their numeric totals cannot be interpreted as equivalent, even if both are low.

npm support

For npm repositories, package.json root dependencies define direct production scope, excluding names also declared in optionalDependencies. Root package-lock.json versions 2 and 3 or root Yarn Classic v1 yarn.lock provide exact used versions. Yarn evidence requires the canonical v1 header and an exact textual match between the manifest declaration and a lock selector; libyears does not evaluate ranges or execute Yarn/npm, install packages, or resolve dependencies. Development, peer, optional-only, workspace, and transitive dependencies are outside this feature. Yarn Berry/v2+, PnP, and workspace aggregation are not supported. --resolved remains Python-specific.

If both root lockfiles exist, neither is used, even if their versions agree: source_errors reports conflicting_resolution_sources. Malformed supported Yarn v1 uses invalid_lockfile; unsupported or safely unclassifiable Yarn uses unsupported_lockfile_format. These whole-source failures retain the known npm declaration count, but npm measured count and coverage are null. Mixed repository coverage is also null; successful Python measurements and their numeric aggregates survive. An invalid manifest makes npm counts and coverage unknown.

Missing, unsupported-source, conflicting, or ambiguous dependency evidence appears in unmeasurable, preserves the original declaration, and reduces ordinary measured/discovered coverage without blocking other dependencies. Human output identifies yarn.lock evidence and explains source failures. JSON remains schema v3: Yarn measurements use version_source: "yarn_lock", and version_evidence.npm includes format: "yarn_lock_v1" and snapshot_source: "yarn.lock". Rejected unsupported/unclassifiable formats have null format; simultaneous lockfiles have null format and snapshot source. Existing package-lock and PyPI evidence representations are unchanged.

RubyGems support

Without --ruby-group, a directory scan recognizes exactly the root Gemfile.lock, even without a Gemfile. All direct dependencies recorded in Gemfile.lock are counted; groups are not interpreted. The report describes the supplied lock snapshot. It does not certify committed bytes, current Gemfile contents, installed versions, or runtime compatibility. In this ordinary all-direct mode, Gemfile is never read. --resolved remains Python-specific; direct-file arguments do not activate Ruby discovery.

The supported bounded section format includes GEM, DEPENDENCIES, required nonempty PLATFORMS, and optional BUNDLED WITH / RUBY VERSION. GIT and PATH framing only isolates unsupported dependencies. Unknown fields/sections, including CHECKSUMS, CONTENT ADDRESSES and PLUGIN SOURCE, reject the source. This intentionally supports a subset of Bundler locks, not every generation. No Ruby, Bundler, gemspec, installer, or resolver is executed.

Only a literal https://rubygems.org remote (optional trailing slash) supplies public authority. Private, Git, PATH, multiple-remote and explicitly pinned sources remain unmeasurable; a public namesake is never substituted. Exact selected specs establish used versions; requirements are retained without satisfaction or equivalence checks. Missing specs for direct bundler or tzinfo-data remain counted. Agreeing distinct generic/platform artifacts establish one package version and retain provenance; differing versions or duplicate artifact rows are ambiguous. No host-platform selection occurs.

Ruby publication dates are the earliest admissible authoritative creation times across supported observable artifacts, independently of locked platforms. Missing/invalid dates and all 2009-07-25 UTC dates are excluded; build times are never a fallback. Latest requires currently available stable artifact evidence and chooses the greatest canonical date, with ascending exact version text for ties. These dates are observable evidence, not complete historical knowledge. Missing public records prove neither yanking nor never-existence. Disappearing records may change future measurements.

JSON stays schema v3. version_evidence.rubygems identifies Gemfile.lock, format: "gemfile_lock" (null on source rejection), scope: "all_direct_locked", and publication_basis: "earliest_admissible_supported_observable". Ruby rows use version_source: "gemfile_lock" and additive rubygems_evidence with selected artifacts and admissible used/latest publication contributors. Safely established evidence survives dependency failures; unavailable publication arrays are omitted. Existing Python/npm evidence objects are unchanged.

Whole-lock rejection uses source_errors, with null Ruby counts, coverage and numeric aggregates, and no invented rows. Known direct scope survives local or registry failures, including total outages, in unmeasurable. Other ecosystems' successes survive. Null coverage means unavailable; valid nonempty scope with no measurements has 0% coverage. Completed partial/empty/source-error scans exit 0.

Ruby metadata requests use verified HTTPS with the existing CA precedence, reject all redirects, and allow at most two requests per candidate, 16 MiB per response (32 MiB total), with a default 10-second timeout. One scan-local adapter paces request starts at least 0.11 seconds apart and respects 429 cooldowns without retries. Authorized candidates run in exact name/version lexical order. No archives or private source content are fetched. See the approved Feature 004 specification for the full boundary.

Select declared Ruby groups

Explicitly select groups with repeatable --ruby-group GROUP on a directory:

python3 -m libyears PATH --ruby-group default --ruby-group test
python3 -m libyears PATH --ruby-group default --ruby-group test --json

Names are exact and case-sensitive, using ASCII letters/underscores followed by ASCII letters/digits/underscores. Supply one name per occurrence, without a colon or comma list; abbreviations of --ruby-group are rejected. Repeated names deduplicate, and several groups select their union, counting each dependency once. default must be requested explicitly. production is an ordinary group name with no special behavior. Python/npm scope and Python-only --resolved remain unchanged. Without this option, ordinary all-direct Ruby behavior above is unchanged and Gemfile is not inspected.

Only the supplied directory's root Gemfile and Gemfile.lock participate; scoped direct-file arguments are usage errors. There is no parent/nested search, include, gemspec, configuration or environment-based group discovery. Escaping symlinks are rejected without reading their targets. Ruby and Bundler are never executed, and no packages are installed or resolved.

The conservative static subset accepts one top-level literal source, literal gem declarations with bounded requirement strings, literal group:/groups: options, require: values, and nonnested group :name do blocks (including literal optional flags). Ungrouped gems belong to default; explicit top-level groups replace default, and block/option memberships combine. Optionality and require: do not change declared membership. Complete-file recognition is required: unsupported or ambiguous syntax anywhere, including an unselected group, fails closed with no partial population. Conditions, interpolation, escapes, nested groups, duplicate declarations, platform/source blocks, path/git options and arbitrary Ruby are rejected. Rejection does not mean the file is invalid Ruby. See the approved closed subset for canonical syntax details.

For example, this synthetic supplied Gemfile selects only rack for default:

source "https://rubygems.org"
gem "rack", "~> 3.0"
group :test do
  gem "rake"
end
python3 -m libyears PATH --ruby-group default
python3 -m libyears PATH --ruby-group default --json

Human output includes these lines:

RubyGems selected declared groups: default.
Supplied declaration source: Gemfile; supplied version snapshot: Gemfile.lock.

If Gemfile.lock is absent, the selected rack is counted but unmeasurable with lockfile_missing: a declaration pin or range never supplies the exact version. Its JSON row includes this fragment (the unknown lock_requirement is omitted):

{
  "ecosystem": "rubygems",
  "name": "rack",
  "declared": "rack (~> 3.0)",
  "reason": "lockfile_missing",
  "gemfile_evidence": {
    "declared_groups": ["default"],
    "declaration_requirements": ["~> 3.0"]
  }
}

Gemfile establishes declaration membership; only independently authorized exact Gemfile.lock evidence establishes a measurable version. Association requires an exact direct name and identical supported requirement text. Textual differences produce declaration_lock_requirement_mismatch without version association or lookup: equivalence is not inferred. Transitive matches cannot substitute. Public authority must be established independently for both files; nonpublic sources never use public namesakes.

Schema v3 adds scoped scope: "selected_declared_groups", sorted unique requested_groups, and declaration source Gemfile under version_evidence.rubygems. Row gemfile_evidence keeps full declared memberships and ordered safe requirements separate from lock-version/publication artifacts. lock_requirement is null for known absence, omitted for missing or unsafe requirement evidence, and otherwise safe text. Raw source lines, private URLs and unsupported requirement text are not emitted. Ordinary scans omit these scoped additions; --verbose does not change JSON.

Unknown groups or rejected Gemfiles make the denominator unavailable. A rejected lock preserves the selected count but makes Ruby measurement/coverage unavailable, with a source error and no rows. A missing lock with nonempty known scope yields unmeasurable rows and 0% coverage. An established empty group with absent or accepted lock has 0 discovered/measured and null coverage. Successful sibling ecosystems survive. Completed evidence-error reports still exit 0; malformed requests/path errors exit 1 and systemic failures exit 2.

Results describe supplied-file declaration membership and lock evidence only. They establish neither historical coherence nor installed, deployed, runtime loading or compatibility state. Publication history remains incomplete as above. Scoped bounds are 1 MiB Gemfile, 16 KiB per physical line and 16 MiB lock; platforms without required safe file-access capabilities fail closed. The Feature 005 contract holds canonical failure/field rules; validation evidence separates offline correctness from pending authorized real-root validation.

Current limitations

  • Python declarations are read from requirements.txt; npm declarations are read from the root package.json; RubyGems scope comes from root Gemfile.lock DEPENDENCIES unless --ruby-group explicitly selects declared Gemfile groups within the closed static subset.
  • Direct dependencies only.
  • The resolved snapshot format is a narrow list of exact NAME==VERSION entries; repository declarations determine which entries are in scope.
  • Libyears does not install or resolve dependencies.
  • Transitive dependencies are not measured by default.
  • No other package ecosystems are supported.

Development

Local Development

See Getting Started for development setup.

Run the offline test suite with:

python3 -m unittest discover -s tests -v

See SPEC.md for the full behavioral specification and AGENTS.md for repository engineering rules.

Coding Style and Linters

See Coding Style and Linters for repository coding guidance and configured checks.

Contributing

See CONTRIBUTING.md for contribution instructions.

Codeowners

Repository ownership is recorded in .github/CODEOWNERS.

Community

See COMMUNITY.md for community principles and roles.

Community Guidelines

Read the Community Guidelines and Code of Conduct before participating.

Policies

Open Source Policy

We adhere to the CMS Open Source Policy. If you have any questions, just shoot us an email.

Security and Responsible Disclosure Policy

Submit a vulnerability: Vulnerability reports can be submitted through Bugcrowd. Reports may be submitted anonymously. If you share contact information, we will acknowledge receipt of your report within 3 business days. For more information about our Security, Vulnerability, and Responsible Disclosure Policies, see SECURITY.md.

Software Bill of Materials (SBOM)

A Software Bill of Materials (SBOM) is a formal record containing the details and supply chain relationships of various components used in building software. In the spirit of Executive Order 14028 - Improving the Nation's Cyber Security, a SBOM for this repository is provided here: https://github.com/DSACMS/libyears/network/dependencies. For more information and resources about SBOMs, visit: https://www.cisa.gov/sbom.

Public Domain

This project is in the public domain within the United States, and copyright and related rights in the work worldwide are waived through the CC0 1.0 Universal public domain dedication as indicated in LICENSE. All contributions to this project will be released under the CC0 dedication. By submitting a pull request or issue, you are agreeing to comply with this waiver of copyright interest.

About

Attempt at implementing CHAOSS metric libyears as a standalone python library

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages