Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4d6227b
Prepare the 1.3.0 release and correct the claims the docs make
thebenignhacker Jul 28, 2026
454d6dd
Count what a scan examined, not what the database happened to cover
thebenignhacker Jul 28, 2026
0ad8066
Correct what the changelog says about the database download
thebenignhacker Jul 28, 2026
09c916c
Disclose two defects the second release-test pass found
thebenignhacker Jul 28, 2026
a8ac1ca
Guard the dependency name and the Maven coordinate, not just the version
thebenignhacker Jul 29, 2026
129d8fe
Disclose that a deep scan of unreadable source still counts as examined
thebenignhacker Jul 29, 2026
5d995e9
Claim an examination from files that were read, not from an absent error
thebenignhacker Jul 29, 2026
fc7d842
Bound what a cache reset may delete, and keep an entry it cannot iden…
thebenignhacker Jul 29, 2026
8056f4c
Refuse the Windows and protocol spellings of a local path reference
thebenignhacker Jul 29, 2026
65291c7
Record what this release fixes in the deep source path, and what it h…
thebenignhacker Jul 29, 2026
18c93d9
Date the 1.3.0 entry to the day it ships
thebenignhacker Jul 29, 2026
8ce62ab
Ignore key and credential file patterns
thebenignhacker Jul 29, 2026
8dab971
Hold names to their registry's grammar, and make examination mean a p…
thebenignhacker Jul 29, 2026
97d329f
Record the bypass, the evidence correction and the coverage gap
thebenignhacker Jul 29, 2026
01c829d
Do not call a binary file text because its head walked down to nothing
thebenignhacker Jul 29, 2026
5b00b61
Read source in a single-byte encoding, and say which files went unread
thebenignhacker Jul 30, 2026
01242a0
Close what the fourth adversarial round found in the third round's fixes
thebenignhacker Jul 30, 2026
c7666d8
Correct what the changelog claims, and disclose what 1.3.0 does not c…
thebenignhacker Jul 30, 2026
0baa14d
Stop refusing real dependencies, and stop pip building what it fetches
thebenignhacker Jul 30, 2026
9585143
Record the fifth round in the changelog, and price what it costs
thebenignhacker Jul 30, 2026
9701e30
End flag parsing before the npm spec
thebenignhacker Jul 30, 2026
f69e546
Price the larger source-file cap
thebenignhacker Jul 30, 2026
22ed7b5
Refuse an empty --fail-on, and correct what the changelog still claimed
thebenignhacker Jul 30, 2026
d7b8b4c
State what a scan actually covers, in the README, the help and the ch…
thebenignhacker Jul 30, 2026
4f01240
Correct the hybrid-PQC explanation against the published artifact
thebenignhacker Jul 30, 2026
94cc811
Stop a reporting filter from opening the CI gate
thebenignhacker Jul 30, 2026
952d3d4
Correct four release-note claims against the artifacts they describe
thebenignhacker Jul 30, 2026
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
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,19 @@ Thumbs.db
# Private/local files
.private/
.env
.env.local
.env.*.local
*.local

# Credentials and keys. A scanner that publishes findings about other people's
# secrets should not be able to commit its own by accident.
secrets.json
credentials.json
*.pem
*.key
*.p12
*.pfx

# Downloaded package sources (cache)
.cache/

Expand Down
4 changes: 2 additions & 2 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ builds:

archives:
- id: default
format: tar.gz
formats: [tar.gz]
name_template: >-
{{ .ProjectName }}_
{{- .Version }}_
Expand All @@ -49,7 +49,7 @@ archives:
{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides:
- goos: windows
format: zip
formats: [zip]
files:
- README.md
- LICENSE
Expand Down
807 changes: 754 additions & 53 deletions CHANGELOG.md

Large diffs are not rendered by default.

44 changes: 25 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Quantum-Safe Dependency Analysis for Your Software Supply Chain**

Find every cryptographic vulnerability in your dependencies. Know your quantum risk. Focus on what matters.
Find the cryptography your dependencies bring in, see which of it quantum computers will break, and tell apart what your code actually calls from what is merely available to it.

[![CI](https://github.com/csnp/qramm-cryptodeps/actions/workflows/ci.yml/badge.svg)](https://github.com/csnp/qramm-cryptodeps/actions/workflows/ci.yml)
[![Go Report Card](https://img.shields.io/badge/go%20report-A-brightgreen)](https://goreportcard.com/report/github.com/csnp/qramm-cryptodeps)
Expand All @@ -15,13 +15,15 @@ Find every cryptographic vulnerability in your dependencies. Know your quantum r

## The Quantum Computing Challenge

Quantum computers will break RSA, ECDSA, and Diffie-Hellman within the next decade. This isn't speculation—the NSA, NIST, and major technology companies are already migrating to post-quantum cryptography (PQC).
Quantum computers will break RSA, ECDSA, and Diffie-Hellman within the next decade. This isn't speculation: the NSA, NIST, and major technology companies are already migrating to post-quantum cryptography (PQC).

The challenge? **You can't migrate what you can't find.**

Your code might be quantum-safe, but what about your **dependencies**? The average software project has 300-1000+ transitive dependencies. Each one potentially uses cryptographic algorithms that quantum computers will break. Traditional security scanners miss this—they focus on CVEs, not cryptographic readiness.
Your code might be quantum-safe, but what about your **dependencies**? The average software project has 300-1000+ transitive dependencies. Each one potentially uses cryptographic algorithms that quantum computers will break. Traditional security scanners miss this: they focus on CVEs, not cryptographic readiness.

CryptoDeps solves this by analyzing your entire dependency tree and using **reachability analysis** to show exactly which crypto your code actually uses versus what's merely present in libraries.
CryptoDeps reads the dependencies your manifests declare, identifies the cryptography each one provides, and uses **reachability analysis** to show which of it your code actually calls rather than merely has available.

How much of the tree that covers depends on the ecosystem, so it is worth stating plainly. In Go, `go.mod` lists indirect requirements, and CryptoDeps reads and analyzes them, so a tidied `go.mod` gives you the full closure. In npm, Python and Maven it reads only what the manifest itself declares: lock files are not parsed, so a package pulled in only by another package is outside what those scans report.

---

Expand All @@ -31,7 +33,7 @@ CryptoDeps is purpose-built for quantum readiness assessment:

| Capability | CryptoDeps | grep/ripgrep | Commercial Tools |
|------------|------------|--------------|------------------|
| Dependency tree analysis | Yes | No | Some |
| Declared dependency analysis | Yes | No | Some |
| Reachability analysis | Yes (Go) | No | Rarely |
| Quantum risk classification | Yes | No | Some |
| Context-aware confidence | Yes | No | Varies |
Expand All @@ -46,7 +48,7 @@ CryptoDeps is purpose-built for quantum readiness assessment:
<details>
<summary><strong>What These Capabilities Mean</strong></summary>

- **Dependency tree analysis**: Scans all transitive dependencies, not just direct ones
- **Declared dependency analysis**: Scans what your manifests declare. Go `// indirect` requirements are included, so a tidied `go.mod` covers the closure; npm, Python and Maven are direct-only because lock files are not parsed
- **Reachability analysis**: Traces call graphs to find crypto your code actually invokes
- **Quantum risk classification**: Categorizes by threat level (VULNERABLE, PARTIAL, SAFE)
- **Context-aware confidence**: Distinguishes confirmed usage from mere availability
Expand Down Expand Up @@ -281,7 +283,10 @@ cryptodeps status
## Sample Output

```
[*] Scanning go.mod... found 2 dependencies

Scanning /home/dev/myproject...

[*] Scanning ./go.mod... found 2 dependencies

[!] CONFIRMED - Actually used by your code (requires action):
──────────────────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -386,7 +391,7 @@ cryptodeps:
|------|---------|---------|
| `0` | Success | No findings matching `--fail-on` threshold |
| `1` | Vulnerable | Quantum-vulnerable crypto detected |
| `2` | Error | Analysis failed (invalid manifest, network error) |
| `2` | Error | Analysis failed, including any manifest that was found but could not be read |
| `3` | Partial | Partial-risk crypto detected (with `--fail-on partial`) |

---
Expand Down Expand Up @@ -458,7 +463,7 @@ qramm-cryptodeps/
│ ├── crypto/ # Algorithm patterns & remediation
│ ├── output/ # Formatters (table, JSON, CBOM, SARIF)
│ └── types/ # Shared type definitions
├── data/ # Crypto database (901 packages)
├── data/ # Crypto database (downloaded by `cryptodeps update`)
└── examples/ # Sample projects for testing
```

Expand All @@ -474,7 +479,8 @@ qramm-cryptodeps/
- [x] Quantum risk classification with CNSA 2.0 timeline
- [x] Smart remediation guidance with NIST references
- [x] GitHub repository URL scanning
- [x] Crypto database of 901 packages (69 verified, 832 inferred)
- [x] Crypto database of curated and inferred package entries, refreshed weekly
(run `cryptodeps status` for the counts your install currently holds)
- [x] Workspace & monorepo support (npm, pnpm, Go workspaces)
- [x] Multi-project aggregated results

Expand Down Expand Up @@ -568,28 +574,28 @@ Learn more at [qramm.org](https://qramm.org) and [csnp.org](https://csnp.org).

### NIST Post-Quantum Cryptography Standards

- [FIPS 203 - ML-KEM](https://csrc.nist.gov/pubs/fips/203/final) — Module-Lattice-Based Key-Encapsulation Mechanism (replaces RSA/ECDH)
- [FIPS 204 - ML-DSA](https://csrc.nist.gov/pubs/fips/204/final) — Module-Lattice-Based Digital Signature Algorithm (replaces RSA/ECDSA)
- [FIPS 205 - SLH-DSA](https://csrc.nist.gov/pubs/fips/205/final) — Stateless Hash-Based Digital Signature Algorithm
- [NIST SP 800-131A Rev 2](https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final) — Transitioning cryptographic algorithms and key lengths
- [FIPS 203 - ML-KEM](https://csrc.nist.gov/pubs/fips/203/final): Module-Lattice-Based Key-Encapsulation Mechanism (replaces RSA/ECDH)
- [FIPS 204 - ML-DSA](https://csrc.nist.gov/pubs/fips/204/final): Module-Lattice-Based Digital Signature Algorithm (replaces RSA/ECDSA)
- [FIPS 205 - SLH-DSA](https://csrc.nist.gov/pubs/fips/205/final): Stateless Hash-Based Digital Signature Algorithm
- [NIST SP 800-131A Rev 2](https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final): Transitioning cryptographic algorithms and key lengths

### Additional Resources

- [NSA CNSA 2.0](https://media.defense.gov/2022/Sep/07/2003071834/-1/-1/0/CSA_CNSA_2.0_ALGORITHMS_.PDF) — Commercial National Security Algorithm Suite
- [OMB M-23-02](https://www.whitehouse.gov/wp-content/uploads/2022/11/M-23-02-M-Memo-on-Migrating-to-Post-Quantum-Cryptography.pdf) — Federal PQC Migration Requirements
- [NSA CNSA 2.0](https://media.defense.gov/2022/Sep/07/2003071834/-1/-1/0/CSA_CNSA_2.0_ALGORITHMS_.PDF): Commercial National Security Algorithm Suite
- [OMB M-23-02](https://www.whitehouse.gov/wp-content/uploads/2022/11/M-23-02-M-Memo-on-Migrating-to-Post-Quantum-Cryptography.pdf): Federal PQC Migration Requirements
- [CISA Post-Quantum Cryptography Initiative](https://www.cisa.gov/quantum)
- [CycloneDX CBOM](https://cyclonedx.org/capabilities/cbom/) — Cryptographic Bill of Materials
- [CycloneDX CBOM](https://cyclonedx.org/capabilities/cbom/): Cryptographic Bill of Materials

---

## License

Apache License 2.0 — see [LICENSE](LICENSE) for details.
Apache License 2.0. See [LICENSE](LICENSE) for details.

Copyright 2025 Cyber Security Non-Profit (CSNP)

---

Built with purpose by [CSNP](https://csnp.org) — Advancing cybersecurity for everyone
Built with purpose by [CSNP](https://csnp.org). Advancing cybersecurity for everyone.

[QRAMM](https://qramm.org) | [CSNP](https://csnp.org) | [Issues](https://github.com/csnp/qramm-cryptodeps/issues) | [Twitter](https://twitter.com/caborgsec)
104 changes: 104 additions & 0 deletions cmd/cryptodeps/exit_code_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2025-2026 CyberSecurity NonProfit (CSNP)
// SPDX-License-Identifier: Apache-2.0

package main

import (
"testing"

"github.com/csnp/qramm-cryptodeps/internal/analyzer"
"github.com/csnp/qramm-cryptodeps/pkg/types"
)

// partialRiskScan is a project whose only findings are partial risk, which is
// the one shape in which --fail-on partial and the default policy disagree. A
// project with a vulnerable finding exits 1 under every threshold, so it cannot
// detect a threshold that was ignored.
func partialRiskScan() *types.ScanResult {
return &types.ScanResult{
Summary: types.ScanSummary{
TotalDependencies: 1,
WithCrypto: 1,
QuantumPartial: 1,
},
}
}

// TestFailOnIsReadTheSameWayItWasValidated is the regression test for a silent
// loosening of the CI gate by whitespace.
//
// ValidateFailOn trimmed and lowercased before deciding, and determineExitCode
// only lowercased. So " partial " passed validation, matched no case, and fell
// through to the default vulnerable-only policy: a project with partial-risk
// findings exited 3 for "partial" and 0 for " partial ", with nothing on either
// stream. Whitespace around a value is the ordinary result of a YAML block
// scalar or a workflow expression, which is exactly where this flag is used, and
// the validation added in this release is what made the value look accepted.
func TestFailOnIsReadTheSameWayItWasValidated(t *testing.T) {
result := partialRiskScan()

// Guard the fixture: the two policies have to disagree on it, or every
// assertion below passes for the wrong reason.
if got := determineExitCode(result, "partial"); got != ExitPartial {
t.Fatalf("determineExitCode(partial) = %d, want %d; the fixture does not exercise "+
"the partial policy", got, ExitPartial)
}
if got := determineExitCode(result, "vulnerable"); got != ExitSuccess {
t.Fatalf("determineExitCode(vulnerable) = %d, want %d; the fixture does not "+
"distinguish the two policies", got, ExitSuccess)
}

for _, threshold := range []string{" partial", "partial ", " partial ", "\tpartial", "PARTIAL", " Partial\n"} {
t.Run(threshold, func(t *testing.T) {
// Whatever the validator accepts, the exit code must honour.
if err := analyzer.ValidateFailOn(threshold); err != nil {
t.Fatalf("ValidateFailOn(%q) rejected it, so this case is not the one under "+
"test: %v", threshold, err)
}
if got := determineExitCode(result, threshold); got != ExitPartial {
t.Errorf("determineExitCode(%q) = %d, want %d: the value was accepted as valid "+
"and then ignored, reverting the gate to the default policy",
threshold, got, ExitPartial)
}
})
}
}

// TestFailOnRejectsWhatItCannotHonour is the inverse: canonicalising must not
// turn a typo into a valid value, because a gate that silently accepts "partail"
// is the defect the validation exists to prevent.
func TestFailOnRejectsWhatItCannotHonour(t *testing.T) {
for _, threshold := range []string{"partail", "vulnerble", "maybe", "partial risk", "1"} {
if err := analyzer.ValidateFailOn(threshold); err == nil {
t.Errorf("ValidateFailOn(%q) accepted a value that determineExitCode reads as the "+
"default policy", threshold)
}
}
}

// TestFailOnRefusesAnEmptyValue closes the last spelling of the silent gate
// loosening.
//
// The padded value was fixed by canonicalising; the empty string still fell
// through, because "" was listed as a legal value and matched no policy. A
// project with partial-risk findings exited 3 for "partial" and 0 for "", with
// nothing on either stream, and an unset workflow input is exactly how a CI gate
// arrives here empty. This flag has a default, so a blank value expresses
// nothing: unlike --risk and --min-severity, where empty means "do not filter".
func TestFailOnRefusesAnEmptyValue(t *testing.T) {
result := partialRiskScan()

// Guard the fixture: the two policies must disagree on it, or the assertion
// below is answered by a project no threshold would fail.
if determineExitCode(result, "partial") == determineExitCode(result, "vulnerable") {
t.Fatalf("the fixture does not distinguish the partial policy from the default")
}

for _, empty := range []string{"", " ", "\t", "\n"} {
if err := analyzer.ValidateFailOn(empty); err == nil {
t.Errorf("ValidateFailOn(%q) accepted a value that determineExitCode reads as the "+
"default policy, so a gate asking for partial risk passes a partial-risk "+
"project", empty)
}
}
}
Loading
Loading