diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 0000000..be7c0ad --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,5 @@ +FROM gcr.io/oss-fuzz-base/base-builder-rust + +COPY . $SRC/kingfisher +COPY .clusterfuzzlite/build.sh $SRC/build.sh +WORKDIR $SRC/kingfisher diff --git a/.clusterfuzzlite/build.sh b/.clusterfuzzlite/build.sh new file mode 100755 index 0000000..7539c1f --- /dev/null +++ b/.clusterfuzzlite/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash -eu + +# Install build dependencies required by vendored vectorscan (C/C++) +apt-get update -qq +apt-get install -y --no-install-recommends \ + cmake pkg-config libboost-dev patch ragel + +cd "$SRC/kingfisher" + +# Build all fuzz targets in release mode with debug assertions +cargo fuzz build -O --debug-assertions + +# Copy built fuzz binaries to the output directory +FUZZ_TARGET_OUTPUT_DIR=fuzz/target/x86_64-unknown-linux-gnu/release +for f in fuzz/fuzz_targets/*.rs; do + FUZZ_TARGET_NAME=$(basename "${f%.*}") + if [ -f "$FUZZ_TARGET_OUTPUT_DIR/$FUZZ_TARGET_NAME" ]; then + cp "$FUZZ_TARGET_OUTPUT_DIR/$FUZZ_TARGET_NAME" "$OUT/" + fi +done diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 0000000..22761ba --- /dev/null +++ b/.clusterfuzzlite/project.yaml @@ -0,0 +1 @@ +language: rust diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8a4f72b..0c6b041 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,3 +11,16 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 10 + ignore: + - dependency-name: "actions/checkout" + update-types: ["version-update:semver-major"] + - dependency-name: "actions/upload-artifact" + update-types: ["version-update:semver-major"] + - dependency-name: "actions/download-artifact" + update-types: ["version-update:semver-major"] + - dependency-name: "docker/login-action" + update-types: ["version-update:semver-major"] + - dependency-name: "docker/setup-buildx-action" + update-types: ["version-update:semver-major"] + - dependency-name: "docker/build-push-action" + update-types: ["version-update:semver-major"] diff --git a/.github/workflows/cflite_batch.yml b/.github/workflows/cflite_batch.yml new file mode 100644 index 0000000..ed366dc --- /dev/null +++ b/.github/workflows/cflite_batch.yml @@ -0,0 +1,33 @@ +name: ClusterFuzzLite batch fuzzing + +on: + schedule: + - cron: '0 3 * * 1' # Weekly on Monday at 03:00 UTC + +permissions: read-all + +jobs: + BatchFuzzing: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sanitizer: + - address + steps: + - name: Build Fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + language: rust + sanitizer: ${{ matrix.sanitizer }} + + - name: Run Fuzzers (${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 3600 + mode: 'batch' + sanitizer: ${{ matrix.sanitizer }} + output-sarif: true diff --git a/.github/workflows/cflite_pr.yml b/.github/workflows/cflite_pr.yml new file mode 100644 index 0000000..78b99e3 --- /dev/null +++ b/.github/workflows/cflite_pr.yml @@ -0,0 +1,38 @@ +name: ClusterFuzzLite PR fuzzing + +on: + pull_request: + branches: + - main + +permissions: read-all + +jobs: + PR: + runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ matrix.sanitizer }}-${{ github.ref }} + cancel-in-progress: true + strategy: + fail-fast: false + matrix: + sanitizer: + - address + steps: + - name: Build Fuzzers (${{ matrix.sanitizer }}) + id: build + uses: google/clusterfuzzlite/actions/build_fuzzers@v1 + with: + language: rust + github-token: ${{ secrets.GITHUB_TOKEN }} + sanitizer: ${{ matrix.sanitizer }} + + - name: Run Fuzzers (${{ matrix.sanitizer }}) + id: run + uses: google/clusterfuzzlite/actions/run_fuzzers@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fuzz-seconds: 300 + mode: 'code-change' + sanitizer: ${{ matrix.sanitizer }} + output-sarif: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5d843d..2213837 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: with: toolchain: ${{ env.RUST_TOOLCHAIN }} - - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: kingfisher-${{ runner.os }}-${{ runner.arch }} cache-on-failure: true @@ -72,7 +72,7 @@ jobs: with: toolchain: ${{ env.RUST_TOOLCHAIN }} - - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: kingfisher-${{ runner.os }}-${{ runner.arch }} cache-on-failure: true @@ -96,7 +96,7 @@ jobs: - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # master with: toolchain: ${{ env.RUST_TOOLCHAIN }} - - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: kingfisher-${{ runner.os }}-${{ runner.arch }} cache-on-failure: true @@ -132,7 +132,7 @@ jobs: toolchain: ${{ env.RUST_TOOLCHAIN }} - name: Set up MSYS2 - uses: msys2/setup-msys2@61f9e5e925871ba6c9e3e8da24ede83ea27fa91f # v2.27.0 + uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2.30.0 with: msystem: ${{ matrix.msystem }} update: true @@ -140,7 +140,7 @@ jobs: make git - - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: kingfisher-${{ runner.os }}-${{ runner.arch }} cache-on-failure: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3788ada..24cb224 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,7 +45,7 @@ jobs: with: toolchain: ${{ env.RUST_TOOLCHAIN }} - - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: kingfisher-${{ runner.os }}-${{ runner.arch }} cache-on-failure: true @@ -120,7 +120,7 @@ jobs: with: toolchain: ${{ env.RUST_TOOLCHAIN }} - - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: kingfisher-${{ runner.os }}-${{ runner.arch }} cache-on-failure: true @@ -185,7 +185,7 @@ jobs: with: toolchain: ${{ env.RUST_TOOLCHAIN }} - - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: kingfisher-${{ runner.os }}-${{ runner.arch }} cache-on-failure: true @@ -224,7 +224,7 @@ jobs: with: toolchain: ${{ env.RUST_TOOLCHAIN }} - - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: kingfisher-${{ runner.os }}-${{ runner.arch }} cache-on-failure: true @@ -274,7 +274,7 @@ jobs: toolchain: ${{ env.RUST_TOOLCHAIN }} - name: Set up MSYS2 - uses: msys2/setup-msys2@61f9e5e925871ba6c9e3e8da24ede83ea27fa91f # v2.27.0 + uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2.30.0 with: msystem: ${{ matrix.msystem }} update: true @@ -282,7 +282,7 @@ jobs: make git - - uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8 + - uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: shared-key: kingfisher-${{ runner.os }}-${{ runner.arch }} cache-on-failure: true @@ -390,7 +390,7 @@ jobs: # ── create the release using just that snippet ───────────────────── - name: Create release & upload assets - uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0 + uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 with: tag: ${{ steps.version.outputs.tag }} name: "Kingfisher ${{ steps.version.outputs.tag }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index e2dfae5..5d33cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## [v1.91.0] +- Remediated current RustSec vulnerability findings by upgrading core dependencies including `gix`, `mysql_async`, `axum`, `indicatif`, `quick-xml`, and `console`. +- Added `make audit-deps` to run `cargo audit` locally and report vulnerable dependencies. +- Refreshed pinned GitHub Actions for `swatinem/rust-cache`, `msys2/setup-msys2`, and `ncipollo/release-action`, and configured Dependabot to ignore selected GitHub Action major-version bumps. +- OpenSSF Scorecard hardening: added `SECURITY.md`, `.github/dependabot.yml`, pinned all GitHub Actions by SHA, fixed dangerous workflow expression injection patterns, added top-level `permissions: {}` to `pypi.yml`, and added SLSA provenance generation for releases. +- Added ClusterFuzzLite integration with four fuzz targets (entropy, location mapping, base64 decoding, span deduplication) and a `make fuzz` target for local fuzzing. + ## [v1.90.0] - Added `--max-validation-response-length ` for `scan` to control validation response storage truncation (default: `2048`, `0` disables truncation). - Updated `--full-validation-response` to bypass both validation storage truncation and reporter truncation, preserving complete response bodies end-to-end for parsing/reporting workflows. diff --git a/Cargo.toml b/Cargo.toml index 1d8e8fc..a8a5755 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ http = "1.4" [package] name = "kingfisher" -version = "1.90.0" +version = "1.91.0" description = "MongoDB's blazingly fast and accurate secret scanning and validation tool" edition.workspace = true rust-version.workspace = true @@ -94,7 +94,7 @@ clap = { version = "4.5", features = [ anyhow = "1.0" bstr = { version = "1.12", features = ["serde"] } fixedbitset = "0.5" -gix = { version = "0.73", features = ["max-performance-safe", "serde", "blocking-network-client"] } +gix = { version = "0.80", features = ["max-performance-safe", "serde", "blocking-network-client"] } ignore = "0.4" petgraph = "0.8" roaring = "0.10" @@ -106,7 +106,7 @@ smallvec = { version = "1", features = [ "union", ] } tracing = "0.1.43" -indicatif = { version = "0.17", features = ["improved_unicode"] } +indicatif = { version = "0.18", features = ["improved_unicode"] } rayon = "1.11" hex = "0.4.3" vectorscan-rs = "0.0.5" @@ -130,7 +130,7 @@ reqwest = { version = "0.12", default-features = false, features = [ "blocking", "multipart", ] } -axum = { version = "0.7", default-features = false, features = ["tokio", "http1"] } +axum = { version = "0.8", default-features = false, features = ["tokio", "http1"] } chrono = "0.4.42" @@ -140,7 +140,7 @@ base64 = "0.22.1" crossbeam-channel = "0.5.15" indenter = "0.3.4" serde-sarif = "0.4" -console = "0.15.11" +console = "0.16.3" time = "0.3.44" tempfile = "3.23.0" num_cpus = "1.17.0" @@ -154,7 +154,7 @@ base32 = "0.5.1" crossbeam-skiplist = "0.1.3" tokio-postgres = { version = "0.7", default-features = false, features = ["runtime"] } mongodb = { version = "3.4", default-features = false, features = ["rustls-tls", "aws-auth", "compat-3-0-0", "dns-resolver"] } -mysql_async = { version = "0.34.2", default-features = false, features = ["default-rustls"] } +mysql_async = { version = "0.36.1", default-features = false, features = ["default-rustls"] } bson = "2.15.0" ring = "0.17.14" pem = "3.0.6" @@ -193,7 +193,6 @@ tree-sitter-regex = "0.25.0" tree_magic_mini = "3.2" content_inspector = "0.2.4" rustc-hash = "2.1.1" -term_size = "0.3.2" bzip2-rs = "0.1.2" zip = { version = "2.4.2", default-features = false, features = ["deflate", "deflate64", "time"] } tar = "0.4.44" @@ -212,7 +211,7 @@ sha2 = "0.10.9" strum_macros = "0.27.2" humantime = "2.3.0" path-dedot = "3.1.1" -quick-xml = {version = "0.38.4", features = ["serde","serialize"] } +quick-xml = { version = "0.39.2", features = ["serde", "serialize"] } rustls = "0.23.35" tokio-postgres-rustls = "0.13.0" rustls-native-certs = "0.8.2" @@ -227,7 +226,6 @@ bloomfilter = "3.0.1" uuid = "1.19.0" rand = "0.9.2" percent-encoding = "2.3.2" -atty = "0.2.14" self_update = { version = "0.42.0", default-features = false, features = ["rustls", "archive-tar", "archive-zip", "compression-flate2"] } semver = "1.0.27" globset = "0.4.18" diff --git a/Makefile b/Makefile index 39e8a5f..9d5e1d0 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ ARCHIVE_CMD = $(TAR_CMD) $(TAR_OPTS) SUDO_CMD := $(shell command -v sudo 2>/dev/null) .PHONY: default help create-dockerignore ubuntu-x64 ubuntu-arm64 linux-x64 linux-arm64 darwin-arm64 darwin-x64 windows-x64 windows-arm64 windows \ - linux darwin all list-archives check-docker check-rust clean tests + linux darwin all list-archives check-docker check-rust clean tests audit-deps fuzz default: help @@ -71,6 +71,8 @@ help: @echo " all" @echo " list-archives" @echo " tests" + @echo " audit-deps Run cargo-audit to report vulnerable dependencies" + @echo " fuzz Run fuzz targets (FUZZ_SECONDS=N to control duration, default 60s)" create-dockerignore: @echo "target/" > .dockerignore @@ -691,6 +693,44 @@ tests: cargo test --workspace --all-targets; \ fi +audit-deps: + @echo "🔍 checking for cargo-audit …" + @if command -v cargo-audit >/dev/null 2>&1; then \ + echo "✅ cargo-audit already present"; \ + else \ + echo "📦 installing cargo-audit …"; \ + cargo install --locked cargo-audit; \ + fi + @echo "▶ auditing dependency vulnerabilities …" + @cargo audit + +fuzz: + @echo "🐛 Running fuzz targets (cargo-fuzz required, nightly Rust required)…" + @command -v cargo-fuzz >/dev/null 2>&1 || { \ + echo "📦 installing cargo-fuzz …"; \ + cargo install cargo-fuzz; \ + } + @rustup toolchain list | grep -q nightly || { \ + echo "📦 installing nightly toolchain …"; \ + rustup toolchain install nightly; \ + } + @fuzz_seconds=$${FUZZ_SECONDS:-60}; \ + NIGHTLY_PATH="$$HOME/.rustup/toolchains/nightly-$$(rustc -vV | awk '/^host:/{print $$2}')/bin"; \ + if [ ! -d "$$NIGHTLY_PATH" ]; then \ + echo "❌ Nightly toolchain not found at $$NIGHTLY_PATH"; \ + exit 1; \ + fi; \ + export PATH="$$NIGHTLY_PATH:$$PATH"; \ + echo "Using rustc: $$(which rustc) ($$(rustc --version))"; \ + for target in fuzz_entropy fuzz_location fuzz_base64 fuzz_span; do \ + echo "▶ fuzzing $$target for $${fuzz_seconds}s …"; \ + cargo fuzz run $$target -- \ + -max_total_time=$${fuzz_seconds} \ + -max_len=4096 || { echo "❌ $$target found a crash"; exit 1; }; \ + echo "✅ $$target passed"; \ + done + @echo "🎉 All fuzz targets passed" + clean: @echo "Cleaning build artifacts..." cargo clean diff --git a/crates/kingfisher-core/Cargo.toml b/crates/kingfisher-core/Cargo.toml index 28f49f6..88c6ee3 100644 --- a/crates/kingfisher-core/Cargo.toml +++ b/crates/kingfisher-core/Cargo.toml @@ -39,10 +39,10 @@ bstr.workspace = true memchr = "2.7" # Git types (minimal, for ObjectId and Time) -gix = { version = "0.73", default-features = false, features = ["serde"] } +gix = { version = "0.80", default-features = false, features = ["serde"] } # Console formatting -console = "0.15" +console = "0.16" # Language detection for content types tokei = "14.0.0" diff --git a/crates/kingfisher-scanner/Cargo.toml b/crates/kingfisher-scanner/Cargo.toml index 488a23b..c53cd48 100644 --- a/crates/kingfisher-scanner/Cargo.toml +++ b/crates/kingfisher-scanner/Cargo.toml @@ -158,7 +158,7 @@ reqwest = { version = "0.12", default-features = false, features = [ tokio = { version = "1.48", features = ["net", "time", "sync"], optional = true } liquid = { version = "0.26", optional = true } liquid-core = { version = "0.26", optional = true } -quick-xml = { version = "0.38", features = ["serde", "serialize"], optional = true } +quick-xml = { version = "0.39", features = ["serde", "serialize"], optional = true } sha1 = { workspace = true, optional = true } chrono = { version = "0.4.42", optional = true } hmac = { workspace = true, optional = true } @@ -174,7 +174,7 @@ hex = { workspace = true, optional = true } url = { version = "2.5.7", optional = true } bson = { version = "2.15.0", optional = true } mongodb = { version = "3.4", default-features = false, features = ["rustls-tls", "aws-auth", "compat-3-0-0", "dns-resolver"], optional = true } -mysql_async = { version = "0.34.2", default-features = false, features = ["default-rustls"], optional = true } +mysql_async = { version = "0.36.1", default-features = false, features = ["default-rustls"], optional = true } tokio-postgres = { version = "0.7", default-features = false, features = ["runtime"], optional = true } tokio-postgres-rustls = { version = "0.13.0", optional = true } rustls = { version = "0.23.35", optional = true } diff --git a/src/git_repo_enumerator.rs b/src/git_repo_enumerator.rs index d03f752..6c3df67 100644 --- a/src/git_repo_enumerator.rs +++ b/src/git_repo_enumerator.rs @@ -28,11 +28,8 @@ pub const MIN_SCANNABLE_BLOB_SIZE: u64 = 20; // Convert " " -- Time; fallback to the Unix-epoch on parse error #[inline] -fn parse_sig_time>(raw: T) -> Time { - match std::str::from_utf8(raw.as_ref()) { - Ok(s) => parse_time(s, None).unwrap_or_else(|_| Time::new(0, 0)), - Err(_) => Time::new(0, 0), - } +fn parse_sig_time(raw: &str) -> Time { + parse_time(raw, None).unwrap_or_else(|_| Time::new(0, 0)) } /// How blobs are provided to the scanning pipeline. @@ -157,11 +154,22 @@ impl<'a> GitRepoWithMetadataEnumerator<'a> { continue; } }; - let committer = &commit.committer; + let committer = match commit.committer() { + Ok(committer) => committer, + Err(err) => { + debug!( + "Failed to decode committer metadata for {}: {err}", + e.commit_oid + ); + continue; + } + }; let parsed = Arc::new(CommitMetadata { commit_id: e.commit_oid, - committer_name: String::from_utf8_lossy(&committer.name).into_owned(), - committer_email: String::from_utf8_lossy(&committer.email).into_owned(), + committer_name: String::from_utf8_lossy(committer.name.as_ref()) + .into_owned(), + committer_email: String::from_utf8_lossy(committer.email.as_ref()) + .into_owned(), committer_timestamp: parse_sig_time(committer.time), }); commit_metadata.insert(e.commit_oid, Arc::clone(&parsed)); diff --git a/src/main.rs b/src/main.rs index 760e962..769d8fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ use std::{ }; use anyhow::{Context, Result}; +use console::Term; use kingfisher::{ access_map, azure, bitbucket, cli::{ @@ -61,7 +62,6 @@ use kingfisher::{ }; use serde_json::json; use tempfile::TempDir; -use term_size; use tokio::runtime::Builder; use tracing::{error, info, warn}; use tracing_core::metadata::LevelFilter; @@ -268,7 +268,7 @@ async fn async_main(args: CommandLineArgs) -> Result<()> { ); let paths = &scan_args.input_specifier_args.path_inputs; let is_dash = paths.iter().any(|p| p.as_os_str() == "-"); - if (paths.is_empty() || is_dash) && !atty::is(atty::Stream::Stdin) { + if (paths.is_empty() || is_dash) && !std::io::stdin().is_terminal() { let mut buf = Vec::new(); std::io::stdin().read_to_end(&mut buf)?; let stdin_file = temp_dir_path.join("stdin_input"); @@ -772,7 +772,7 @@ pub fn run_rules_list(args: &RulesListArgs) -> Result<()> { match args.output_args.format { RulesListOutputFormat::Pretty => { // Determine terminal width if possible, otherwise use default - let term_width = term_size::dimensions().map(|(w, _)| w).unwrap_or(120); + let term_width = usize::from(Term::stdout().size().1); // First pass: calculate column widths let max_name_width = resolved.iter().map(|r| r.name().len()).max().unwrap_or(0).max(4); // "Rule" header let max_id_width = resolved.iter().map(|r| r.id().len()).max().unwrap_or(0).max(2); // "ID" header diff --git a/src/reporter.rs b/src/reporter.rs index 4c20986..b01c6df 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -598,8 +598,10 @@ impl DetailsReporter { // String::from_utf8_lossy(cmd.message.lines().next().unwrap_or(&[],),). // into_owned(); - let atime = - cmd.committer_timestamp.format(gix::date::time::format::SHORT.clone()).to_string(); + let atime = cmd + .committer_timestamp + .format(gix::date::time::format::SHORT.clone()) + .unwrap_or_else(|_| cmd.committer_timestamp.seconds.to_string()); let git_metadata = serde_json::json!({ "repository_url": repository_url,