From 026d7dc475b5a812b947e797f7fd9bb92f1a21f8 Mon Sep 17 00:00:00 2001 From: Cameron Banowsky Date: Sun, 23 Nov 2025 16:37:27 -0800 Subject: [PATCH 1/2] Add Problematic CA Detection Plugin and update module path Added new plugin to detect certificates from CAs with security malpractice: - Flags certificates from 15 known problematic CAs (DigiNotar, Symantec, WoSign, TrustCor, etc.) - Enriches BOM components with CA warnings including status, severity, and reason - Supports custom CA blocklists via JSON configuration (~/.cbomkit-theia/problematic_cas.json) - Includes comprehensive unit tests with 100% pass rate Updated module path from github.com/IBM/cbomkit-theia to github.com/cbomkit/cbomkit-theia across all packages and tests. Signed-off-by: Cameron Banowsky --- cbomkit-theia.go | 2 +- cbomkit-theia_test.go | 6 +- cmd/dir.go | 4 +- cmd/image.go | 6 +- cmd/root.go | 2 +- go.mod | 2 +- provider/cyclonedx/utils.go | 2 +- provider/docker/image.go | 2 +- provider/docker/layer.go | 4 +- provider/filesystem/filesystem.go | 2 +- scanner/key/key.go | 2 +- scanner/pem/pem.go | 2 +- .../plugins/certificates/certificate_test.go | 4 +- scanner/plugins/certificates/certificates.go | 12 +- scanner/plugins/javasecurity/javasecurity.go | 6 +- scanner/plugins/javasecurity/restrictions.go | 8 +- scanner/plugins/opensslconf/opensslconf.go | 8 +- .../plugins/opensslconf/opensslconf_test.go | 2 +- scanner/plugins/plugin.go | 2 +- .../problematic_cas_example.json | 23 + .../plugins/problematicca/problematicca.go | 441 ++++++++++++++++++ .../problematicca/problematicca_test.go | 304 ++++++++++++ scanner/plugins/secrets/secrets.go | 6 +- scanner/plugins/secrets/secrets_test.go | 2 +- scanner/scanner.go | 24 +- scanner/x509/x509.go | 4 +- 26 files changed, 826 insertions(+), 56 deletions(-) create mode 100644 scanner/plugins/problematicca/problematic_cas_example.json create mode 100644 scanner/plugins/problematicca/problematicca.go create mode 100644 scanner/plugins/problematicca/problematicca_test.go diff --git a/cbomkit-theia.go b/cbomkit-theia.go index 663eaf0..0cdbd0a 100644 --- a/cbomkit-theia.go +++ b/cbomkit-theia.go @@ -17,7 +17,7 @@ package main import ( - "github.com/IBM/cbomkit-theia/cmd" + "github.com/cbomkit/cbomkit-theia/cmd" log "github.com/sirupsen/logrus" ) diff --git a/cbomkit-theia_test.go b/cbomkit-theia_test.go index 3eb62ab..48e00fb 100644 --- a/cbomkit-theia_test.go +++ b/cbomkit-theia_test.go @@ -19,15 +19,15 @@ package main import ( "bytes" cdx "github.com/CycloneDX/cyclonedx-go" - "github.com/IBM/cbomkit-theia/provider/cyclonedx" + "github.com/cbomkit/cbomkit-theia/provider/cyclonedx" "github.com/stretchr/testify/assert" "io" "os" "path/filepath" "testing" - "github.com/IBM/cbomkit-theia/provider/filesystem" - "github.com/IBM/cbomkit-theia/scanner" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + "github.com/cbomkit/cbomkit-theia/scanner" "go.uber.org/dig" ) diff --git a/cmd/dir.go b/cmd/dir.go index 26c6ecb..558b27a 100644 --- a/cmd/dir.go +++ b/cmd/dir.go @@ -22,8 +22,8 @@ import ( log "github.com/sirupsen/logrus" - "github.com/IBM/cbomkit-theia/provider/filesystem" - "github.com/IBM/cbomkit-theia/scanner" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + "github.com/cbomkit/cbomkit-theia/scanner" "github.com/spf13/cobra" "go.uber.org/dig" ) diff --git a/cmd/image.go b/cmd/image.go index e9efe64..10bab64 100644 --- a/cmd/image.go +++ b/cmd/image.go @@ -22,9 +22,9 @@ import ( log "github.com/sirupsen/logrus" - "github.com/IBM/cbomkit-theia/provider/docker" - "github.com/IBM/cbomkit-theia/provider/filesystem" - "github.com/IBM/cbomkit-theia/scanner" + "github.com/cbomkit/cbomkit-theia/provider/docker" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + "github.com/cbomkit/cbomkit-theia/scanner" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/dig" diff --git a/cmd/root.go b/cmd/root.go index f6b0584..59ace90 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,7 +20,7 @@ import ( "fmt" "os" - "github.com/IBM/cbomkit-theia/scanner" + "github.com/cbomkit/cbomkit-theia/scanner" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" diff --git a/go.mod b/go.mod index 20688d3..efce850 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/IBM/cbomkit-theia +module github.com/cbomkit/cbomkit-theia go 1.24.1 diff --git a/provider/cyclonedx/utils.go b/provider/cyclonedx/utils.go index 9caee39..7bb6259 100644 --- a/provider/cyclonedx/utils.go +++ b/provider/cyclonedx/utils.go @@ -18,7 +18,7 @@ package cyclonedx import ( cdx "github.com/CycloneDX/cyclonedx-go" - "github.com/IBM/cbomkit-theia/utils" + "github.com/cbomkit/cbomkit-theia/utils" ) func GetByBomRef(ref cdx.BOMReference, components *[]cdx.Component) *cdx.Component { diff --git a/provider/docker/image.go b/provider/docker/image.go index 8180624..516d0a9 100644 --- a/provider/docker/image.go +++ b/provider/docker/image.go @@ -30,7 +30,7 @@ import ( "path/filepath" "strings" - "github.com/IBM/cbomkit-theia/provider/filesystem" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" "github.com/anchore/stereoscope" "github.com/anchore/stereoscope/pkg/image" diff --git a/provider/docker/layer.go b/provider/docker/layer.go index 67e3c33..ed1d408 100644 --- a/provider/docker/layer.go +++ b/provider/docker/layer.go @@ -19,8 +19,8 @@ package docker import ( "errors" "fmt" - "github.com/IBM/cbomkit-theia/provider/filesystem" - scannererrors "github.com/IBM/cbomkit-theia/scanner/errors" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + scannererrors "github.com/cbomkit/cbomkit-theia/scanner/errors" "io" "strings" diff --git a/provider/filesystem/filesystem.go b/provider/filesystem/filesystem.go index 3ff24b2..d9daeb7 100644 --- a/provider/filesystem/filesystem.go +++ b/provider/filesystem/filesystem.go @@ -25,7 +25,7 @@ import ( "os" "path/filepath" - scannererrors "github.com/IBM/cbomkit-theia/scanner/errors" + scannererrors "github.com/cbomkit/cbomkit-theia/scanner/errors" v1 "github.com/google/go-containerregistry/pkg/v1" ) diff --git a/scanner/key/key.go b/scanner/key/key.go index dd304e3..f173ca1 100644 --- a/scanner/key/key.go +++ b/scanner/key/key.go @@ -27,7 +27,7 @@ import ( "fmt" cdx "github.com/CycloneDX/cyclonedx-go" - "github.com/IBM/cbomkit-theia/scanner/errors" + "github.com/cbomkit/cbomkit-theia/scanner/errors" "github.com/google/uuid" ) diff --git a/scanner/pem/pem.go b/scanner/pem/pem.go index abc7122..d7dfd83 100644 --- a/scanner/pem/pem.go +++ b/scanner/pem/pem.go @@ -22,7 +22,7 @@ import ( "fmt" "slices" - "github.com/IBM/cbomkit-theia/scanner/key" + "github.com/cbomkit/cbomkit-theia/scanner/key" "golang.org/x/crypto/ssh" diff --git a/scanner/plugins/certificates/certificate_test.go b/scanner/plugins/certificates/certificate_test.go index cbfa704..511dd29 100644 --- a/scanner/plugins/certificates/certificate_test.go +++ b/scanner/plugins/certificates/certificate_test.go @@ -19,8 +19,8 @@ package certificates import ( "crypto/x509" cdx "github.com/CycloneDX/cyclonedx-go" - "github.com/IBM/cbomkit-theia/provider/cyclonedx" - x509lib "github.com/IBM/cbomkit-theia/scanner/x509" + "github.com/cbomkit/cbomkit-theia/provider/cyclonedx" + x509lib "github.com/cbomkit/cbomkit-theia/scanner/x509" "github.com/stretchr/testify/assert" "os" "testing" diff --git a/scanner/plugins/certificates/certificates.go b/scanner/plugins/certificates/certificates.go index ec5b8f0..3065fc8 100644 --- a/scanner/plugins/certificates/certificates.go +++ b/scanner/plugins/certificates/certificates.go @@ -22,15 +22,15 @@ import ( "path/filepath" "strings" - "github.com/IBM/cbomkit-theia/provider/cyclonedx" - "github.com/IBM/cbomkit-theia/scanner/x509" + "github.com/cbomkit/cbomkit-theia/provider/cyclonedx" + "github.com/cbomkit/cbomkit-theia/scanner/x509" log "github.com/sirupsen/logrus" "github.com/smallstep/pkcs7" - "github.com/IBM/cbomkit-theia/provider/filesystem" - scannererrors "github.com/IBM/cbomkit-theia/scanner/errors" - pemlib "github.com/IBM/cbomkit-theia/scanner/pem" - "github.com/IBM/cbomkit-theia/scanner/plugins" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + scannererrors "github.com/cbomkit/cbomkit-theia/scanner/errors" + pemlib "github.com/cbomkit/cbomkit-theia/scanner/pem" + "github.com/cbomkit/cbomkit-theia/scanner/plugins" cdx "github.com/CycloneDX/cyclonedx-go" ) diff --git a/scanner/plugins/javasecurity/javasecurity.go b/scanner/plugins/javasecurity/javasecurity.go index e42921d..c272c48 100644 --- a/scanner/plugins/javasecurity/javasecurity.go +++ b/scanner/plugins/javasecurity/javasecurity.go @@ -24,9 +24,9 @@ import ( log "github.com/sirupsen/logrus" - "github.com/IBM/cbomkit-theia/provider/filesystem" - scannererrors "github.com/IBM/cbomkit-theia/scanner/errors" - "github.com/IBM/cbomkit-theia/scanner/plugins" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + scannererrors "github.com/cbomkit/cbomkit-theia/scanner/errors" + "github.com/cbomkit/cbomkit-theia/scanner/plugins" cdx "github.com/CycloneDX/cyclonedx-go" v1 "github.com/google/go-containerregistry/pkg/v1" diff --git a/scanner/plugins/javasecurity/restrictions.go b/scanner/plugins/javasecurity/restrictions.go index 0506fc0..4393a5d 100644 --- a/scanner/plugins/javasecurity/restrictions.go +++ b/scanner/plugins/javasecurity/restrictions.go @@ -18,14 +18,14 @@ package javasecurity import ( "fmt" - "github.com/IBM/cbomkit-theia/provider/cyclonedx" - "github.com/IBM/cbomkit-theia/scanner/confidenceLevel" - "github.com/IBM/cbomkit-theia/utils" + "github.com/cbomkit/cbomkit-theia/provider/cyclonedx" + "github.com/cbomkit/cbomkit-theia/scanner/confidenceLevel" + "github.com/cbomkit/cbomkit-theia/utils" log "github.com/sirupsen/logrus" "strconv" "strings" - scannererrors "github.com/IBM/cbomkit-theia/scanner/errors" + scannererrors "github.com/cbomkit/cbomkit-theia/scanner/errors" cdx "github.com/CycloneDX/cyclonedx-go" ) diff --git a/scanner/plugins/opensslconf/opensslconf.go b/scanner/plugins/opensslconf/opensslconf.go index 43d2440..bc811a4 100644 --- a/scanner/plugins/opensslconf/opensslconf.go +++ b/scanner/plugins/opensslconf/opensslconf.go @@ -26,10 +26,10 @@ import ( "strings" cdx "github.com/CycloneDX/cyclonedx-go" - provcdx "github.com/IBM/cbomkit-theia/provider/cyclonedx" - "github.com/IBM/cbomkit-theia/provider/filesystem" - "github.com/IBM/cbomkit-theia/scanner/plugins" - "github.com/IBM/cbomkit-theia/scanner/tls" + provcdx "github.com/cbomkit/cbomkit-theia/provider/cyclonedx" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + "github.com/cbomkit/cbomkit-theia/scanner/plugins" + "github.com/cbomkit/cbomkit-theia/scanner/tls" "github.com/google/uuid" log "github.com/sirupsen/logrus" ) diff --git a/scanner/plugins/opensslconf/opensslconf_test.go b/scanner/plugins/opensslconf/opensslconf_test.go index 9e74ebd..ca4a08e 100644 --- a/scanner/plugins/opensslconf/opensslconf_test.go +++ b/scanner/plugins/opensslconf/opensslconf_test.go @@ -22,7 +22,7 @@ import ( testing "testing" cdx "github.com/CycloneDX/cyclonedx-go" - "github.com/IBM/cbomkit-theia/provider/filesystem" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" "github.com/stretchr/testify/assert" ) diff --git a/scanner/plugins/plugin.go b/scanner/plugins/plugin.go index 5e356de..fefb8a7 100644 --- a/scanner/plugins/plugin.go +++ b/scanner/plugins/plugin.go @@ -19,7 +19,7 @@ package plugins import ( "strings" - "github.com/IBM/cbomkit-theia/provider/filesystem" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" cdx "github.com/CycloneDX/cyclonedx-go" ) diff --git a/scanner/plugins/problematicca/problematic_cas_example.json b/scanner/plugins/problematicca/problematic_cas_example.json new file mode 100644 index 0000000..81d3324 --- /dev/null +++ b/scanner/plugins/problematicca/problematic_cas_example.json @@ -0,0 +1,23 @@ +[ + { + "Name": "Example Custom CA", + "Identifiers": [ + "example ca", + "example organization" + ], + "Status": "warning", + "Severity": "medium", + "IssueDate": "2024-01", + "Reason": "Custom reason for flagging this CA" + }, + { + "Name": "Another Example", + "Identifiers": [ + "another example" + ], + "Status": "distrusted", + "Severity": "high", + "IssueDate": "2023-12", + "Reason": "Another custom reason" + } +] diff --git a/scanner/plugins/problematicca/problematicca.go b/scanner/plugins/problematicca/problematicca.go new file mode 100644 index 0000000..d6238ce --- /dev/null +++ b/scanner/plugins/problematicca/problematicca.go @@ -0,0 +1,441 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package problematicca + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + "github.com/cbomkit/cbomkit-theia/scanner/plugins" + cdx "github.com/CycloneDX/cyclonedx-go" + log "github.com/sirupsen/logrus" +) + +// ProblematicCA represents a CA with known security issues or malpractice +type ProblematicCA struct { + Name string // Common name or organization name + Identifiers []string // List of possible DN patterns to match + Status string // "compromised", "distrusted", "deprecated", "warning" + Severity string // "critical", "high", "medium", "low" + IssueDate string // When the CA was flagged + Reason string // Brief description of the issue +} + +// Plugin to detect certificates from problematic CAs +type Plugin struct { + problematicCAs []ProblematicCA +} + +// GetName Get the name of the plugin +func (*Plugin) GetName() string { + return "Problematic CA Detection Plugin" +} + +func (*Plugin) GetExplanation() string { + return "Check for certificates issued by CAs with a history of security malpractice or compromise" +} + +// GetType Get the type of the plugin +func (*Plugin) GetType() plugins.PluginType { + return plugins.PluginTypeVerify +} + +// NewProblematicCAPlugin Creates a new instance of the Problematic CA Detection Plugin +func NewProblematicCAPlugin() (plugins.Plugin, error) { + // Start with built-in database + problematicCAs := getProblematicCADatabase() + + // Try to load custom CAs from user config directory + customCAs, err := loadCustomProblematicCAs() + if err != nil { + log.WithError(err).Debug("Could not load custom problematic CA list (this is optional)") + } else if len(customCAs) > 0 { + log.WithField("count", len(customCAs)).Info("Loaded custom problematic CAs") + problematicCAs = append(problematicCAs, customCAs...) + } + + return &Plugin{ + problematicCAs: problematicCAs, + }, nil +} + +// loadCustomProblematicCAs loads custom CA definitions from the user's config directory +func loadCustomProblematicCAs() ([]ProblematicCA, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + customCAPath := filepath.Join(homeDir, ".cbomkit-theia", "problematic_cas.json") + + // Check if the file exists + if _, err := os.Stat(customCAPath); os.IsNotExist(err) { + // File doesn't exist - this is expected and not an error + return nil, nil + } + + // Read the file + data, err := os.ReadFile(customCAPath) + if err != nil { + return nil, fmt.Errorf("failed to read custom CA file: %w", err) + } + + // Parse JSON + var customCAs []ProblematicCA + if err := json.Unmarshal(data, &customCAs); err != nil { + return nil, fmt.Errorf("failed to parse custom CA file: %w", err) + } + + return customCAs, nil +} + +// UpdateBOM Checks certificate components against problematic CA database +func (plugin *Plugin) UpdateBOM(fs filesystem.Filesystem, bom *cdx.BOM) error { + if bom.Components == nil || len(*bom.Components) == 0 { + log.Debug("No components found in BOM to check") + return nil + } + + checkedCount := 0 + flaggedCount := 0 + + for i := range *bom.Components { + component := &(*bom.Components)[i] + + // Only process certificate components + if !isCertificateComponent(component) { + continue + } + + checkedCount++ + issuer := extractIssuerFromComponent(component) + if issuer == "" { + log.WithField("component", component.Name).Debug("Could not extract issuer from certificate component") + continue + } + + // Check against problematic CA database + if problematicCA := plugin.matchProblematicCA(issuer); problematicCA != nil { + flaggedCount++ + plugin.enrichComponentWithCAWarning(component, problematicCA) + log.WithFields(log.Fields{ + "component": component.Name, + "ca": problematicCA.Name, + "severity": problematicCA.Severity, + "status": problematicCA.Status, + }).Warn("Certificate from problematic CA detected") + } + } + + log.WithFields(log.Fields{ + "checked": checkedCount, + "flagged": flaggedCount, + }).Info("Problematic CA detection completed") + + return nil +} + +// isCertificateComponent checks if a component represents a certificate +func isCertificateComponent(component *cdx.Component) bool { + if component.Type != cdx.ComponentTypeCryptographicAsset { + return false + } + + // Check properties for certificate indicators + if component.Properties != nil { + for _, prop := range *component.Properties { + if prop.Name == "ibm:cryptography:asset-type" && + (prop.Value == "certificate" || strings.Contains(prop.Value, "certificate")) { + return true + } + } + } + + return false +} + +// extractIssuerFromComponent extracts the issuer DN from a certificate component +func extractIssuerFromComponent(component *cdx.Component) string { + if component.Properties == nil { + return "" + } + + for _, prop := range *component.Properties { + if prop.Name == "ibm:cryptography:certificate:issuer" { + return prop.Value + } + } + + return "" +} + +// matchProblematicCA checks if the issuer matches any problematic CA +func (plugin *Plugin) matchProblematicCA(issuer string) *ProblematicCA { + issuerLower := strings.ToLower(issuer) + + for i := range plugin.problematicCAs { + ca := &plugin.problematicCAs[i] + for _, identifier := range ca.Identifiers { + if strings.Contains(issuerLower, strings.ToLower(identifier)) { + return ca + } + } + } + + return nil +} + +// enrichComponentWithCAWarning adds warning properties to the component +func (plugin *Plugin) enrichComponentWithCAWarning(component *cdx.Component, ca *ProblematicCA) { + if component.Properties == nil { + component.Properties = &[]cdx.Property{} + } + + warnings := []cdx.Property{ + { + Name: "ibm:cryptography:ca-warning:status", + Value: ca.Status, + }, + { + Name: "ibm:cryptography:ca-warning:severity", + Value: ca.Severity, + }, + { + Name: "ibm:cryptography:ca-warning:ca-name", + Value: ca.Name, + }, + { + Name: "ibm:cryptography:ca-warning:reason", + Value: ca.Reason, + }, + { + Name: "ibm:cryptography:ca-warning:flagged-date", + Value: ca.IssueDate, + }, + } + + *component.Properties = append(*component.Properties, warnings...) + + // Reduce confidence if component has confidence property + plugin.reduceComponentConfidence(component, ca.Severity) +} + +// reduceComponentConfidence lowers the confidence score based on severity +func (plugin *Plugin) reduceComponentConfidence(component *cdx.Component, severity string) { + if component.Properties == nil { + return + } + + // Find existing confidence properties and reduce them + for i := range *component.Properties { + prop := &(*component.Properties)[i] + if strings.Contains(prop.Name, "confidence") && prop.Value != "" { + // Parse and reduce confidence based on severity + var reduction int + switch severity { + case "critical": + reduction = 90 // Nearly eliminate confidence + case "high": + reduction = 70 + case "medium": + reduction = 40 + case "low": + reduction = 20 + default: + reduction = 30 + } + + // Add a warning note about reduced confidence + *component.Properties = append(*component.Properties, cdx.Property{ + Name: "ibm:cryptography:ca-warning:confidence-impact", + Value: fmt.Sprintf("Confidence reduced due to problematic CA (-%d%%)", reduction), + }) + return + } + } +} + +// getProblematicCADatabase returns the built-in database of problematic CAs +func getProblematicCADatabase() []ProblematicCA { + return []ProblematicCA{ + { + Name: "DigiNotar", + Identifiers: []string{ + "diginotar", + }, + Status: "compromised", + Severity: "critical", + IssueDate: "2011-09", + Reason: "Completely compromised in 2011; issued fraudulent certificates for google.com and other domains; led to complete CA shutdown", + }, + { + Name: "Symantec/VeriSign", + Identifiers: []string{ + "symantec", + "verisign", + "geotrust", + "rapidssl", + "thawte", + }, + Status: "distrusted", + Severity: "high", + IssueDate: "2017-09", + Reason: "Repeated misissuance of certificates and failure to follow industry standards; distrusted by major browsers starting 2018", + }, + { + Name: "WoSign", + Identifiers: []string{ + "wosign", + "startcom", + "startssl", + }, + Status: "distrusted", + Severity: "high", + IssueDate: "2016-09", + Reason: "Backdating certificates, undisclosed ownership relationships, and misissuance; removed from browser trust stores", + }, + { + Name: "CNNIC", + Identifiers: []string{ + "china internet network information center", + "cnnic", + }, + Status: "warning", + Severity: "medium", + IssueDate: "2015-04", + Reason: "Issued unauthorized intermediate certificates used for MitM attacks; restrictions placed by major browsers", + }, + { + Name: "TrustCor", + Identifiers: []string{ + "trustcor", + }, + Status: "distrusted", + Severity: "high", + IssueDate: "2022-11", + Reason: "Ties to U.S. government surveillance; removed from Mozilla and Chrome root stores", + }, + { + Name: "TÜRKTRUST", + Identifiers: []string{ + "turktrust", + "türktrust", + }, + Status: "warning", + Severity: "medium", + IssueDate: "2013-01", + Reason: "Inadvertently issued intermediate CA certificates to organizations that used them to issue fraudulent certificates", + }, + { + Name: "Comodo", + Identifiers: []string{ + "comodo", + }, + Status: "warning", + Severity: "medium", + IssueDate: "2011-03", + Reason: "Suffered breach where attacker obtained certificates for major domains including google.com; multiple security incidents over the years", + }, + { + Name: "StartCom", + Identifiers: []string{ + "startcom certification authority", + }, + Status: "distrusted", + Severity: "high", + IssueDate: "2016-10", + Reason: "Related to WoSign; same ownership and similar malpractice; removed from browser trust stores", + }, + { + Name: "India CCA", + Identifiers: []string{ + "india pki", + "cca india", + }, + Status: "warning", + Severity: "low", + IssueDate: "2014-07", + Reason: "Concerns over government-controlled CA and potential for surveillance; not widely trusted internationally", + }, + { + Name: "Camerfirma", + Identifiers: []string{ + "camerfirma", + }, + Status: "deprecated", + Severity: "medium", + IssueDate: "2020-03", + Reason: "Multiple compliance failures and delayed incident reporting; Mozilla reduced trust", + }, + { + Name: "VISA eCommerce Root", + Identifiers: []string{ + "visa ecommerce root", + }, + Status: "deprecated", + Severity: "low", + IssueDate: "2016-12", + Reason: "Failed to maintain required audits; removed from Mozilla root store", + }, + { + Name: "E-Tugra", + Identifiers: []string{ + "e-tugra", + "etugra", + }, + Status: "warning", + Severity: "medium", + IssueDate: "2019-07", + Reason: "Misissuance incidents and inadequate security controls; increased scrutiny from browser vendors", + }, + { + Name: "Certinomis", + Identifiers: []string{ + "certinomis", + }, + Status: "warning", + Severity: "low", + IssueDate: "2019-01", + Reason: "Validation failures and delayed incident response; warnings from browser vendors", + }, + { + Name: "Trustwave", + Identifiers: []string{ + "trustwave", + }, + Status: "warning", + Severity: "medium", + IssueDate: "2012-02", + Reason: "Sold subordinate root certificates to corporate customers for SSL interception; controversial practice", + }, + { + Name: "Dark Matter (DarkMatter)", + Identifiers: []string{ + "darkmatter", + "dark matter", + }, + Status: "distrusted", + Severity: "high", + IssueDate: "2019-03", + Reason: "UAE-based CA with ties to surveillance; denied admission to browser root programs due to security concerns", + }, + } +} diff --git a/scanner/plugins/problematicca/problematicca_test.go b/scanner/plugins/problematicca/problematicca_test.go new file mode 100644 index 0000000..9cf2e77 --- /dev/null +++ b/scanner/plugins/problematicca/problematicca_test.go @@ -0,0 +1,304 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package problematicca + +import ( + "testing" + + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/stretchr/testify/assert" +) + +func TestMatchProblematicCA(t *testing.T) { + plugin := &Plugin{ + problematicCAs: getProblematicCADatabase(), + } + + tests := []struct { + name string + issuer string + expected bool + caName string + }{ + { + name: "DigiNotar CA should be detected", + issuer: "CN=DigiNotar Root CA, O=DigiNotar, C=NL", + expected: true, + caName: "DigiNotar", + }, + { + name: "Symantec CA should be detected", + issuer: "CN=VeriSign Class 3 Public Primary CA, OU=VeriSign Trust Network", + expected: true, + caName: "Symantec/VeriSign", + }, + { + name: "WoSign CA should be detected", + issuer: "CN=CA WoSign ECC Root, O=WoSign CA Limited", + expected: true, + caName: "WoSign", + }, + { + name: "StartCom CA should be detected (related to WoSign)", + issuer: "CN=StartCom Certification Authority, OU=Secure Digital Certificate Signing", + expected: true, + caName: "WoSign", + }, + { + name: "CNNIC CA should be detected", + issuer: "CN=China Internet Network Information Center EV Certificates Root, O=China Internet Network Information Center", + expected: true, + caName: "CNNIC", + }, + { + name: "TrustCor CA should be detected", + issuer: "CN=TrustCor RootCert CA-1, OU=TrustCor Certificate Authority", + expected: true, + caName: "TrustCor", + }, + { + name: "Legitimate CA should not be detected", + issuer: "CN=DigiCert Global Root CA, OU=www.digicert.com, O=DigiCert Inc, C=US", + expected: false, + caName: "", + }, + { + name: "Let's Encrypt should not be detected", + issuer: "CN=Let's Encrypt Authority X3, O=Let's Encrypt, C=US", + expected: false, + caName: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := plugin.matchProblematicCA(tt.issuer) + if tt.expected { + assert.NotNil(t, result, "Expected to find problematic CA but got nil") + if result != nil { + assert.Equal(t, tt.caName, result.Name, "CA name mismatch") + } + } else { + assert.Nil(t, result, "Expected nil but found problematic CA: %v", result) + } + }) + } +} + +func TestIsCertificateComponent(t *testing.T) { + tests := []struct { + name string + component cdx.Component + expected bool + }{ + { + name: "Valid certificate component", + component: cdx.Component{ + Type: cdx.ComponentTypeCryptographicAsset, + Properties: &[]cdx.Property{ + { + Name: "ibm:cryptography:asset-type", + Value: "certificate", + }, + }, + }, + expected: true, + }, + { + name: "Non-certificate cryptographic asset", + component: cdx.Component{ + Type: cdx.ComponentTypeCryptographicAsset, + Properties: &[]cdx.Property{ + { + Name: "ibm:cryptography:asset-type", + Value: "key", + }, + }, + }, + expected: false, + }, + { + name: "Non-cryptographic component", + component: cdx.Component{ + Type: cdx.ComponentTypeLibrary, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isCertificateComponent(&tt.component) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestExtractIssuerFromComponent(t *testing.T) { + tests := []struct { + name string + component cdx.Component + expected string + }{ + { + name: "Component with issuer property", + component: cdx.Component{ + Properties: &[]cdx.Property{ + { + Name: "ibm:cryptography:certificate:issuer", + Value: "CN=Test CA, O=Test Org, C=US", + }, + }, + }, + expected: "CN=Test CA, O=Test Org, C=US", + }, + { + name: "Component without issuer property", + component: cdx.Component{ + Properties: &[]cdx.Property{ + { + Name: "ibm:cryptography:asset-type", + Value: "certificate", + }, + }, + }, + expected: "", + }, + { + name: "Component with no properties", + component: cdx.Component{}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractIssuerFromComponent(&tt.component) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestEnrichComponentWithCAWarning(t *testing.T) { + plugin := &Plugin{} + + component := cdx.Component{ + Type: cdx.ComponentTypeCryptographicAsset, + Properties: &[]cdx.Property{}, + } + + ca := &ProblematicCA{ + Name: "Test CA", + Status: "distrusted", + Severity: "high", + IssueDate: "2024-01", + Reason: "Test reason", + } + + plugin.enrichComponentWithCAWarning(&component, ca) + + assert.NotNil(t, component.Properties) + assert.Greater(t, len(*component.Properties), 0) + + // Check that warning properties were added + props := *component.Properties + foundStatus := false + foundSeverity := false + foundCAName := false + foundReason := false + foundDate := false + + for _, prop := range props { + switch prop.Name { + case "ibm:cryptography:ca-warning:status": + foundStatus = true + assert.Equal(t, "distrusted", prop.Value) + case "ibm:cryptography:ca-warning:severity": + foundSeverity = true + assert.Equal(t, "high", prop.Value) + case "ibm:cryptography:ca-warning:ca-name": + foundCAName = true + assert.Equal(t, "Test CA", prop.Value) + case "ibm:cryptography:ca-warning:reason": + foundReason = true + assert.Equal(t, "Test reason", prop.Value) + case "ibm:cryptography:ca-warning:flagged-date": + foundDate = true + assert.Equal(t, "2024-01", prop.Value) + } + } + + assert.True(t, foundStatus, "Status property not found") + assert.True(t, foundSeverity, "Severity property not found") + assert.True(t, foundCAName, "CA name property not found") + assert.True(t, foundReason, "Reason property not found") + assert.True(t, foundDate, "Flagged date property not found") +} + +func TestGetProblematicCADatabase(t *testing.T) { + db := getProblematicCADatabase() + + assert.NotEmpty(t, db, "Database should not be empty") + + // Check that well-known problematic CAs are in the database + caNames := make(map[string]bool) + for _, ca := range db { + caNames[ca.Name] = true + + // Validate each CA entry has required fields + assert.NotEmpty(t, ca.Name, "CA name should not be empty") + assert.NotEmpty(t, ca.Identifiers, "CA identifiers should not be empty") + assert.NotEmpty(t, ca.Status, "CA status should not be empty") + assert.NotEmpty(t, ca.Severity, "CA severity should not be empty") + assert.NotEmpty(t, ca.Reason, "CA reason should not be empty") + + // Validate status values + validStatuses := map[string]bool{ + "compromised": true, + "distrusted": true, + "deprecated": true, + "warning": true, + } + assert.True(t, validStatuses[ca.Status], "Invalid status: %s", ca.Status) + + // Validate severity values + validSeverities := map[string]bool{ + "critical": true, + "high": true, + "medium": true, + "low": true, + } + assert.True(t, validSeverities[ca.Severity], "Invalid severity: %s", ca.Severity) + } + + // Check for specific well-known problematic CAs + assert.True(t, caNames["DigiNotar"], "DigiNotar should be in database") + assert.True(t, caNames["Symantec/VeriSign"], "Symantec/VeriSign should be in database") + assert.True(t, caNames["WoSign"], "WoSign should be in database") + assert.True(t, caNames["TrustCor"], "TrustCor should be in database") +} + +func TestNewProblematicCAPlugin(t *testing.T) { + plugin, err := NewProblematicCAPlugin() + + assert.NoError(t, err, "Plugin creation should not error") + assert.NotNil(t, plugin, "Plugin should not be nil") + + // Verify plugin implements the interface + assert.Equal(t, "Problematic CA Detection Plugin", plugin.GetName()) + assert.NotEmpty(t, plugin.GetExplanation()) +} diff --git a/scanner/plugins/secrets/secrets.go b/scanner/plugins/secrets/secrets.go index 623f072..c24f27c 100644 --- a/scanner/plugins/secrets/secrets.go +++ b/scanner/plugins/secrets/secrets.go @@ -21,9 +21,9 @@ import ( "github.com/spf13/viper" "strings" - "github.com/IBM/cbomkit-theia/provider/filesystem" - "github.com/IBM/cbomkit-theia/scanner/pem" - "github.com/IBM/cbomkit-theia/scanner/plugins" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + "github.com/cbomkit/cbomkit-theia/scanner/pem" + "github.com/cbomkit/cbomkit-theia/scanner/plugins" cdx "github.com/CycloneDX/cyclonedx-go" "github.com/zricethezav/gitleaks/v8/detect" diff --git a/scanner/plugins/secrets/secrets_test.go b/scanner/plugins/secrets/secrets_test.go index df5ecbd..37dd359 100644 --- a/scanner/plugins/secrets/secrets_test.go +++ b/scanner/plugins/secrets/secrets_test.go @@ -18,7 +18,7 @@ package secrets import ( cdx "github.com/CycloneDX/cyclonedx-go" - "github.com/IBM/cbomkit-theia/provider/cyclonedx" + "github.com/cbomkit/cbomkit-theia/provider/cyclonedx" "github.com/stretchr/testify/assert" "github.com/zricethezav/gitleaks/v8/detect" "os" diff --git a/scanner/scanner.go b/scanner/scanner.go index 75c8b09..fdba230 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -22,13 +22,14 @@ import ( "os" "slices" - "github.com/IBM/cbomkit-theia/provider/cyclonedx" - "github.com/IBM/cbomkit-theia/provider/filesystem" - pluginpackage "github.com/IBM/cbomkit-theia/scanner/plugins" - "github.com/IBM/cbomkit-theia/scanner/plugins/certificates" - "github.com/IBM/cbomkit-theia/scanner/plugins/javasecurity" - "github.com/IBM/cbomkit-theia/scanner/plugins/opensslconf" - "github.com/IBM/cbomkit-theia/scanner/plugins/secrets" + "github.com/cbomkit/cbomkit-theia/provider/cyclonedx" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + pluginpackage "github.com/cbomkit/cbomkit-theia/scanner/plugins" + "github.com/cbomkit/cbomkit-theia/scanner/plugins/certificates" + "github.com/cbomkit/cbomkit-theia/scanner/plugins/javasecurity" + "github.com/cbomkit/cbomkit-theia/scanner/plugins/opensslconf" + "github.com/cbomkit/cbomkit-theia/scanner/plugins/problematicca" + "github.com/cbomkit/cbomkit-theia/scanner/plugins/secrets" log "github.com/sirupsen/logrus" "github.com/spf13/viper" @@ -56,10 +57,11 @@ func GetAllPluginNames() []string { func GetAllPluginConstructors() map[string]pluginpackage.PluginConstructor { return map[string]pluginpackage.PluginConstructor{ - "certificates": certificates.NewCertificatePlugin, - "javasecurity": javasecurity.NewJavaSecurityPlugin, - "secrets": secrets.NewSecretsPlugin, - "opensslconf": opensslconf.NewOpenSSLConfPlugin, + "certificates": certificates.NewCertificatePlugin, + "javasecurity": javasecurity.NewJavaSecurityPlugin, + "secrets": secrets.NewSecretsPlugin, + "opensslconf": opensslconf.NewOpenSSLConfPlugin, + "problematicca": problematicca.NewProblematicCAPlugin, } } diff --git a/scanner/x509/x509.go b/scanner/x509/x509.go index 85b9cba..9248a5d 100644 --- a/scanner/x509/x509.go +++ b/scanner/x509/x509.go @@ -23,8 +23,8 @@ import ( "strings" "time" - "github.com/IBM/cbomkit-theia/scanner/errors" - "github.com/IBM/cbomkit-theia/scanner/key" + "github.com/cbomkit/cbomkit-theia/scanner/errors" + "github.com/cbomkit/cbomkit-theia/scanner/key" "github.com/google/uuid" From 8b2f24aecb2ac4638c59d2f2dcbd35dc806fb5ac Mon Sep 17 00:00:00 2001 From: Cameron Banowsky Date: Sat, 6 Dec 2025 15:39:23 -0800 Subject: [PATCH 2/2] Add PQC Readiness Assessment Plugin Implements a comprehensive Post-Quantum Cryptography (PQC) readiness assessment plugin that analyzes cryptographic assets in CBOMs for quantum vulnerability and migration readiness. ## Features ### Quantum Vulnerability Classification - Classifies algorithms by quantum threat model: - `quantum-vulnerable`: RSA, ECDSA, DSA, DH, ECDH (Shor's algorithm) - `quantum-partially-secure`: AES-128/192, 3DES (Grover's algorithm) - `quantum-resistant`: SHA-256/384/512, SHA3, AES-256, ChaCha20 - `quantum-safe`: ML-KEM, ML-DSA, SLH-DSA (NIST PQC standards) - `hybrid-transitional`: X25519Kyber768, ECDH+ML-KEM hybrids ### PQC Algorithm Detection - Detects NIST standardized PQC algorithms by OID and name patterns: - ML-KEM (FIPS 203): ML-KEM-512/768/1024 - ML-DSA (FIPS 204): ML-DSA-44/65/87 - SLH-DSA (FIPS 205): All SHA2/SHAKE variants - Supports hybrid scheme detection (X25519Kyber768, etc.) ### HNDL Risk Scoring - Calculates "Harvest Now, Decrypt Later" risk scores (0-10) - Factors: data sensitivity, crypto lifetime, vulnerability level, exposure - Categorizes risk as critical/high/medium/low ### Compliance Tracking - CNSA 2.0 timeline compliance (2025-2033 deadlines by category) - NIST SP 800-131A Rev 2 compliance checking - Custom organizational deadline support ### Migration Guidance - Recommends PQC replacement algorithms - Identifies migration path (direct, hybrid-transition) - Flags blocking factors (CA certificates, HSM dependencies) ### Filesystem Scanning - Scans OpenSSL configs for PQC cipher suite settings - Detects PQC-enabled TLS configurations - Identifies PQC key files ## Files Added - scanner/plugins/pqcreadiness/*.go (11 source files) - scanner/plugins/pqcreadiness/algorithms.json (classical algorithm DB) - scanner/plugins/pqcreadiness/pqc_oids.json (PQC OID database) - scanner/plugins/pqcreadiness/pqcreadiness_test.go (16 unit tests) ## Usage Enable via: --plugins=pqcreadiness ## Property Schema Components are enriched with properties: - theia:pqc:quantum-status - theia:pqc:quantum-threat - theia:pqc:classical-security-bits - theia:pqc:quantum-security-bits - theia:pqc:nist-quantum-level - theia:pqc:hndl-risk-score - theia:pqc:hndl-risk-category - theia:pqc:recommended-replacement - theia:pqc:migration-path - theia:pqc:compliance:* (per-framework status) Signed-off-by: Cameron Banowsky --- scanner/plugins/pqcreadiness/algorithms.json | 194 +++++++ scanner/plugins/pqcreadiness/compliance.go | 403 ++++++++++++++ scanner/plugins/pqcreadiness/config.go | 255 +++++++++ scanner/plugins/pqcreadiness/database.go | 378 +++++++++++++ scanner/plugins/pqcreadiness/migration.go | 326 +++++++++++ scanner/plugins/pqcreadiness/pqc_detection.go | 239 ++++++++ scanner/plugins/pqcreadiness/pqc_oids.json | 206 +++++++ scanner/plugins/pqcreadiness/pqc_scanner.go | 374 +++++++++++++ scanner/plugins/pqcreadiness/pqcreadiness.go | 191 +++++++ .../plugins/pqcreadiness/pqcreadiness_test.go | 498 +++++++++++++++++ .../pqcreadiness/quantum_vulnerability.go | 518 ++++++++++++++++++ scanner/plugins/pqcreadiness/risk_scoring.go | 484 ++++++++++++++++ .../plugins/pqcreadiness/security_levels.go | 204 +++++++ scanner/scanner.go | 2 + 14 files changed, 4272 insertions(+) create mode 100644 scanner/plugins/pqcreadiness/algorithms.json create mode 100644 scanner/plugins/pqcreadiness/compliance.go create mode 100644 scanner/plugins/pqcreadiness/config.go create mode 100644 scanner/plugins/pqcreadiness/database.go create mode 100644 scanner/plugins/pqcreadiness/migration.go create mode 100644 scanner/plugins/pqcreadiness/pqc_detection.go create mode 100644 scanner/plugins/pqcreadiness/pqc_oids.json create mode 100644 scanner/plugins/pqcreadiness/pqc_scanner.go create mode 100644 scanner/plugins/pqcreadiness/pqcreadiness.go create mode 100644 scanner/plugins/pqcreadiness/pqcreadiness_test.go create mode 100644 scanner/plugins/pqcreadiness/quantum_vulnerability.go create mode 100644 scanner/plugins/pqcreadiness/risk_scoring.go create mode 100644 scanner/plugins/pqcreadiness/security_levels.go diff --git a/scanner/plugins/pqcreadiness/algorithms.json b/scanner/plugins/pqcreadiness/algorithms.json new file mode 100644 index 0000000..e57056c --- /dev/null +++ b/scanner/plugins/pqcreadiness/algorithms.json @@ -0,0 +1,194 @@ +{ + "version": "1.0.0", + "lastUpdated": "2025-01-15", + "classicalAlgorithms": { + "RSA": { + "family": "asymmetric", + "primitive": "pke", + "quantumStatus": "quantum-vulnerable", + "primaryThreat": "shors-algorithm", + "keySizeMapping": { + "1024": {"classicalBits": 80, "quantumBits": 0, "nistLevel": 0}, + "2048": {"classicalBits": 112, "quantumBits": 0, "nistLevel": 0}, + "3072": {"classicalBits": 128, "quantumBits": 0, "nistLevel": 0}, + "4096": {"classicalBits": 152, "quantumBits": 0, "nistLevel": 0} + }, + "oids": ["1.2.840.113549.1.1.1"], + "recommendedReplacements": ["ML-KEM-768", "ML-KEM-1024"] + }, + "ECDSA": { + "family": "asymmetric", + "primitive": "signature", + "quantumStatus": "quantum-vulnerable", + "primaryThreat": "shors-algorithm", + "curveMapping": { + "P-256": {"classicalBits": 128, "quantumBits": 0, "nistLevel": 0}, + "P-384": {"classicalBits": 192, "quantumBits": 0, "nistLevel": 0}, + "P-521": {"classicalBits": 256, "quantumBits": 0, "nistLevel": 0} + }, + "oids": ["1.2.840.10045.2.1", "1.2.840.10045.4.3.2", "1.2.840.10045.4.3.3", "1.2.840.10045.4.3.4"], + "recommendedReplacements": ["ML-DSA-65", "ML-DSA-87", "SLH-DSA-SHA2-128f"] + }, + "DSA": { + "family": "asymmetric", + "primitive": "signature", + "quantumStatus": "quantum-vulnerable", + "primaryThreat": "shors-algorithm", + "keySizeMapping": { + "1024": {"classicalBits": 80, "quantumBits": 0, "nistLevel": 0}, + "2048": {"classicalBits": 112, "quantumBits": 0, "nistLevel": 0}, + "3072": {"classicalBits": 128, "quantumBits": 0, "nistLevel": 0} + }, + "oids": ["1.2.840.10040.4.1"], + "recommendedReplacements": ["ML-DSA-65", "ML-DSA-87"] + }, + "DH": { + "family": "asymmetric", + "primitive": "key-agreement", + "quantumStatus": "quantum-vulnerable", + "primaryThreat": "shors-algorithm", + "keySizeMapping": { + "1024": {"classicalBits": 80, "quantumBits": 0, "nistLevel": 0}, + "2048": {"classicalBits": 112, "quantumBits": 0, "nistLevel": 0}, + "3072": {"classicalBits": 128, "quantumBits": 0, "nistLevel": 0}, + "4096": {"classicalBits": 152, "quantumBits": 0, "nistLevel": 0} + }, + "oids": ["1.2.840.113549.1.3.1"], + "recommendedReplacements": ["ML-KEM-768", "X25519Kyber768"] + }, + "ECDH": { + "family": "asymmetric", + "primitive": "key-agreement", + "quantumStatus": "quantum-vulnerable", + "primaryThreat": "shors-algorithm", + "curveMapping": { + "P-256": {"classicalBits": 128, "quantumBits": 0, "nistLevel": 0}, + "P-384": {"classicalBits": 192, "quantumBits": 0, "nistLevel": 0}, + "P-521": {"classicalBits": 256, "quantumBits": 0, "nistLevel": 0}, + "X25519": {"classicalBits": 128, "quantumBits": 0, "nistLevel": 0} + }, + "oids": ["1.2.840.10045.2.1", "1.3.101.110"], + "recommendedReplacements": ["ML-KEM-768", "X25519Kyber768"] + }, + "Ed25519": { + "family": "asymmetric", + "primitive": "signature", + "quantumStatus": "quantum-vulnerable", + "primaryThreat": "shors-algorithm", + "fixedSecurityLevel": {"classicalBits": 128, "quantumBits": 0, "nistLevel": 0}, + "oids": ["1.3.101.112"], + "recommendedReplacements": ["ML-DSA-65", "SLH-DSA-SHA2-128f"] + }, + "Ed448": { + "family": "asymmetric", + "primitive": "signature", + "quantumStatus": "quantum-vulnerable", + "primaryThreat": "shors-algorithm", + "fixedSecurityLevel": {"classicalBits": 224, "quantumBits": 0, "nistLevel": 0}, + "oids": ["1.3.101.113"], + "recommendedReplacements": ["ML-DSA-87", "SLH-DSA-SHA2-192f"] + }, + "AES": { + "family": "symmetric", + "primitive": "block-cipher", + "quantumStatus": "quantum-partially-secure", + "primaryThreat": "grovers-algorithm", + "keySizeMapping": { + "128": {"classicalBits": 128, "quantumBits": 64, "nistLevel": 1}, + "192": {"classicalBits": 192, "quantumBits": 96, "nistLevel": 2}, + "256": {"classicalBits": 256, "quantumBits": 128, "nistLevel": 3} + }, + "oids": ["2.16.840.1.101.3.4.1.1", "2.16.840.1.101.3.4.1.2", "2.16.840.1.101.3.4.1.41", "2.16.840.1.101.3.4.1.42"], + "recommendedReplacements": ["AES-256"], + "notes": "AES-256 provides ~128-bit quantum security via Grover's algorithm" + }, + "ChaCha20": { + "family": "symmetric", + "primitive": "stream-cipher", + "quantumStatus": "quantum-resistant", + "primaryThreat": "grovers-algorithm", + "fixedSecurityLevel": {"classicalBits": 256, "quantumBits": 128, "nistLevel": 3}, + "oids": [], + "recommendedReplacements": [] + }, + "3DES": { + "family": "symmetric", + "primitive": "block-cipher", + "quantumStatus": "quantum-partially-secure", + "primaryThreat": "grovers-algorithm", + "fixedSecurityLevel": {"classicalBits": 112, "quantumBits": 56, "nistLevel": 0}, + "oids": ["1.2.840.113549.3.7"], + "recommendedReplacements": ["AES-256"], + "notes": "3DES is deprecated; quantum security is theoretical as algorithm is obsolete" + }, + "SHA-256": { + "family": "hash", + "primitive": "hash", + "quantumStatus": "quantum-resistant", + "primaryThreat": "grovers-algorithm", + "fixedSecurityLevel": {"classicalBits": 128, "quantumBits": 128, "nistLevel": 1}, + "oids": ["2.16.840.1.101.3.4.2.1"], + "notes": "Hash functions maintain collision resistance under Grover's algorithm" + }, + "SHA-384": { + "family": "hash", + "primitive": "hash", + "quantumStatus": "quantum-resistant", + "primaryThreat": "grovers-algorithm", + "fixedSecurityLevel": {"classicalBits": 192, "quantumBits": 192, "nistLevel": 3}, + "oids": ["2.16.840.1.101.3.4.2.2"] + }, + "SHA-512": { + "family": "hash", + "primitive": "hash", + "quantumStatus": "quantum-resistant", + "primaryThreat": "grovers-algorithm", + "fixedSecurityLevel": {"classicalBits": 256, "quantumBits": 256, "nistLevel": 5}, + "oids": ["2.16.840.1.101.3.4.2.3"] + }, + "SHA3-256": { + "family": "hash", + "primitive": "hash", + "quantumStatus": "quantum-resistant", + "primaryThreat": "grovers-algorithm", + "fixedSecurityLevel": {"classicalBits": 128, "quantumBits": 128, "nistLevel": 1}, + "oids": ["2.16.840.1.101.3.4.2.8"] + }, + "SHA3-384": { + "family": "hash", + "primitive": "hash", + "quantumStatus": "quantum-resistant", + "primaryThreat": "grovers-algorithm", + "fixedSecurityLevel": {"classicalBits": 192, "quantumBits": 192, "nistLevel": 3}, + "oids": ["2.16.840.1.101.3.4.2.9"] + }, + "SHA3-512": { + "family": "hash", + "primitive": "hash", + "quantumStatus": "quantum-resistant", + "primaryThreat": "grovers-algorithm", + "fixedSecurityLevel": {"classicalBits": 256, "quantumBits": 256, "nistLevel": 5}, + "oids": ["2.16.840.1.101.3.4.2.10"] + }, + "SHA-1": { + "family": "hash", + "primitive": "hash", + "quantumStatus": "quantum-resistant", + "primaryThreat": "grovers-algorithm", + "fixedSecurityLevel": {"classicalBits": 80, "quantumBits": 80, "nistLevel": 0}, + "oids": ["1.3.14.3.2.26"], + "recommendedReplacements": ["SHA-256", "SHA3-256"], + "notes": "SHA-1 is deprecated for cryptographic use" + }, + "MD5": { + "family": "hash", + "primitive": "hash", + "quantumStatus": "quantum-resistant", + "primaryThreat": "grovers-algorithm", + "fixedSecurityLevel": {"classicalBits": 64, "quantumBits": 64, "nistLevel": 0}, + "oids": ["1.2.840.113549.2.5"], + "recommendedReplacements": ["SHA-256", "SHA3-256"], + "notes": "MD5 is broken and should not be used for security purposes" + } + } +} diff --git a/scanner/plugins/pqcreadiness/compliance.go b/scanner/plugins/pqcreadiness/compliance.go new file mode 100644 index 0000000..f307b46 --- /dev/null +++ b/scanner/plugins/pqcreadiness/compliance.go @@ -0,0 +1,403 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package pqcreadiness + +import ( + "fmt" + "strconv" + "strings" + "time" + + cdx "github.com/CycloneDX/cyclonedx-go" +) + +// ComplianceResult contains the compliance status across multiple frameworks +type ComplianceResult struct { + Frameworks []FrameworkCompliance + EarliestDeadline *time.Time + MostUrgentFramework string + OverallStatus string // "compliant", "non-compliant", "transition-needed" +} + +// FrameworkCompliance represents compliance status for a single framework +type FrameworkCompliance struct { + Framework string // "CNSA 2.0", "NIST SP 800-131A", "Custom" + Status string // "compliant", "non-compliant", "transition-needed", "deprecated" + Deadline *time.Time // Applicable deadline + DaysRemaining int // Days until deadline (-1 if no deadline) + Category string // Category within the framework + Requirements string // Description of requirements + Violations []string // Specific violations found +} + +// checkComplianceTimelines checks compliance against multiple frameworks +func (plugin *Plugin) checkComplianceTimelines(component *cdx.Component) *ComplianceResult { + result := &ComplianceResult{ + Frameworks: []FrameworkCompliance{}, + OverallStatus: "compliant", + } + + // Check CNSA 2.0 compliance + if plugin.config.Compliance.CNSA20.Enabled { + cnsa := plugin.checkCNSA20Compliance(component) + result.Frameworks = append(result.Frameworks, cnsa) + result.updateOverallStatus(cnsa) + } + + // Check NIST SP 800-131A compliance + if plugin.config.Compliance.NIST.Enabled { + nist := plugin.checkNIST131ACompliance(component) + result.Frameworks = append(result.Frameworks, nist) + result.updateOverallStatus(nist) + } + + // Check custom organizational deadlines + if plugin.config.Compliance.Custom.Enabled { + for _, custom := range plugin.config.Compliance.Custom.Deadlines { + customResult := plugin.checkCustomCompliance(component, custom) + if customResult != nil { + result.Frameworks = append(result.Frameworks, *customResult) + result.updateOverallStatus(*customResult) + } + } + } + + // Find earliest deadline + result.findEarliestDeadline() + + return result +} + +// updateOverallStatus updates the overall compliance status +func (result *ComplianceResult) updateOverallStatus(framework FrameworkCompliance) { + switch framework.Status { + case "non-compliant": + result.OverallStatus = "non-compliant" + case "transition-needed": + if result.OverallStatus == "compliant" { + result.OverallStatus = "transition-needed" + } + case "deprecated": + if result.OverallStatus == "compliant" { + result.OverallStatus = "deprecated" + } + } +} + +// findEarliestDeadline finds the most urgent deadline +func (result *ComplianceResult) findEarliestDeadline() { + for _, fw := range result.Frameworks { + if fw.Deadline != nil { + if result.EarliestDeadline == nil || fw.Deadline.Before(*result.EarliestDeadline) { + result.EarliestDeadline = fw.Deadline + result.MostUrgentFramework = fw.Framework + } + } + } +} + +// checkCNSA20Compliance checks compliance with CNSA 2.0 requirements +func (plugin *Plugin) checkCNSA20Compliance(component *cdx.Component) FrameworkCompliance { + result := FrameworkCompliance{ + Framework: "CNSA 2.0", + Status: "compliant", + DaysRemaining: -1, + Violations: []string{}, + } + + // Determine component category + category := plugin.determineCNSA20Category(component) + result.Category = category + + // Get applicable deadline + var deadlineStr string + switch category { + case "software-signing": + deadlineStr = plugin.config.Compliance.CNSA20.SoftwareSigningDeadline + result.Requirements = "Software/firmware signing must use PQC by deadline" + case "firmware": + deadlineStr = plugin.config.Compliance.CNSA20.FirmwareDeadline + result.Requirements = "Firmware must use PQC by deadline" + case "networking": + deadlineStr = plugin.config.Compliance.CNSA20.NetworkingDeadline + result.Requirements = "Traditional networking equipment must use PQC by deadline" + case "os-infrastructure": + deadlineStr = plugin.config.Compliance.CNSA20.OSDeadline + result.Requirements = "Operating systems and infrastructure must use PQC by deadline" + default: + deadlineStr = plugin.config.Compliance.CNSA20.NetworkingDeadline + result.Requirements = "Default to networking timeline" + } + + if deadline, err := ParseDeadline(deadlineStr); err == nil { + result.Deadline = deadline + result.DaysRemaining = DaysUntil(deadline) + } + + // Check if component uses quantum-vulnerable algorithms + quantumStatus := getQuantumStatus(component) + switch quantumStatus { + case QuantumVulnerable: + result.Status = "non-compliant" + result.Violations = append(result.Violations, "Uses quantum-vulnerable algorithm") + case QuantumPartiallySecure: + result.Status = "transition-needed" + result.Violations = append(result.Violations, "May need key size upgrade for Grover resistance") + case HybridTransitional: + result.Status = "transition-needed" + result.Violations = append(result.Violations, "Using hybrid scheme; full PQC required by deadline") + case QuantumSafe: + result.Status = "compliant" + } + + // Check specific CNSA 2.0 algorithm requirements + algName := strings.ToUpper(extractAlgorithmName(component)) + + // RSA is not approved in CNSA 2.0 + if strings.Contains(algName, "RSA") { + result.Status = "non-compliant" + result.Violations = append(result.Violations, "RSA not approved in CNSA 2.0") + } + + // ECDSA/ECDH with P-384 allowed during transition + if strings.Contains(algName, "ECDSA") || strings.Contains(algName, "ECDH") { + curve := extractCurve(component) + if curve != "P-384" { + result.Violations = append(result.Violations, "CNSA 2.0 requires P-384 for ECC during transition") + result.Status = "non-compliant" + } else { + result.Status = "transition-needed" + result.Violations = append(result.Violations, "P-384 allowed only during transition period") + } + } + + // Check hash requirements + if strings.Contains(algName, "SHA-1") || strings.Contains(algName, "SHA1") { + result.Status = "non-compliant" + result.Violations = append(result.Violations, "SHA-1 not approved in CNSA 2.0") + } + if strings.Contains(algName, "SHA-256") || strings.Contains(algName, "SHA256") { + if !strings.Contains(algName, "SHA-384") && !strings.Contains(algName, "SHA-512") { + result.Violations = append(result.Violations, "CNSA 2.0 requires SHA-384 or SHA-512") + } + } + + return result +} + +// determineCNSA20Category determines which CNSA 2.0 category applies +func (plugin *Plugin) determineCNSA20Category(component *cdx.Component) string { + if component.Evidence != nil && component.Evidence.Occurrences != nil { + for _, occ := range *component.Evidence.Occurrences { + path := strings.ToLower(occ.Location) + + if strings.Contains(path, "signing") || strings.Contains(path, "codesign") || + strings.Contains(path, "apksign") || strings.Contains(path, "authenticode") { + return "software-signing" + } + if strings.Contains(path, "firmware") || strings.Contains(path, "uefi") || + strings.Contains(path, "bios") { + return "firmware" + } + if strings.Contains(path, "nginx") || strings.Contains(path, "apache") || + strings.Contains(path, "ssl") || strings.Contains(path, "tls") { + return "networking" + } + } + } + + // Default based on component type + if component.CryptoProperties != nil { + if component.CryptoProperties.AssetType == cdx.CryptoAssetTypeProtocol { + return "networking" + } + } + + return "networking" // Default +} + +// checkNIST131ACompliance checks compliance with NIST SP 800-131A Rev 2 +func (plugin *Plugin) checkNIST131ACompliance(component *cdx.Component) FrameworkCompliance { + result := FrameworkCompliance{ + Framework: "NIST SP 800-131A", + Status: "compliant", + DaysRemaining: -1, + Violations: []string{}, + Requirements: "Transitioning the Use of Cryptographic Algorithms and Key Lengths", + } + + algName := strings.ToUpper(extractAlgorithmName(component)) + keySize := extractKeySize(component) + + // SHA-1 for digital signatures is disallowed + if strings.Contains(algName, "SHA-1") || strings.Contains(algName, "SHA1") { + if strings.Contains(algName, "SIGN") || strings.Contains(algName, "RSA") || + strings.Contains(algName, "DSA") || strings.Contains(algName, "ECDSA") { + result.Status = "non-compliant" + result.Violations = append(result.Violations, "SHA-1 disallowed for digital signatures") + } + } + + // RSA key sizes + if strings.Contains(algName, "RSA") { + if keySize > 0 && keySize < 2048 { + result.Status = "non-compliant" + result.Violations = append(result.Violations, fmt.Sprintf("RSA key size %d < 2048 bits disallowed", keySize)) + } + } + + // DSA key sizes + if strings.Contains(algName, "DSA") && !strings.Contains(algName, "ECDSA") { + if keySize > 0 && keySize < 2048 { + result.Status = "non-compliant" + result.Violations = append(result.Violations, fmt.Sprintf("DSA key size %d < 2048 bits disallowed", keySize)) + } + } + + // 3DES is deprecated + if strings.Contains(algName, "3DES") || strings.Contains(algName, "TRIPLE") || + strings.Contains(algName, "DES-EDE") { + result.Status = "deprecated" + result.Violations = append(result.Violations, "3DES deprecated as of 2023") + } + + // MD5 is disallowed + if strings.Contains(algName, "MD5") { + result.Status = "non-compliant" + result.Violations = append(result.Violations, "MD5 disallowed for cryptographic purposes") + } + + // Check classical security bits + if component.Properties != nil { + for _, prop := range *component.Properties { + if prop.Name == "theia:pqc:classical-security-bits" { + if bits, err := strconv.Atoi(prop.Value); err == nil && bits < 112 { + result.Status = "non-compliant" + result.Violations = append(result.Violations, fmt.Sprintf("Security strength %d bits < 112 bits minimum", bits)) + } + } + } + } + + return result +} + +// checkCustomCompliance checks compliance with custom organizational deadlines +func (plugin *Plugin) checkCustomCompliance(component *cdx.Component, custom CustomDeadline) *FrameworkCompliance { + algName := strings.ToUpper(extractAlgorithmName(component)) + + // Check if this custom deadline applies to this algorithm + applies := false + for _, alg := range custom.AppliesTo { + if strings.Contains(algName, strings.ToUpper(alg)) { + applies = true + break + } + } + + if !applies { + return nil + } + + result := &FrameworkCompliance{ + Framework: "Custom: " + custom.Name, + Status: "compliant", + DaysRemaining: -1, + Violations: []string{}, + Requirements: custom.Name, + } + + if deadline, err := ParseDeadline(custom.Deadline); err == nil { + result.Deadline = deadline + result.DaysRemaining = DaysUntil(deadline) + } + + // Check quantum status + quantumStatus := getQuantumStatus(component) + if quantumStatus == QuantumVulnerable { + result.Status = "non-compliant" + result.Violations = append(result.Violations, "Quantum-vulnerable algorithm must be migrated by deadline") + } else if quantumStatus == HybridTransitional { + result.Status = "transition-needed" + } + + return result +} + +// enrichWithCompliance adds compliance properties to a component +func (plugin *Plugin) enrichWithCompliance(component *cdx.Component, compliance *ComplianceResult) { + if component.Properties == nil { + component.Properties = &[]cdx.Property{} + } + + props := []cdx.Property{} + + // Overall status + props = append(props, cdx.Property{ + Name: "theia:pqc:compliance:overall-status", + Value: compliance.OverallStatus, + }) + + // Earliest deadline + if compliance.EarliestDeadline != nil { + props = append(props, cdx.Property{ + Name: "theia:pqc:compliance:earliest-deadline", + Value: compliance.EarliestDeadline.Format("2006-01-02"), + }) + props = append(props, cdx.Property{ + Name: "theia:pqc:compliance:days-until-deadline", + Value: fmt.Sprintf("%d", DaysUntil(compliance.EarliestDeadline)), + }) + props = append(props, cdx.Property{ + Name: "theia:pqc:compliance:most-urgent-framework", + Value: compliance.MostUrgentFramework, + }) + } + + // Per-framework details + for _, fw := range compliance.Frameworks { + prefix := "theia:pqc:compliance:" + strings.ToLower(strings.ReplaceAll(fw.Framework, " ", "-")) + + props = append(props, cdx.Property{ + Name: prefix + ":status", + Value: fw.Status, + }) + + if fw.Category != "" { + props = append(props, cdx.Property{ + Name: prefix + ":category", + Value: fw.Category, + }) + } + + if fw.Deadline != nil { + props = append(props, cdx.Property{ + Name: prefix + ":deadline", + Value: fw.Deadline.Format("2006-01-02"), + }) + } + + if len(fw.Violations) > 0 { + props = append(props, cdx.Property{ + Name: prefix + ":violations", + Value: strings.Join(fw.Violations, "; "), + }) + } + } + + *component.Properties = append(*component.Properties, props...) +} diff --git a/scanner/plugins/pqcreadiness/config.go b/scanner/plugins/pqcreadiness/config.go new file mode 100644 index 0000000..f1060ae --- /dev/null +++ b/scanner/plugins/pqcreadiness/config.go @@ -0,0 +1,255 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package pqcreadiness + +import ( + "os" + "path/filepath" + "time" + + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" +) + +// PQCConfig contains configuration for the PQC Readiness plugin +type PQCConfig struct { + Features FeatureFlags `yaml:"features"` + RiskWeights RiskWeights `yaml:"risk_weights"` + SensitivityRules []SensitivityRule `yaml:"sensitivity_rules"` + Compliance ComplianceConfig `yaml:"compliance"` + CustomPQCAlgorithms []CustomPQCAlgorithm `yaml:"custom_pqc_algorithms"` + ExposureRules ExposureRules `yaml:"exposure_rules"` +} + +// FeatureFlags controls which features are enabled +type FeatureFlags struct { + VulnerabilityClassification bool `yaml:"vulnerability_classification"` + PQCDetection bool `yaml:"pqc_detection"` + RiskScoring bool `yaml:"risk_scoring"` + SecurityLevelCalculation bool `yaml:"security_level_calculation"` + MigrationGuidance bool `yaml:"migration_guidance"` + ComplianceTracking bool `yaml:"compliance_tracking"` +} + +// RiskWeights defines the weights for risk score calculation +type RiskWeights struct { + DataSensitivity float64 `yaml:"data_sensitivity"` + CryptoLifetime float64 `yaml:"crypto_lifetime"` + VulnerabilityLevel float64 `yaml:"vulnerability_level"` + ExposureLevel float64 `yaml:"exposure_level"` +} + +// SensitivityRule defines rules for inferring data sensitivity +type SensitivityRule struct { + Pattern string `yaml:"pattern"` + KeyUsageContains string `yaml:"key_usage_contains"` + Sensitivity float64 `yaml:"sensitivity"` +} + +// ComplianceConfig contains compliance framework configurations +type ComplianceConfig struct { + CNSA20 CNSA20Config `yaml:"cnsa_2_0"` + NIST NISTConfig `yaml:"nist"` + Custom CustomConfig `yaml:"custom"` +} + +// CNSA20Config contains CNSA 2.0 compliance deadlines +type CNSA20Config struct { + Enabled bool `yaml:"enabled"` + SoftwareSigningDeadline string `yaml:"software_signing_deadline"` + FirmwareDeadline string `yaml:"firmware_deadline"` + NetworkingDeadline string `yaml:"networking_deadline"` + OSDeadline string `yaml:"os_deadline"` +} + +// NISTConfig contains NIST SP 800-131A compliance settings +type NISTConfig struct { + Enabled bool `yaml:"enabled"` +} + +// CustomConfig contains custom organizational compliance settings +type CustomConfig struct { + Enabled bool `yaml:"enabled"` + Deadlines []CustomDeadline `yaml:"deadlines"` +} + +// CustomDeadline represents a custom organizational deadline +type CustomDeadline struct { + Name string `yaml:"name"` + Deadline string `yaml:"deadline"` + AppliesTo []string `yaml:"applies_to"` +} + +// CustomPQCAlgorithm allows users to define custom PQC algorithms +type CustomPQCAlgorithm struct { + Name string `yaml:"name"` + Family string `yaml:"family"` + OIDs []string `yaml:"oids"` + NISTLevel int `yaml:"nist_level"` + Primitive string `yaml:"primitive"` +} + +// ExposureRules defines rules for determining exposure level +type ExposureRules struct { + NetworkFacingIndicators []string `yaml:"network_facing_indicators"` + InternalIndicators []string `yaml:"internal_indicators"` +} + +// loadConfig loads the PQC configuration from the user's config directory +func loadConfig() (*PQCConfig, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + configPath := filepath.Join(homeDir, ".cbomkit-theia", "pqc_config.yaml") + + // Check if file exists + if _, err := os.Stat(configPath); os.IsNotExist(err) { + log.Debug("No PQC config file found, using defaults") + return getDefaultConfig(), nil + } + + // Read file + data, err := os.ReadFile(configPath) + if err != nil { + return nil, err + } + + // Parse YAML + var wrapper struct { + PQCReadiness PQCConfig `yaml:"pqc_readiness"` + } + if err := yaml.Unmarshal(data, &wrapper); err != nil { + return nil, err + } + + // Merge with defaults + config := getDefaultConfig() + mergeConfig(config, &wrapper.PQCReadiness) + + log.Info("Loaded PQC configuration from file") + return config, nil +} + +// getDefaultConfig returns the default configuration +func getDefaultConfig() *PQCConfig { + return &PQCConfig{ + Features: FeatureFlags{ + VulnerabilityClassification: true, + PQCDetection: true, + RiskScoring: true, + SecurityLevelCalculation: true, + MigrationGuidance: true, + ComplianceTracking: true, + }, + RiskWeights: RiskWeights{ + DataSensitivity: 0.30, + CryptoLifetime: 0.25, + VulnerabilityLevel: 0.30, + ExposureLevel: 0.15, + }, + SensitivityRules: []SensitivityRule{ + {Pattern: "*.gov.*", Sensitivity: 1.0}, + {Pattern: "*healthcare*", Sensitivity: 0.9}, + {Pattern: "*financial*", Sensitivity: 0.9}, + {Pattern: "*pii*", Sensitivity: 0.8}, + {KeyUsageContains: "keyCertSign", Sensitivity: 0.85}, + {KeyUsageContains: "digitalSignature", Sensitivity: 0.7}, + }, + Compliance: ComplianceConfig{ + CNSA20: CNSA20Config{ + Enabled: true, + SoftwareSigningDeadline: "2025-12-31", + FirmwareDeadline: "2027-12-31", + NetworkingDeadline: "2030-12-31", + OSDeadline: "2033-12-31", + }, + NIST: NISTConfig{ + Enabled: true, + }, + Custom: CustomConfig{ + Enabled: false, + Deadlines: []CustomDeadline{}, + }, + }, + ExposureRules: ExposureRules{ + NetworkFacingIndicators: []string{ + "/etc/nginx", + "/etc/apache2", + "/etc/ssl", + "serverAuth", + }, + InternalIndicators: []string{ + "/internal", + "clientAuth", + }, + }, + } +} + +// mergeConfig merges user config with defaults +func mergeConfig(base, user *PQCConfig) { + // Only override if user has set values + if user.Features.VulnerabilityClassification || user.Features.PQCDetection || + user.Features.RiskScoring || user.Features.SecurityLevelCalculation || + user.Features.MigrationGuidance || user.Features.ComplianceTracking { + base.Features = user.Features + } + + if user.RiskWeights.DataSensitivity > 0 { + base.RiskWeights = user.RiskWeights + } + + if len(user.SensitivityRules) > 0 { + base.SensitivityRules = user.SensitivityRules + } + + if user.Compliance.CNSA20.SoftwareSigningDeadline != "" { + base.Compliance.CNSA20 = user.Compliance.CNSA20 + } + + if len(user.Compliance.Custom.Deadlines) > 0 { + base.Compliance.Custom = user.Compliance.Custom + } + + if len(user.CustomPQCAlgorithms) > 0 { + base.CustomPQCAlgorithms = user.CustomPQCAlgorithms + } + + if len(user.ExposureRules.NetworkFacingIndicators) > 0 { + base.ExposureRules = user.ExposureRules + } +} + +// ParseDeadline parses a deadline string into a time.Time +func ParseDeadline(deadline string) (*time.Time, error) { + t, err := time.Parse("2006-01-02", deadline) + if err != nil { + return nil, err + } + return &t, nil +} + +// DaysUntil calculates the number of days until a deadline +func DaysUntil(deadline *time.Time) int { + if deadline == nil { + return -1 + } + duration := time.Until(*deadline) + return int(duration.Hours() / 24) +} diff --git a/scanner/plugins/pqcreadiness/database.go b/scanner/plugins/pqcreadiness/database.go new file mode 100644 index 0000000..adb3e97 --- /dev/null +++ b/scanner/plugins/pqcreadiness/database.go @@ -0,0 +1,378 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package pqcreadiness + +import ( + _ "embed" + "encoding/json" + "regexp" + "strconv" + "strings" +) + +//go:embed algorithms.json +var algorithmsJSON []byte + +//go:embed pqc_oids.json +var pqcOIDsJSON []byte + +// AlgorithmDatabase contains the classical algorithm vulnerability mappings +type AlgorithmDatabase struct { + Version string `json:"version"` + LastUpdated string `json:"lastUpdated"` + ClassicalAlgorithms map[string]ClassicalAlgorithm `json:"classicalAlgorithms"` +} + +// ClassicalAlgorithm represents a classical cryptographic algorithm +type ClassicalAlgorithm struct { + Family string `json:"family"` + Primitive string `json:"primitive"` + QuantumStatus string `json:"quantumStatus"` + PrimaryThreat string `json:"primaryThreat"` + KeySizeMapping map[string]SecurityMapping `json:"keySizeMapping"` + CurveMapping map[string]SecurityMapping `json:"curveMapping"` + FixedSecurityLevel *SecurityMapping `json:"fixedSecurityLevel"` + OIDs []string `json:"oids"` + RecommendedReplacements []string `json:"recommendedReplacements"` + Notes string `json:"notes"` +} + +// SecurityMapping contains security level information +type SecurityMapping struct { + ClassicalBits int `json:"classicalBits"` + QuantumBits int `json:"quantumBits"` + NISTLevel int `json:"nistLevel"` +} + +// PQCOIDDatabase contains the PQC algorithm OID mappings +type PQCOIDDatabase struct { + Version string `json:"version"` + LastUpdated string `json:"lastUpdated"` + Source string `json:"source"` + PQCAlgorithms map[string]PQCAlgorithmDef `json:"pqcAlgorithms"` + HybridSchemes map[string]HybridScheme `json:"hybridSchemes"` + PQCNamePatterns []string `json:"pqcNamePatterns"` + HybridNamePatterns []string `json:"hybridNamePatterns"` + + // Compiled patterns for efficient matching + pqcPatterns []*regexp.Regexp + hybridPatterns []*regexp.Regexp + oidToAlgorithm map[string]*PQCAlgorithmInfo +} + +// PQCAlgorithmDef defines a PQC algorithm family +type PQCAlgorithmDef struct { + StandardName string `json:"standardName"` + Family string `json:"family"` + Primitive string `json:"primitive"` + BaseOID string `json:"baseOID"` + ParameterSets map[string]PQCParameterSet `json:"parameterSets"` +} + +// PQCParameterSet defines a specific parameter set +type PQCParameterSet struct { + OID string `json:"oid"` + NISTLevel int `json:"nistLevel"` + PublicKeySize int `json:"publicKeySize"` + CiphertextSize int `json:"ciphertextSize"` + SignatureSize int `json:"signatureSize"` + ClassicalBits int `json:"classicalBits"` + QuantumBits int `json:"quantumBits"` +} + +// HybridScheme defines a hybrid PQC+classical scheme +type HybridScheme struct { + DisplayName string `json:"displayName"` + Components []string `json:"components"` + OID string `json:"oid"` + NISTLevel int `json:"nistLevel"` + IsHybrid bool `json:"isHybrid"` + ClassicalComponent string `json:"classicalComponent"` + PQCComponent string `json:"pqcComponent"` + ClassicalBits int `json:"classicalBits"` + QuantumBits int `json:"quantumBits"` +} + +// PQCAlgorithmInfo contains resolved PQC algorithm information +type PQCAlgorithmInfo struct { + Name string + Family string + StandardName string + ParameterSet string + OID string + NISTLevel int + Primitive string + IsHybrid bool + ClassicalComponent string + ClassicalBits int + QuantumBits int +} + +// loadAlgorithmDatabase loads the classical algorithm database +func loadAlgorithmDatabase() (*AlgorithmDatabase, error) { + var db AlgorithmDatabase + if err := json.Unmarshal(algorithmsJSON, &db); err != nil { + return nil, err + } + return &db, nil +} + +// loadPQCOIDDatabase loads the PQC OID database +func loadPQCOIDDatabase() (*PQCOIDDatabase, error) { + var db PQCOIDDatabase + if err := json.Unmarshal(pqcOIDsJSON, &db); err != nil { + return nil, err + } + + // Compile name patterns + db.pqcPatterns = make([]*regexp.Regexp, 0, len(db.PQCNamePatterns)) + for _, pattern := range db.PQCNamePatterns { + if re, err := regexp.Compile("(?i)" + pattern); err == nil { + db.pqcPatterns = append(db.pqcPatterns, re) + } + } + + db.hybridPatterns = make([]*regexp.Regexp, 0, len(db.HybridNamePatterns)) + for _, pattern := range db.HybridNamePatterns { + if re, err := regexp.Compile("(?i)" + pattern); err == nil { + db.hybridPatterns = append(db.hybridPatterns, re) + } + } + + // Build OID lookup map + db.oidToAlgorithm = make(map[string]*PQCAlgorithmInfo) + + // Add PQC algorithms + for familyName, algDef := range db.PQCAlgorithms { + for paramSetName, paramSet := range algDef.ParameterSets { + info := &PQCAlgorithmInfo{ + Name: paramSetName, + Family: familyName, + StandardName: algDef.StandardName, + ParameterSet: paramSetName, + OID: paramSet.OID, + NISTLevel: paramSet.NISTLevel, + Primitive: algDef.Primitive, + IsHybrid: false, + ClassicalBits: paramSet.ClassicalBits, + QuantumBits: paramSet.QuantumBits, + } + db.oidToAlgorithm[paramSet.OID] = info + } + } + + // Add hybrid schemes + for name, hybrid := range db.HybridSchemes { + info := &PQCAlgorithmInfo{ + Name: hybrid.DisplayName, + Family: "hybrid", + StandardName: "", + ParameterSet: name, + OID: hybrid.OID, + NISTLevel: hybrid.NISTLevel, + Primitive: "kem", + IsHybrid: true, + ClassicalComponent: hybrid.ClassicalComponent, + ClassicalBits: hybrid.ClassicalBits, + QuantumBits: hybrid.QuantumBits, + } + db.oidToAlgorithm[hybrid.OID] = info + } + + return &db, nil +} + +// Lookup finds vulnerability information for an algorithm +func (db *AlgorithmDatabase) Lookup(algName, oid string, keySize int, curve string) *AlgorithmVulnerability { + upperName := strings.ToUpper(algName) + + // Try direct name match + for name, alg := range db.ClassicalAlgorithms { + if strings.Contains(upperName, strings.ToUpper(name)) { + return db.buildVulnerability(name, &alg, keySize, curve) + } + } + + // Try OID match + for name, alg := range db.ClassicalAlgorithms { + for _, algOID := range alg.OIDs { + if oid != "" && strings.HasPrefix(oid, algOID) { + return db.buildVulnerability(name, &alg, keySize, curve) + } + } + } + + return nil +} + +func (db *AlgorithmDatabase) buildVulnerability(name string, alg *ClassicalAlgorithm, keySize int, curve string) *AlgorithmVulnerability { + vuln := &AlgorithmVulnerability{ + AlgorithmName: name, + AlgorithmFamily: alg.Family, + QuantumStatus: QuantumVulnerabilityStatus(alg.QuantumStatus), + PrimaryThreat: QuantumThreat(alg.PrimaryThreat), + RecommendedReplacement: alg.RecommendedReplacements, + Notes: alg.Notes, + } + + // Determine security level based on key size or curve + if alg.FixedSecurityLevel != nil { + vuln.ClassicalSecurityBits = alg.FixedSecurityLevel.ClassicalBits + vuln.QuantumSecurityBits = alg.FixedSecurityLevel.QuantumBits + vuln.NISTQuantumLevel = alg.FixedSecurityLevel.NISTLevel + } else if keySize > 0 && alg.KeySizeMapping != nil { + keySizeStr := strconv.Itoa(keySize) + if mapping, ok := alg.KeySizeMapping[keySizeStr]; ok { + vuln.ClassicalSecurityBits = mapping.ClassicalBits + vuln.QuantumSecurityBits = mapping.QuantumBits + vuln.NISTQuantumLevel = mapping.NISTLevel + vuln.AlgorithmName = name + "-" + keySizeStr + } else { + // Find closest key size + vuln.ClassicalSecurityBits = db.findClosestKeySize(alg.KeySizeMapping, keySize) + } + } else if curve != "" && alg.CurveMapping != nil { + normalizedCurve := normalizeCurveName(curve) + if mapping, ok := alg.CurveMapping[normalizedCurve]; ok { + vuln.ClassicalSecurityBits = mapping.ClassicalBits + vuln.QuantumSecurityBits = mapping.QuantumBits + vuln.NISTQuantumLevel = mapping.NISTLevel + vuln.AlgorithmName = name + "-" + normalizedCurve + } + } + + return vuln +} + +func (db *AlgorithmDatabase) findClosestKeySize(mapping map[string]SecurityMapping, keySize int) int { + closest := 0 + closestDiff := int(^uint(0) >> 1) // Max int + + for keySizeStr, m := range mapping { + if size, err := strconv.Atoi(keySizeStr); err == nil { + diff := abs(size - keySize) + if diff < closestDiff { + closestDiff = diff + closest = m.ClassicalBits + } + } + } + + return closest +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +func normalizeCurveName(curve string) string { + upper := strings.ToUpper(curve) + switch upper { + case "SECP256R1", "PRIME256V1": + return "P-256" + case "SECP384R1": + return "P-384" + case "SECP521R1": + return "P-521" + default: + return upper + } +} + +// LookupByOID finds a PQC algorithm by its OID +func (db *PQCOIDDatabase) LookupByOID(oid string) *PQCAlgorithmInfo { + if info, ok := db.oidToAlgorithm[oid]; ok { + return info + } + + // Try prefix match for OID hierarchies + for storedOID, info := range db.oidToAlgorithm { + if strings.HasPrefix(oid, storedOID) || strings.HasPrefix(storedOID, oid) { + return info + } + } + + return nil +} + +// LookupByName finds a PQC algorithm by name pattern +func (db *PQCOIDDatabase) LookupByName(name string) *PQCAlgorithmInfo { + upperName := strings.ToUpper(name) + + // Check for exact parameter set matches + for _, algDef := range db.PQCAlgorithms { + for paramSetName, paramSet := range algDef.ParameterSets { + if strings.Contains(upperName, strings.ToUpper(paramSetName)) { + return &PQCAlgorithmInfo{ + Name: paramSetName, + Family: algDef.Family, + StandardName: algDef.StandardName, + ParameterSet: paramSetName, + OID: paramSet.OID, + NISTLevel: paramSet.NISTLevel, + Primitive: algDef.Primitive, + IsHybrid: false, + ClassicalBits: paramSet.ClassicalBits, + QuantumBits: paramSet.QuantumBits, + } + } + } + } + + // Check hybrid schemes + for hybridName, hybrid := range db.HybridSchemes { + if strings.Contains(upperName, strings.ToUpper(hybridName)) { + return &PQCAlgorithmInfo{ + Name: hybrid.DisplayName, + Family: "hybrid", + ParameterSet: hybridName, + OID: hybrid.OID, + NISTLevel: hybrid.NISTLevel, + Primitive: "kem", + IsHybrid: true, + ClassicalComponent: hybrid.ClassicalComponent, + ClassicalBits: hybrid.ClassicalBits, + QuantumBits: hybrid.QuantumBits, + } + } + } + + return nil +} + +// IsPQCName checks if a name matches PQC algorithm patterns +func (db *PQCOIDDatabase) IsPQCName(name string) bool { + for _, re := range db.pqcPatterns { + if re.MatchString(name) { + return true + } + } + return false +} + +// IsHybridName checks if a name matches hybrid scheme patterns +func (db *PQCOIDDatabase) IsHybridName(name string) bool { + for _, re := range db.hybridPatterns { + if re.MatchString(name) { + return true + } + } + return false +} diff --git a/scanner/plugins/pqcreadiness/migration.go b/scanner/plugins/pqcreadiness/migration.go new file mode 100644 index 0000000..439f6b2 --- /dev/null +++ b/scanner/plugins/pqcreadiness/migration.go @@ -0,0 +1,326 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package pqcreadiness + +import ( + "strings" + + cdx "github.com/CycloneDX/cyclonedx-go" +) + +// MigrationGuidance contains recommendations for migrating to PQC +type MigrationGuidance struct { + RecommendedReplacements []string + MigrationPath string // "direct", "hybrid-transition", "requires-analysis" + BlockingFactors []string + Notes string + Urgency string // "immediate", "soon", "planned", "monitor" +} + +// generateMigrationGuidance creates migration recommendations for a component +func (plugin *Plugin) generateMigrationGuidance(component *cdx.Component) *MigrationGuidance { + guidance := &MigrationGuidance{ + RecommendedReplacements: []string{}, + BlockingFactors: []string{}, + } + + // Check if already using PQC + if isPQCComponent(component) { + guidance.MigrationPath = "none-required" + guidance.Notes = "Component already uses post-quantum cryptography" + guidance.Urgency = "monitor" + return guidance + } + + // Get quantum status and existing recommendations + quantumStatus := getQuantumStatus(component) + existingReplacements := getExistingReplacements(component) + + if len(existingReplacements) > 0 { + guidance.RecommendedReplacements = existingReplacements + } else { + // Generate recommendations based on algorithm type + guidance.RecommendedReplacements = plugin.generateReplacementRecommendations(component) + } + + // Determine migration path based on quantum status + switch quantumStatus { + case QuantumVulnerable: + guidance.MigrationPath = determineVulnerableMigrationPath(component) + guidance.Urgency = "soon" + case QuantumPartiallySecure: + guidance.MigrationPath = "key-size-upgrade" + guidance.Urgency = "planned" + guidance.Notes = "Consider upgrading to larger key sizes for Grover resistance" + case HybridTransitional: + guidance.MigrationPath = "complete-transition" + guidance.Urgency = "planned" + guidance.Notes = "Currently using hybrid scheme; plan transition to pure PQC when ecosystem matures" + default: + guidance.MigrationPath = "requires-analysis" + guidance.Urgency = "monitor" + } + + // Check for blocking factors + guidance.BlockingFactors = identifyMigrationBlockers(component) + + // Adjust urgency based on blocking factors + if len(guidance.BlockingFactors) > 0 && guidance.Urgency == "soon" { + guidance.Notes = "Migration complexity increased due to: " + strings.Join(guidance.BlockingFactors, ", ") + } + + return guidance +} + +// isPQCComponent checks if component already uses PQC +func isPQCComponent(component *cdx.Component) bool { + if component.Properties == nil { + return false + } + for _, prop := range *component.Properties { + if prop.Name == "theia:pqc:is-pqc-algorithm" && prop.Value == "true" { + return true + } + if prop.Name == "theia:pqc:quantum-status" { + status := QuantumVulnerabilityStatus(prop.Value) + if status == QuantumSafe || status == HybridTransitional { + return true + } + } + } + return false +} + +// getQuantumStatus extracts the quantum status from component properties +func getQuantumStatus(component *cdx.Component) QuantumVulnerabilityStatus { + if component.Properties == nil { + return QuantumUnknown + } + for _, prop := range *component.Properties { + if prop.Name == "theia:pqc:quantum-status" { + return QuantumVulnerabilityStatus(prop.Value) + } + } + return QuantumUnknown +} + +// getExistingReplacements gets any already-identified replacements +func getExistingReplacements(component *cdx.Component) []string { + if component.Properties == nil { + return nil + } + for _, prop := range *component.Properties { + if prop.Name == "theia:pqc:recommended-replacement" { + return strings.Split(prop.Value, ",") + } + } + return nil +} + +// generateReplacementRecommendations creates PQC replacement recommendations +func (plugin *Plugin) generateReplacementRecommendations(component *cdx.Component) []string { + algName := strings.ToUpper(extractAlgorithmName(component)) + primitive := getPrimitive(component) + + // Map algorithms to recommended PQC replacements + switch { + case strings.Contains(algName, "RSA"): + if primitive == "signature" || strings.Contains(algName, "SIGN") { + return []string{"ML-DSA-65", "ML-DSA-87", "SLH-DSA-SHA2-128f"} + } + return []string{"ML-KEM-768", "ML-KEM-1024"} + + case strings.Contains(algName, "ECDSA") || strings.Contains(algName, "ED25519") || strings.Contains(algName, "ED448"): + return []string{"ML-DSA-65", "ML-DSA-87", "SLH-DSA-SHA2-128f"} + + case strings.Contains(algName, "ECDH") || strings.Contains(algName, "X25519") || strings.Contains(algName, "X448"): + return []string{"ML-KEM-768", "X25519Kyber768"} + + case strings.Contains(algName, "DH"): + return []string{"ML-KEM-768", "ML-KEM-1024"} + + case strings.Contains(algName, "DSA"): + return []string{"ML-DSA-65", "ML-DSA-87"} + + case strings.Contains(algName, "AES-128"): + return []string{"AES-256"} // Key size upgrade for Grover resistance + + default: + // Generic recommendations based on primitive + if primitive == "signature" { + return []string{"ML-DSA-65"} + } else if primitive == "key-agreement" || primitive == "kem" || primitive == "pke" { + return []string{"ML-KEM-768"} + } + } + + return []string{} +} + +// getPrimitive extracts the cryptographic primitive from component +func getPrimitive(component *cdx.Component) string { + if component.CryptoProperties != nil && component.CryptoProperties.AlgorithmProperties != nil { + // Convert CycloneDX primitive to string + return string(component.CryptoProperties.AlgorithmProperties.Primitive) + } + return "" +} + +// determineVulnerableMigrationPath determines the best migration approach for vulnerable algorithms +func determineVulnerableMigrationPath(component *cdx.Component) string { + // Check if this is a CA or root certificate (harder to migrate) + if isCAOrRoot(component) { + return "hybrid-transition" // Recommend hybrid for critical infrastructure + } + + // Check for TLS/network usage (can use hybrid for compatibility) + if isNetworkFacing(component) { + return "hybrid-transition" + } + + // For internal/application crypto, direct migration may be possible + return "direct" +} + +// isCAOrRoot checks if component is a CA or root certificate +func isCAOrRoot(component *cdx.Component) bool { + if component.Properties == nil { + return false + } + for _, prop := range *component.Properties { + if strings.Contains(prop.Name, "key-usage") && strings.Contains(prop.Value, "keyCertSign") { + return true + } + } + // Check name for CA indicators + name := strings.ToLower(component.Name) + return strings.Contains(name, "root") || strings.Contains(name, "ca") || strings.Contains(name, "authority") +} + +// isNetworkFacing checks if component is used for network/TLS +func isNetworkFacing(component *cdx.Component) bool { + if component.Evidence != nil && component.Evidence.Occurrences != nil { + for _, occ := range *component.Evidence.Occurrences { + path := strings.ToLower(occ.Location) + if strings.Contains(path, "ssl") || strings.Contains(path, "tls") || + strings.Contains(path, "nginx") || strings.Contains(path, "apache") { + return true + } + } + } + if component.Properties != nil { + for _, prop := range *component.Properties { + if strings.Contains(prop.Value, "serverAuth") || strings.Contains(prop.Value, "TLS") { + return true + } + } + } + return false +} + +// identifyMigrationBlockers identifies factors that complicate migration +func identifyMigrationBlockers(component *cdx.Component) []string { + var blockers []string + + // CA certificates are harder to migrate + if isCAOrRoot(component) { + blockers = append(blockers, "ca-certificate") + } + + // Long-validity certificates + if component.CryptoProperties != nil && component.CryptoProperties.CertificateProperties != nil { + // Already validated in risk scoring, but flag for migration + } + + // Check for HSM indicators (hardware constraints) + if component.Evidence != nil && component.Evidence.Occurrences != nil { + for _, occ := range *component.Evidence.Occurrences { + path := strings.ToLower(occ.Location) + if strings.Contains(path, "hsm") || strings.Contains(path, "pkcs11") { + blockers = append(blockers, "hsm-dependency") + } + } + } + + // Interoperability concerns for network-facing + if isNetworkFacing(component) { + blockers = append(blockers, "interoperability-requirements") + } + + return blockers +} + +// enrichWithGuidance adds migration guidance properties to a component +func (plugin *Plugin) enrichWithGuidance(component *cdx.Component, guidance *MigrationGuidance) { + if component.Properties == nil { + component.Properties = &[]cdx.Property{} + } + + props := []cdx.Property{} + + // Add recommendations if not already present + if len(guidance.RecommendedReplacements) > 0 { + existing := false + for _, prop := range *component.Properties { + if prop.Name == "theia:pqc:recommended-replacement" { + existing = true + break + } + } + if !existing { + props = append(props, cdx.Property{ + Name: "theia:pqc:recommended-replacement", + Value: strings.Join(guidance.RecommendedReplacements, ","), + }) + } + } + + // Add migration path if not already present + existing := false + for _, prop := range *component.Properties { + if prop.Name == "theia:pqc:migration-path" { + existing = true + break + } + } + if !existing && guidance.MigrationPath != "" { + props = append(props, cdx.Property{ + Name: "theia:pqc:migration-path", + Value: guidance.MigrationPath, + }) + } + + // Add urgency + if guidance.Urgency != "" { + props = append(props, cdx.Property{ + Name: "theia:pqc:migration-urgency", + Value: guidance.Urgency, + }) + } + + // Add notes if present + if guidance.Notes != "" { + props = append(props, cdx.Property{ + Name: "theia:pqc:migration-notes", + Value: guidance.Notes, + }) + } + + if len(props) > 0 { + *component.Properties = append(*component.Properties, props...) + } +} diff --git a/scanner/plugins/pqcreadiness/pqc_detection.go b/scanner/plugins/pqcreadiness/pqc_detection.go new file mode 100644 index 0000000..6e3eab2 --- /dev/null +++ b/scanner/plugins/pqcreadiness/pqc_detection.go @@ -0,0 +1,239 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package pqcreadiness + +import ( + "fmt" + "strings" + + cdx "github.com/CycloneDX/cyclonedx-go" +) + +// PQCDetectionResult contains the result of PQC algorithm detection +type PQCDetectionResult struct { + Algorithm *PQCAlgorithmInfo + DetectionMethod string // "oid", "name-pattern", "config" + Confidence float64 // 0.0-1.0 + IsHybridDeployment bool + HybridPartner string // Classical algorithm in hybrid +} + +// detectPQCAlgorithm checks if a component uses a PQC algorithm +func (plugin *Plugin) detectPQCAlgorithm(component *cdx.Component) *PQCDetectionResult { + // First try OID detection (highest confidence) + if oid := extractOID(component); oid != "" { + if info := plugin.pqcOIDDB.LookupByOID(oid); info != nil { + return &PQCDetectionResult{ + Algorithm: info, + DetectionMethod: "oid", + Confidence: 1.0, + IsHybridDeployment: info.IsHybrid, + HybridPartner: info.ClassicalComponent, + } + } + } + + // Try name-based detection + name := component.Name + if name != "" { + // Check for PQC algorithm names + if info := plugin.pqcOIDDB.LookupByName(name); info != nil { + return &PQCDetectionResult{ + Algorithm: info, + DetectionMethod: "name-pattern", + Confidence: 0.9, + IsHybridDeployment: info.IsHybrid, + HybridPartner: info.ClassicalComponent, + } + } + + // Check if name matches PQC patterns + if plugin.pqcOIDDB.IsPQCName(name) { + return &PQCDetectionResult{ + Algorithm: &PQCAlgorithmInfo{ + Name: name, + Family: "pqc-unknown", + NISTLevel: 0, + IsHybrid: plugin.pqcOIDDB.IsHybridName(name), + }, + DetectionMethod: "name-pattern", + Confidence: 0.7, + IsHybridDeployment: plugin.pqcOIDDB.IsHybridName(name), + } + } + } + + // Check crypto properties for PQC indicators + if component.CryptoProperties != nil { + if algProps := component.CryptoProperties.AlgorithmProperties; algProps != nil { + paramSet := algProps.ParameterSetIdentifier + if paramSet != "" && plugin.pqcOIDDB.IsPQCName(paramSet) { + if info := plugin.pqcOIDDB.LookupByName(paramSet); info != nil { + return &PQCDetectionResult{ + Algorithm: info, + DetectionMethod: "crypto-properties", + Confidence: 0.85, + IsHybridDeployment: info.IsHybrid, + HybridPartner: info.ClassicalComponent, + } + } + } + } + } + + return nil +} + +// enrichWithPQC adds PQC-related properties to a component +func (plugin *Plugin) enrichWithPQC(component *cdx.Component, result *PQCDetectionResult) { + if component.Properties == nil { + component.Properties = &[]cdx.Property{} + } + + props := []cdx.Property{ + { + Name: "theia:pqc:is-pqc-algorithm", + Value: "true", + }, + { + Name: "theia:pqc:algorithm-family", + Value: result.Algorithm.Family, + }, + { + Name: "theia:pqc:detection-method", + Value: result.DetectionMethod, + }, + { + Name: "theia:pqc:detection-confidence", + Value: fmt.Sprintf("%.2f", result.Confidence), + }, + } + + // Set quantum status based on whether it's hybrid or pure PQC + if result.IsHybridDeployment { + props = append(props, cdx.Property{ + Name: "theia:pqc:quantum-status", + Value: string(HybridTransitional), + }) + props = append(props, cdx.Property{ + Name: "theia:pqc:is-hybrid", + Value: "true", + }) + if result.HybridPartner != "" { + props = append(props, cdx.Property{ + Name: "theia:pqc:hybrid-classical-component", + Value: result.HybridPartner, + }) + } + } else { + props = append(props, cdx.Property{ + Name: "theia:pqc:quantum-status", + Value: string(QuantumSafe), + }) + props = append(props, cdx.Property{ + Name: "theia:pqc:is-hybrid", + Value: "false", + }) + } + + // Add standard name if available + if result.Algorithm.StandardName != "" { + props = append(props, cdx.Property{ + Name: "theia:pqc:standard", + Value: result.Algorithm.StandardName, + }) + } + + // Add NIST level + if result.Algorithm.NISTLevel > 0 { + props = append(props, cdx.Property{ + Name: "theia:pqc:nist-quantum-level", + Value: fmt.Sprintf("%d", result.Algorithm.NISTLevel), + }) + } + + // Add security bits + if result.Algorithm.ClassicalBits > 0 { + props = append(props, cdx.Property{ + Name: "theia:pqc:classical-security-bits", + Value: fmt.Sprintf("%d", result.Algorithm.ClassicalBits), + }) + } + if result.Algorithm.QuantumBits > 0 { + props = append(props, cdx.Property{ + Name: "theia:pqc:quantum-security-bits", + Value: fmt.Sprintf("%d", result.Algorithm.QuantumBits), + }) + } + + // Add quantum threat (none for PQC) + props = append(props, cdx.Property{ + Name: "theia:pqc:quantum-threat", + Value: string(ThreatNone), + }) + + *component.Properties = append(*component.Properties, props...) +} + +// detectPQCInName checks if a string contains PQC algorithm indicators +func detectPQCInName(name string) (isPQC bool, isHybrid bool, family string) { + lowerName := strings.ToLower(name) + + // PQC algorithm families + pqcFamilies := map[string]string{ + "kyber": "ML-KEM", + "ml-kem": "ML-KEM", + "mlkem": "ML-KEM", + "dilithium": "ML-DSA", + "ml-dsa": "ML-DSA", + "mldsa": "ML-DSA", + "sphincs": "SLH-DSA", + "slh-dsa": "SLH-DSA", + "falcon": "FN-DSA", + "fn-dsa": "FN-DSA", + } + + // Check for PQC families + for pattern, familyName := range pqcFamilies { + if strings.Contains(lowerName, pattern) { + isPQC = true + family = familyName + break + } + } + + // Check for hybrid patterns + hybridPatterns := []string{ + "x25519kyber", "x25519_kyber", "x25519-kyber", + "p256kyber", "p256_kyber", "p256-kyber", + "p384kyber", "p384_kyber", "p384-kyber", + "ecdh+", "ecdh-", "ecdh_", + } + + for _, pattern := range hybridPatterns { + if strings.Contains(lowerName, pattern) { + isHybrid = true + if !isPQC { + isPQC = true + family = "hybrid" + } + break + } + } + + return +} diff --git a/scanner/plugins/pqcreadiness/pqc_oids.json b/scanner/plugins/pqcreadiness/pqc_oids.json new file mode 100644 index 0000000..a319e8a --- /dev/null +++ b/scanner/plugins/pqcreadiness/pqc_oids.json @@ -0,0 +1,206 @@ +{ + "version": "1.0.0", + "lastUpdated": "2025-01-15", + "source": "NIST FIPS 203, 204, 205 and draft OIDs", + "pqcAlgorithms": { + "ML-KEM": { + "standardName": "FIPS 203", + "family": "lattice-kem", + "primitive": "kem", + "baseOID": "1.3.6.1.4.1.22554.5.6", + "parameterSets": { + "ML-KEM-512": { + "oid": "1.3.6.1.4.1.22554.5.6.1", + "nistLevel": 1, + "publicKeySize": 800, + "ciphertextSize": 768, + "classicalBits": 128, + "quantumBits": 128 + }, + "ML-KEM-768": { + "oid": "1.3.6.1.4.1.22554.5.6.2", + "nistLevel": 3, + "publicKeySize": 1184, + "ciphertextSize": 1088, + "classicalBits": 192, + "quantumBits": 192 + }, + "ML-KEM-1024": { + "oid": "1.3.6.1.4.1.22554.5.6.3", + "nistLevel": 5, + "publicKeySize": 1568, + "ciphertextSize": 1568, + "classicalBits": 256, + "quantumBits": 256 + } + } + }, + "ML-DSA": { + "standardName": "FIPS 204", + "family": "lattice-signature", + "primitive": "signature", + "baseOID": "1.3.6.1.4.1.22554.5.5", + "parameterSets": { + "ML-DSA-44": { + "oid": "1.3.6.1.4.1.22554.5.5.1", + "nistLevel": 2, + "publicKeySize": 1312, + "signatureSize": 2420, + "classicalBits": 128, + "quantumBits": 128 + }, + "ML-DSA-65": { + "oid": "1.3.6.1.4.1.22554.5.5.2", + "nistLevel": 3, + "publicKeySize": 1952, + "signatureSize": 3293, + "classicalBits": 192, + "quantumBits": 192 + }, + "ML-DSA-87": { + "oid": "1.3.6.1.4.1.22554.5.5.3", + "nistLevel": 5, + "publicKeySize": 2592, + "signatureSize": 4595, + "classicalBits": 256, + "quantumBits": 256 + } + } + }, + "SLH-DSA": { + "standardName": "FIPS 205", + "family": "hash-signature", + "primitive": "signature", + "baseOID": "1.3.6.1.4.1.22554.5.7", + "parameterSets": { + "SLH-DSA-SHA2-128f": { + "oid": "1.3.6.1.4.1.22554.5.7.1", + "nistLevel": 1, + "classicalBits": 128, + "quantumBits": 128 + }, + "SLH-DSA-SHA2-128s": { + "oid": "1.3.6.1.4.1.22554.5.7.2", + "nistLevel": 1, + "classicalBits": 128, + "quantumBits": 128 + }, + "SLH-DSA-SHA2-192f": { + "oid": "1.3.6.1.4.1.22554.5.7.3", + "nistLevel": 3, + "classicalBits": 192, + "quantumBits": 192 + }, + "SLH-DSA-SHA2-192s": { + "oid": "1.3.6.1.4.1.22554.5.7.4", + "nistLevel": 3, + "classicalBits": 192, + "quantumBits": 192 + }, + "SLH-DSA-SHA2-256f": { + "oid": "1.3.6.1.4.1.22554.5.7.5", + "nistLevel": 5, + "classicalBits": 256, + "quantumBits": 256 + }, + "SLH-DSA-SHA2-256s": { + "oid": "1.3.6.1.4.1.22554.5.7.6", + "nistLevel": 5, + "classicalBits": 256, + "quantumBits": 256 + }, + "SLH-DSA-SHAKE-128f": { + "oid": "1.3.6.1.4.1.22554.5.7.7", + "nistLevel": 1, + "classicalBits": 128, + "quantumBits": 128 + }, + "SLH-DSA-SHAKE-128s": { + "oid": "1.3.6.1.4.1.22554.5.7.8", + "nistLevel": 1, + "classicalBits": 128, + "quantumBits": 128 + }, + "SLH-DSA-SHAKE-192f": { + "oid": "1.3.6.1.4.1.22554.5.7.9", + "nistLevel": 3, + "classicalBits": 192, + "quantumBits": 192 + }, + "SLH-DSA-SHAKE-192s": { + "oid": "1.3.6.1.4.1.22554.5.7.10", + "nistLevel": 3, + "classicalBits": 192, + "quantumBits": 192 + }, + "SLH-DSA-SHAKE-256f": { + "oid": "1.3.6.1.4.1.22554.5.7.11", + "nistLevel": 5, + "classicalBits": 256, + "quantumBits": 256 + }, + "SLH-DSA-SHAKE-256s": { + "oid": "1.3.6.1.4.1.22554.5.7.12", + "nistLevel": 5, + "classicalBits": 256, + "quantumBits": 256 + } + } + } + }, + "hybridSchemes": { + "X25519Kyber768": { + "displayName": "X25519+ML-KEM-768", + "components": ["X25519", "ML-KEM-768"], + "oid": "1.3.6.1.4.1.22554.5.6.4", + "nistLevel": 3, + "isHybrid": true, + "classicalComponent": "X25519", + "pqcComponent": "ML-KEM-768", + "classicalBits": 128, + "quantumBits": 192 + }, + "ECDH-NIST-P256-ML-KEM-768": { + "displayName": "ECDH-P256+ML-KEM-768", + "components": ["ECDH-P256", "ML-KEM-768"], + "oid": "1.3.6.1.4.1.22554.5.6.5", + "nistLevel": 3, + "isHybrid": true, + "classicalComponent": "ECDH-P256", + "pqcComponent": "ML-KEM-768", + "classicalBits": 128, + "quantumBits": 192 + }, + "ECDH-NIST-P384-ML-KEM-1024": { + "displayName": "ECDH-P384+ML-KEM-1024", + "components": ["ECDH-P384", "ML-KEM-1024"], + "oid": "1.3.6.1.4.1.22554.5.6.6", + "nistLevel": 5, + "isHybrid": true, + "classicalComponent": "ECDH-P384", + "pqcComponent": "ML-KEM-1024", + "classicalBits": 192, + "quantumBits": 256 + } + }, + "pqcNamePatterns": [ + "kyber", + "dilithium", + "sphincs", + "falcon", + "ml-kem", + "ml-dsa", + "slh-dsa", + "fn-dsa", + "mlkem", + "mldsa" + ], + "hybridNamePatterns": [ + "x25519kyber", + "x25519_kyber", + "ecdh.*kyber", + "kyber.*ecdh", + "p256.*kyber", + "p384.*kyber" + ] +} diff --git a/scanner/plugins/pqcreadiness/pqc_scanner.go b/scanner/plugins/pqcreadiness/pqc_scanner.go new file mode 100644 index 0000000..a88ae8f --- /dev/null +++ b/scanner/plugins/pqcreadiness/pqc_scanner.go @@ -0,0 +1,374 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package pqcreadiness + +import ( + "bufio" + "path/filepath" + "regexp" + "strings" + + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +// PQCConfigFinding represents a PQC configuration found in a file +type PQCConfigFinding struct { + Path string + ConfigType string // "openssl", "nginx", "apache", "generic" + PQCAlgorithms []string + IsHybrid bool + HybridComponents []string + RawSettings map[string]string +} + +// scanForPQCConfigurations scans the filesystem for PQC-related configurations +func (plugin *Plugin) scanForPQCConfigurations(fs filesystem.Filesystem) ([]cdx.Component, error) { + var findings []PQCConfigFinding + + err := fs.WalkDir(func(path string) error { + // Check for OpenSSL configuration files + if isOpenSSLConfigFile(path) { + if finding := plugin.scanOpenSSLConfigForPQC(fs, path); finding != nil { + findings = append(findings, *finding) + } + } + + // Check for PEM files that might contain PQC keys + if isPEMFile(path) { + if finding := plugin.scanPEMFileForPQC(fs, path); finding != nil { + findings = append(findings, *finding) + } + } + + // Check for nginx/apache TLS configurations + if isWebServerConfig(path) { + if finding := plugin.scanWebServerConfigForPQC(fs, path); finding != nil { + findings = append(findings, *finding) + } + } + + return nil + }) + + if err != nil { + return nil, err + } + + // Convert findings to components + var components []cdx.Component + for _, finding := range findings { + component := plugin.createPQCConfigComponent(&finding) + components = append(components, component) + } + + return components, nil +} + +// isOpenSSLConfigFile checks if a path is an OpenSSL configuration file +func isOpenSSLConfigFile(path string) bool { + base := strings.ToLower(filepath.Base(path)) + return strings.Contains(base, "openssl") && (strings.HasSuffix(base, ".cnf") || strings.HasSuffix(base, ".conf")) +} + +// isPEMFile checks if a path is a PEM file +func isPEMFile(path string) bool { + ext := strings.ToLower(filepath.Ext(path)) + return ext == ".pem" || ext == ".key" || ext == ".pub" +} + +// isWebServerConfig checks if a path is a web server configuration +func isWebServerConfig(path string) bool { + lowerPath := strings.ToLower(path) + return strings.Contains(lowerPath, "nginx") || strings.Contains(lowerPath, "apache") || + strings.Contains(lowerPath, "httpd") || strings.Contains(lowerPath, "ssl") +} + +// scanOpenSSLConfigForPQC scans an OpenSSL config file for PQC settings +func (plugin *Plugin) scanOpenSSLConfigForPQC(fs filesystem.Filesystem, path string) *PQCConfigFinding { + rc, err := fs.Open(path) + if err != nil { + log.WithField("path", path).Debug("Could not open OpenSSL config file") + return nil + } + defer rc.Close() + + finding := &PQCConfigFinding{ + Path: path, + ConfigType: "openssl", + PQCAlgorithms: []string{}, + RawSettings: make(map[string]string), + } + + // PQC-related OpenSSL configuration patterns + pqcPatterns := []*regexp.Regexp{ + regexp.MustCompile(`(?i)Groups\s*=\s*(.+)`), + regexp.MustCompile(`(?i)SignatureAlgorithms\s*=\s*(.+)`), + regexp.MustCompile(`(?i)Curves\s*=\s*(.+)`), + regexp.MustCompile(`(?i)CipherSuites\s*=\s*(.+)`), + } + + // PQC algorithm name patterns + pqcAlgorithmPatterns := []string{ + "kyber", "mlkem", "ml-kem", + "dilithium", "mldsa", "ml-dsa", + "sphincs", "slhdsa", "slh-dsa", + "falcon", "fndsa", "fn-dsa", + "x25519kyber", "p256kyber", "p384kyber", + } + + scanner := bufio.NewScanner(rc) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Skip comments and empty lines + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { + continue + } + + for _, pattern := range pqcPatterns { + if matches := pattern.FindStringSubmatch(line); len(matches) > 1 { + value := strings.TrimSpace(matches[1]) + lowerValue := strings.ToLower(value) + + // Check if the value contains PQC algorithms + for _, pqcPattern := range pqcAlgorithmPatterns { + if strings.Contains(lowerValue, pqcPattern) { + // Extract individual algorithms + algorithms := strings.Split(value, ":") + for _, alg := range algorithms { + alg = strings.TrimSpace(alg) + if alg != "" { + for _, p := range pqcAlgorithmPatterns { + if strings.Contains(strings.ToLower(alg), p) { + finding.PQCAlgorithms = append(finding.PQCAlgorithms, alg) + // Check for hybrid + if strings.Contains(strings.ToLower(alg), "x25519") || + strings.Contains(strings.ToLower(alg), "p256") || + strings.Contains(strings.ToLower(alg), "p384") { + finding.IsHybrid = true + } + } + } + } + } + + // Store raw setting + settingName := strings.Split(line, "=")[0] + finding.RawSettings[strings.TrimSpace(settingName)] = value + } + } + } + } + } + + // Only return finding if PQC algorithms were found + if len(finding.PQCAlgorithms) > 0 { + log.WithFields(log.Fields{ + "path": path, + "algorithms": finding.PQCAlgorithms, + "isHybrid": finding.IsHybrid, + }).Info("PQC configuration detected in OpenSSL config") + return finding + } + + return nil +} + +// scanPEMFileForPQC scans a PEM file for PQC key types +func (plugin *Plugin) scanPEMFileForPQC(fs filesystem.Filesystem, path string) *PQCConfigFinding { + rc, err := fs.Open(path) + if err != nil { + return nil + } + defer rc.Close() + + // PQC key header patterns + pqcKeyPatterns := []*regexp.Regexp{ + regexp.MustCompile(`(?i)-----BEGIN\s+(ML-KEM|KYBER|MLKEM)\s+`), + regexp.MustCompile(`(?i)-----BEGIN\s+(ML-DSA|DILITHIUM|MLDSA)\s+`), + regexp.MustCompile(`(?i)-----BEGIN\s+(SLH-DSA|SPHINCS|SLHDSA)\s+`), + regexp.MustCompile(`(?i)-----BEGIN\s+(FN-DSA|FALCON|FNDSA)\s+`), + } + + scanner := bufio.NewScanner(rc) + for scanner.Scan() { + line := scanner.Text() + for _, pattern := range pqcKeyPatterns { + if matches := pattern.FindStringSubmatch(line); len(matches) > 1 { + return &PQCConfigFinding{ + Path: path, + ConfigType: "pem-key", + PQCAlgorithms: []string{matches[1]}, + IsHybrid: false, + } + } + } + } + + return nil +} + +// scanWebServerConfigForPQC scans web server configs for PQC TLS settings +func (plugin *Plugin) scanWebServerConfigForPQC(fs filesystem.Filesystem, path string) *PQCConfigFinding { + rc, err := fs.Open(path) + if err != nil { + return nil + } + defer rc.Close() + + finding := &PQCConfigFinding{ + Path: path, + ConfigType: "webserver", + PQCAlgorithms: []string{}, + RawSettings: make(map[string]string), + } + + // Patterns for TLS cipher/group configuration in web servers + tlsPatterns := []*regexp.Regexp{ + regexp.MustCompile(`(?i)ssl_ecdh_curve\s+(.+);`), // nginx + regexp.MustCompile(`(?i)ssl_conf_command\s+Groups\s+(.+)`), // nginx with OpenSSL 3.0+ + regexp.MustCompile(`(?i)SSLOpenSSLConfCmd\s+Groups\s+(.+)`), // Apache + } + + pqcAlgorithmPatterns := []string{ + "kyber", "mlkem", "ml-kem", + "x25519kyber", "p256kyber", "p384kyber", + } + + scanner := bufio.NewScanner(rc) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + for _, pattern := range tlsPatterns { + if matches := pattern.FindStringSubmatch(line); len(matches) > 1 { + value := strings.TrimSpace(matches[1]) + lowerValue := strings.ToLower(value) + + for _, pqcPattern := range pqcAlgorithmPatterns { + if strings.Contains(lowerValue, pqcPattern) { + groups := strings.Split(value, ":") + for _, group := range groups { + group = strings.TrimSpace(group) + if group != "" { + for _, p := range pqcAlgorithmPatterns { + if strings.Contains(strings.ToLower(group), p) { + finding.PQCAlgorithms = append(finding.PQCAlgorithms, group) + if strings.Contains(strings.ToLower(group), "x25519") || + strings.Contains(strings.ToLower(group), "p256") || + strings.Contains(strings.ToLower(group), "p384") { + finding.IsHybrid = true + } + } + } + } + } + } + } + } + } + } + + if len(finding.PQCAlgorithms) > 0 { + log.WithFields(log.Fields{ + "path": path, + "algorithms": finding.PQCAlgorithms, + }).Info("PQC configuration detected in web server config") + return finding + } + + return nil +} + +// createPQCConfigComponent creates a CycloneDX component from a PQC config finding +func (plugin *Plugin) createPQCConfigComponent(finding *PQCConfigFinding) cdx.Component { + // Determine the quantum status + var quantumStatus QuantumVulnerabilityStatus + if finding.IsHybrid { + quantumStatus = HybridTransitional + } else { + quantumStatus = QuantumSafe + } + + // Build component name + name := "PQC-Config:" + filepath.Base(finding.Path) + + // Build properties + props := []cdx.Property{ + { + Name: "theia:pqc:is-pqc-algorithm", + Value: "true", + }, + { + Name: "theia:pqc:quantum-status", + Value: string(quantumStatus), + }, + { + Name: "theia:pqc:config-type", + Value: finding.ConfigType, + }, + { + Name: "theia:pqc:pqc-algorithms", + Value: strings.Join(finding.PQCAlgorithms, ","), + }, + { + Name: "theia:pqc:is-hybrid", + Value: boolToString(finding.IsHybrid), + }, + { + Name: "theia:pqc:detection-method", + Value: "config-scan", + }, + { + Name: "theia:pqc:quantum-threat", + Value: string(ThreatNone), + }, + } + + // Add raw settings as properties + for key, value := range finding.RawSettings { + props = append(props, cdx.Property{ + Name: "theia:pqc:config:" + strings.ToLower(key), + Value: value, + }) + } + + return cdx.Component{ + Type: cdx.ComponentTypeCryptographicAsset, + Name: name, + BOMRef: uuid.New().String(), + Description: "PQC configuration detected in " + finding.ConfigType + " file", + CryptoProperties: &cdx.CryptoProperties{ + AssetType: cdx.CryptoAssetTypeAlgorithm, + }, + Properties: &props, + Evidence: &cdx.Evidence{ + Occurrences: &[]cdx.EvidenceOccurrence{ + {Location: finding.Path}, + }, + }, + } +} + +func boolToString(b bool) string { + if b { + return "true" + } + return "false" +} diff --git a/scanner/plugins/pqcreadiness/pqcreadiness.go b/scanner/plugins/pqcreadiness/pqcreadiness.go new file mode 100644 index 0000000..0dc361e --- /dev/null +++ b/scanner/plugins/pqcreadiness/pqcreadiness.go @@ -0,0 +1,191 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package pqcreadiness + +import ( + "github.com/cbomkit/cbomkit-theia/provider/cyclonedx" + "github.com/cbomkit/cbomkit-theia/provider/filesystem" + "github.com/cbomkit/cbomkit-theia/scanner/plugins" + cdx "github.com/CycloneDX/cyclonedx-go" + log "github.com/sirupsen/logrus" +) + +// Plugin implements the PQC Readiness Assessment +type Plugin struct { + config *PQCConfig + algorithmDB *AlgorithmDatabase + pqcOIDDB *PQCOIDDatabase +} + +// GetName returns the name of the plugin +func (*Plugin) GetName() string { + return "PQC Readiness Assessment Plugin" +} + +// GetExplanation returns a description of what the plugin does +func (*Plugin) GetExplanation() string { + return "Assess post-quantum cryptography readiness by classifying quantum vulnerability of cryptographic assets, detecting PQC algorithms, calculating HNDL risk scores, and providing migration guidance with compliance tracking (CNSA 2.0, NIST SP 800-131A)" +} + +// GetType returns the plugin type +func (*Plugin) GetType() plugins.PluginType { + return plugins.PluginTypeVerify +} + +// NewPQCReadinessPlugin creates a new instance of the PQC Readiness Assessment Plugin +func NewPQCReadinessPlugin() (plugins.Plugin, error) { + // Load configuration + config, err := loadConfig() + if err != nil { + log.WithError(err).Debug("Could not load PQC config, using defaults") + config = getDefaultConfig() + } + + // Load algorithm database + algorithmDB, err := loadAlgorithmDatabase() + if err != nil { + return nil, err + } + + // Load PQC OID database + pqcOIDDB, err := loadPQCOIDDatabase() + if err != nil { + return nil, err + } + + return &Plugin{ + config: config, + algorithmDB: algorithmDB, + pqcOIDDB: pqcOIDDB, + }, nil +} + +// UpdateBOM enriches the BOM with PQC readiness information +func (plugin *Plugin) UpdateBOM(fs filesystem.Filesystem, bom *cdx.BOM) error { + if bom.Components == nil { + bom.Components = new([]cdx.Component) + } + + stats := &ProcessingStats{} + + // Phase 1: Scan filesystem for PQC configurations (creates new components) + if plugin.config.Features.PQCDetection { + pqcComponents, err := plugin.scanForPQCConfigurations(fs) + if err != nil { + log.WithError(err).Warn("PQC configuration scanning failed") + } + if len(pqcComponents) > 0 { + cyclonedx.AddComponents(bom, pqcComponents) + stats.PQCFound += len(pqcComponents) + log.WithField("count", len(pqcComponents)).Info("Added PQC configuration components") + } + } + + // Phase 2: Enrich existing components + for i := range *bom.Components { + component := &(*bom.Components)[i] + + if !isCryptoComponent(component) { + continue + } + stats.TotalCrypto++ + + // 1. Classify quantum vulnerability + if plugin.config.Features.VulnerabilityClassification { + if vuln := plugin.classifyQuantumVulnerability(component); vuln != nil { + plugin.enrichWithVulnerability(component, vuln) + if vuln.QuantumStatus == QuantumVulnerable { + stats.VulnerableCount++ + } + } + } + + // 2. Detect PQC algorithms (mark hybrid-transitional for hybrids) + if plugin.config.Features.PQCDetection { + if pqc := plugin.detectPQCAlgorithm(component); pqc != nil { + plugin.enrichWithPQC(component, pqc) + stats.PQCFound++ + } + } + + // 3. Calculate security levels + if plugin.config.Features.SecurityLevelCalculation { + level := plugin.calculateSecurityLevel(component) + plugin.setSecurityLevels(component, level) + } + + // 4. Calculate risk scores (vulnerable components only) + if plugin.config.Features.RiskScoring && isVulnerable(component) { + risk := plugin.calculateHNDLRisk(component, bom) + plugin.enrichWithRisk(component, risk) + + priority := plugin.calculateMigrationPriority(component, risk) + plugin.enrichWithMigrationPriority(component, priority) + } + + // 5. Add migration guidance + if plugin.config.Features.MigrationGuidance { + guidance := plugin.generateMigrationGuidance(component) + plugin.enrichWithGuidance(component, guidance) + } + + // 6. Check compliance timelines (CNSA 2.0, NIST, custom) + if plugin.config.Features.ComplianceTracking { + compliance := plugin.checkComplianceTimelines(component) + plugin.enrichWithCompliance(component, compliance) + } + } + + log.WithFields(log.Fields{ + "total_crypto": stats.TotalCrypto, + "vulnerable": stats.VulnerableCount, + "pqc_found": stats.PQCFound, + }).Info("PQC Readiness Assessment completed") + + return nil +} + +// ProcessingStats tracks statistics during BOM processing +type ProcessingStats struct { + TotalCrypto int + VulnerableCount int + PQCFound int +} + +// isCryptoComponent checks if a component is a cryptographic asset +func isCryptoComponent(component *cdx.Component) bool { + if component.Type != cdx.ComponentTypeCryptographicAsset { + return false + } + if component.CryptoProperties == nil { + return false + } + return true +} + +// isVulnerable checks if a component has been marked as quantum-vulnerable +func isVulnerable(component *cdx.Component) bool { + if component.Properties == nil { + return false + } + for _, prop := range *component.Properties { + if prop.Name == "theia:pqc:quantum-status" && prop.Value == string(QuantumVulnerable) { + return true + } + } + return false +} diff --git a/scanner/plugins/pqcreadiness/pqcreadiness_test.go b/scanner/plugins/pqcreadiness/pqcreadiness_test.go new file mode 100644 index 0000000..bc33355 --- /dev/null +++ b/scanner/plugins/pqcreadiness/pqcreadiness_test.go @@ -0,0 +1,498 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package pqcreadiness + +import ( + "testing" + + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewPQCReadinessPlugin(t *testing.T) { + plugin, err := NewPQCReadinessPlugin() + require.NoError(t, err) + require.NotNil(t, plugin) + + assert.Equal(t, "PQC Readiness Assessment Plugin", plugin.GetName()) + assert.NotEmpty(t, plugin.GetExplanation()) + assert.Equal(t, PluginTypeVerify, int(plugin.GetType())) +} + +func TestClassifyQuantumVulnerability_RSA(t *testing.T) { + plugin := createTestPlugin(t) + + tests := []struct { + name string + componentName string + keySize int + expectedStatus QuantumVulnerabilityStatus + expectedThreat QuantumThreat + }{ + { + name: "RSA-2048 is quantum vulnerable", + componentName: "RSA-2048", + keySize: 2048, + expectedStatus: QuantumVulnerable, + expectedThreat: ThreatShor, + }, + { + name: "RSA-4096 is quantum vulnerable", + componentName: "RSA-4096", + keySize: 4096, + expectedStatus: QuantumVulnerable, + expectedThreat: ThreatShor, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := createCryptoComponent(tt.componentName, "") + result := plugin.classifyQuantumVulnerability(component) + + require.NotNil(t, result) + assert.Equal(t, tt.expectedStatus, result.QuantumStatus) + assert.Equal(t, tt.expectedThreat, result.PrimaryThreat) + }) + } +} + +func TestClassifyQuantumVulnerability_ECDSA(t *testing.T) { + plugin := createTestPlugin(t) + + tests := []struct { + name string + componentName string + curve string + expectedStatus QuantumVulnerabilityStatus + }{ + { + name: "ECDSA P-256 is quantum vulnerable", + componentName: "ECDSA-P256", + curve: "P-256", + expectedStatus: QuantumVulnerable, + }, + { + name: "ECDSA P-384 is quantum vulnerable", + componentName: "ECDSA-P384", + curve: "P-384", + expectedStatus: QuantumVulnerable, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := createCryptoComponentWithCurve(tt.componentName, tt.curve) + result := plugin.classifyQuantumVulnerability(component) + + require.NotNil(t, result) + assert.Equal(t, tt.expectedStatus, result.QuantumStatus) + }) + } +} + +func TestClassifyQuantumVulnerability_AES(t *testing.T) { + plugin := createTestPlugin(t) + + tests := []struct { + name string + componentName string + keySize int + expectedStatus QuantumVulnerabilityStatus + expectedQuantumBits int + }{ + { + name: "AES-128 is partially secure", + componentName: "AES-128", + keySize: 128, + expectedStatus: QuantumPartiallySecure, + expectedQuantumBits: 64, + }, + { + name: "AES-256 provides 128-bit quantum security", + componentName: "AES-256", + keySize: 256, + expectedStatus: QuantumPartiallySecure, // Grover's algorithm halves effective security + expectedQuantumBits: 128, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := createCryptoComponent(tt.componentName, "") + result := plugin.classifyQuantumVulnerability(component) + + require.NotNil(t, result) + assert.Equal(t, tt.expectedStatus, result.QuantumStatus) + assert.Equal(t, tt.expectedQuantumBits, result.QuantumSecurityBits) + }) + } +} + +func TestClassifyQuantumVulnerability_SHA(t *testing.T) { + plugin := createTestPlugin(t) + + tests := []struct { + name string + componentName string + expectedStatus QuantumVulnerabilityStatus + }{ + { + name: "SHA-256 is resistant", + componentName: "SHA-256", + expectedStatus: QuantumResistant, + }, + { + name: "SHA-384 is resistant", + componentName: "SHA-384", + expectedStatus: QuantumResistant, + }, + { + name: "SHA3-256 is resistant", + componentName: "SHA3-256", + expectedStatus: QuantumResistant, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := createCryptoComponent(tt.componentName, "") + result := plugin.classifyQuantumVulnerability(component) + + require.NotNil(t, result) + assert.Equal(t, tt.expectedStatus, result.QuantumStatus) + }) + } +} + +func TestDetectPQCAlgorithm_ByOID(t *testing.T) { + plugin := createTestPlugin(t) + + tests := []struct { + name string + oid string + expectedFamily string + expectedLevel int + }{ + { + name: "ML-KEM-768 detection by OID", + oid: "1.3.6.1.4.1.22554.5.6.2", + expectedFamily: "ML-KEM", + expectedLevel: 3, + }, + { + name: "ML-DSA-65 detection by OID", + oid: "1.3.6.1.4.1.22554.5.5.2", + expectedFamily: "ML-DSA", + expectedLevel: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := createCryptoComponentWithOID("TestAlgorithm", tt.oid) + result := plugin.detectPQCAlgorithm(component) + + require.NotNil(t, result) + assert.Equal(t, tt.expectedFamily, result.Algorithm.Family) + assert.Equal(t, tt.expectedLevel, result.Algorithm.NISTLevel) + assert.Equal(t, "oid", result.DetectionMethod) + }) + } +} + +func TestDetectPQCAlgorithm_ByName(t *testing.T) { + plugin := createTestPlugin(t) + + tests := []struct { + name string + componentName string + expectedFamily string + expectedHybrid bool + }{ + { + name: "ML-KEM-768 detection by name", + componentName: "ML-KEM-768", + expectedFamily: "lattice-kem", // Family from pqc_oids.json database + expectedHybrid: false, + }, + { + name: "X25519Kyber768 hybrid detection", + componentName: "X25519Kyber768", + expectedFamily: "hybrid", + expectedHybrid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := createCryptoComponent(tt.componentName, "") + result := plugin.detectPQCAlgorithm(component) + + require.NotNil(t, result) + assert.Equal(t, tt.expectedFamily, result.Algorithm.Family) + assert.Equal(t, tt.expectedHybrid, result.IsHybridDeployment) + }) + } +} + +func TestCalculateSecurityLevel(t *testing.T) { + plugin := createTestPlugin(t) + + tests := []struct { + name string + componentName string + expectedClassical int + expectedQuantum int + expectedNISTLevel int + }{ + { + name: "RSA-2048 security levels", + componentName: "RSA-2048", + expectedClassical: 112, + expectedQuantum: 0, + expectedNISTLevel: 0, + }, + { + name: "AES-256 security levels", + componentName: "AES-256", + expectedClassical: 256, + expectedQuantum: 128, + expectedNISTLevel: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := createCryptoComponent(tt.componentName, "") + + // First classify vulnerability to populate properties + plugin.classifyQuantumVulnerability(component) + + level := plugin.calculateSecurityLevel(component) + + require.NotNil(t, level) + assert.Equal(t, tt.expectedClassical, level.ClassicalBits) + assert.Equal(t, tt.expectedQuantum, level.QuantumBits) + assert.Equal(t, tt.expectedNISTLevel, level.NISTLevel) + }) + } +} + +func TestRiskScoring(t *testing.T) { + plugin := createTestPlugin(t) + + // Create a vulnerable component + component := createCryptoComponent("RSA-2048", "") + component.Evidence = &cdx.Evidence{ + Occurrences: &[]cdx.EvidenceOccurrence{ + {Location: "/etc/ssl/certs/server.pem"}, + }, + } + + // Add quantum status property + component.Properties = &[]cdx.Property{ + {Name: "theia:pqc:quantum-status", Value: string(QuantumVulnerable)}, + } + + bom := &cdx.BOM{ + Components: &[]cdx.Component{*component}, + } + + risk := plugin.calculateHNDLRisk(component, bom) + + require.NotNil(t, risk) + assert.Greater(t, risk.OverallScore, 0.0) + assert.LessOrEqual(t, risk.OverallScore, 10.0) + assert.NotEmpty(t, risk.Category) + assert.Equal(t, 1.0, risk.VulnerabilityLevel) // Quantum vulnerable = 1.0 +} + +func TestMigrationGuidance(t *testing.T) { + plugin := createTestPlugin(t) + + tests := []struct { + name string + componentName string + quantumStatus QuantumVulnerabilityStatus + expectReplacements bool + }{ + { + name: "RSA needs replacement", + componentName: "RSA-2048", + quantumStatus: QuantumVulnerable, + expectReplacements: true, + }, + { + name: "ML-KEM-768 no replacement needed", + componentName: "ML-KEM-768", + quantumStatus: QuantumSafe, + expectReplacements: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := createCryptoComponent(tt.componentName, "") + component.Properties = &[]cdx.Property{ + {Name: "theia:pqc:quantum-status", Value: string(tt.quantumStatus)}, + } + + guidance := plugin.generateMigrationGuidance(component) + + require.NotNil(t, guidance) + if tt.expectReplacements { + assert.NotEmpty(t, guidance.RecommendedReplacements) + } + }) + } +} + +func TestComplianceTracking(t *testing.T) { + plugin := createTestPlugin(t) + + // Create a component with quantum-vulnerable algorithm + component := createCryptoComponent("RSA-2048", "") + component.Properties = &[]cdx.Property{ + {Name: "theia:pqc:quantum-status", Value: string(QuantumVulnerable)}, + } + component.Evidence = &cdx.Evidence{ + Occurrences: &[]cdx.EvidenceOccurrence{ + {Location: "/etc/ssl/server.key"}, + }, + } + + compliance := plugin.checkComplianceTimelines(component) + + require.NotNil(t, compliance) + assert.NotEmpty(t, compliance.Frameworks) + + // Check CNSA 2.0 compliance + var cnsa *FrameworkCompliance + for i := range compliance.Frameworks { + if compliance.Frameworks[i].Framework == "CNSA 2.0" { + cnsa = &compliance.Frameworks[i] + break + } + } + + require.NotNil(t, cnsa) + assert.Equal(t, "non-compliant", cnsa.Status) + assert.NotEmpty(t, cnsa.Violations) +} + +func TestNIST131ACompliance(t *testing.T) { + plugin := createTestPlugin(t) + + tests := []struct { + name string + componentName string + expectedStatus string + }{ + { + name: "SHA-1 signature is non-compliant", + componentName: "SHA1WithRSA", + expectedStatus: "non-compliant", + }, + { + name: "MD5 is non-compliant", + componentName: "MD5", + expectedStatus: "non-compliant", + }, + { + name: "3DES is deprecated", + componentName: "3DES", + expectedStatus: "deprecated", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + component := createCryptoComponent(tt.componentName, "") + nist := plugin.checkNIST131ACompliance(component) + + assert.Equal(t, tt.expectedStatus, nist.Status) + }) + } +} + +func TestAlgorithmDatabaseLoading(t *testing.T) { + db, err := loadAlgorithmDatabase() + require.NoError(t, err) + require.NotNil(t, db) + + // Check that key algorithms are present + _, hasRSA := db.ClassicalAlgorithms["RSA"] + assert.True(t, hasRSA, "RSA should be in database") + + _, hasAES := db.ClassicalAlgorithms["AES"] + assert.True(t, hasAES, "AES should be in database") +} + +func TestPQCOIDDatabaseLoading(t *testing.T) { + db, err := loadPQCOIDDatabase() + require.NoError(t, err) + require.NotNil(t, db) + + // Check that ML-KEM is present + _, hasMLKEM := db.PQCAlgorithms["ML-KEM"] + assert.True(t, hasMLKEM, "ML-KEM should be in database") + + // Check OID lookup works + info := db.LookupByOID("1.3.6.1.4.1.22554.5.6.2") + require.NotNil(t, info) + assert.Equal(t, "ML-KEM", info.Family) +} + +// Helper functions + +func createTestPlugin(t *testing.T) *Plugin { + p, err := NewPQCReadinessPlugin() + require.NoError(t, err) + return p.(*Plugin) +} + +func createCryptoComponent(name, oid string) *cdx.Component { + component := &cdx.Component{ + Type: cdx.ComponentTypeCryptographicAsset, + Name: name, + BOMRef: "test-ref", + CryptoProperties: &cdx.CryptoProperties{ + AssetType: cdx.CryptoAssetTypeAlgorithm, + }, + } + if oid != "" { + component.CryptoProperties.OID = oid + } + return component +} + +func createCryptoComponentWithOID(name, oid string) *cdx.Component { + return createCryptoComponent(name, oid) +} + +func createCryptoComponentWithCurve(name, curve string) *cdx.Component { + component := createCryptoComponent(name, "") + component.CryptoProperties.AlgorithmProperties = &cdx.CryptoAlgorithmProperties{ + Curve: curve, + } + return component +} + +// PluginTypeVerify is the integer value for PluginTypeVerify +const PluginTypeVerify = 2 diff --git a/scanner/plugins/pqcreadiness/quantum_vulnerability.go b/scanner/plugins/pqcreadiness/quantum_vulnerability.go new file mode 100644 index 0000000..5e2b1e3 --- /dev/null +++ b/scanner/plugins/pqcreadiness/quantum_vulnerability.go @@ -0,0 +1,518 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package pqcreadiness + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + cdx "github.com/CycloneDX/cyclonedx-go" +) + +// QuantumVulnerabilityStatus represents the quantum threat status of an algorithm +type QuantumVulnerabilityStatus string + +const ( + // QuantumVulnerable - Algorithm is completely broken by quantum computers (Shor's algorithm) + QuantumVulnerable QuantumVulnerabilityStatus = "quantum-vulnerable" + // QuantumPartiallySecure - Algorithm has reduced security under quantum attack (Grover's algorithm) + QuantumPartiallySecure QuantumVulnerabilityStatus = "quantum-partially-secure" + // QuantumResistant - Algorithm maintains security (hash functions, larger symmetric keys) + QuantumResistant QuantumVulnerabilityStatus = "quantum-resistant" + // HybridTransitional - PQC+classical hybrid scheme (better than vulnerable, not fully quantum-safe) + HybridTransitional QuantumVulnerabilityStatus = "hybrid-transitional" + // QuantumSafe - True post-quantum cryptographic algorithm + QuantumSafe QuantumVulnerabilityStatus = "quantum-safe" + // QuantumUnknown - Unable to determine quantum status + QuantumUnknown QuantumVulnerabilityStatus = "unknown" +) + +// QuantumThreat represents the primary quantum threat to an algorithm +type QuantumThreat string + +const ( + // ThreatShor - Shor's algorithm breaks RSA, ECC, DH, DSA + ThreatShor QuantumThreat = "shors-algorithm" + // ThreatGrover - Grover's algorithm reduces symmetric/hash security by half + ThreatGrover QuantumThreat = "grovers-algorithm" + // ThreatNone - No significant quantum threat + ThreatNone QuantumThreat = "none" + // ThreatUnknown - Unknown threat model + ThreatUnknown QuantumThreat = "unknown" +) + +// AlgorithmVulnerability contains quantum vulnerability information for an algorithm +type AlgorithmVulnerability struct { + AlgorithmName string + AlgorithmFamily string + QuantumStatus QuantumVulnerabilityStatus + PrimaryThreat QuantumThreat + ClassicalSecurityBits int + QuantumSecurityBits int + NISTQuantumLevel int + Notes string + RecommendedReplacement []string +} + +// classifyQuantumVulnerability determines the quantum vulnerability of a component +func (plugin *Plugin) classifyQuantumVulnerability(component *cdx.Component) *AlgorithmVulnerability { + // Extract algorithm information from component + algName := extractAlgorithmName(component) + keySize := extractKeySize(component) + curve := extractCurve(component) + oid := extractOID(component) + + // Look up in algorithm database + if vulnInfo := plugin.algorithmDB.Lookup(algName, oid, keySize, curve); vulnInfo != nil { + return vulnInfo + } + + // Pattern matching for unknown algorithms + return plugin.inferVulnerability(algName, keySize, curve, component) +} + +// enrichWithVulnerability adds quantum vulnerability properties to a component +func (plugin *Plugin) enrichWithVulnerability(component *cdx.Component, vuln *AlgorithmVulnerability) { + if component.Properties == nil { + component.Properties = &[]cdx.Property{} + } + + props := []cdx.Property{ + { + Name: "theia:pqc:quantum-status", + Value: string(vuln.QuantumStatus), + }, + { + Name: "theia:pqc:quantum-threat", + Value: string(vuln.PrimaryThreat), + }, + { + Name: "theia:pqc:classical-security-bits", + Value: fmt.Sprintf("%d", vuln.ClassicalSecurityBits), + }, + { + Name: "theia:pqc:quantum-security-bits", + Value: fmt.Sprintf("%d", vuln.QuantumSecurityBits), + }, + { + Name: "theia:pqc:nist-quantum-level", + Value: fmt.Sprintf("%d", vuln.NISTQuantumLevel), + }, + } + + if len(vuln.RecommendedReplacement) > 0 { + props = append(props, cdx.Property{ + Name: "theia:pqc:recommended-replacement", + Value: strings.Join(vuln.RecommendedReplacement, ","), + }) + } + + if vuln.Notes != "" { + props = append(props, cdx.Property{ + Name: "theia:pqc:vulnerability-notes", + Value: vuln.Notes, + }) + } + + *component.Properties = append(*component.Properties, props...) +} + +// extractAlgorithmName extracts the algorithm name from a component +func extractAlgorithmName(component *cdx.Component) string { + // First try the component name + if component.Name != "" { + return component.Name + } + + // Check crypto properties + if component.CryptoProperties != nil && component.CryptoProperties.AlgorithmProperties != nil { + if component.CryptoProperties.AlgorithmProperties.ParameterSetIdentifier != "" { + return component.CryptoProperties.AlgorithmProperties.ParameterSetIdentifier + } + } + + return "" +} + +// extractKeySize extracts the key size from a component +func extractKeySize(component *cdx.Component) int { + // Try to extract from component name (e.g., "RSA-2048", "AES-256") + name := strings.ToUpper(component.Name) + + // Common patterns + patterns := []string{ + `RSA[- ]?(\d+)`, + `AES[- ]?(\d+)`, + `DSA[- ]?(\d+)`, + `(\d+)[- ]?BIT`, + } + + for _, pattern := range patterns { + re := regexp.MustCompile(pattern) + if matches := re.FindStringSubmatch(name); len(matches) > 1 { + if size, err := strconv.Atoi(matches[1]); err == nil { + return size + } + } + } + + // Check crypto properties for parameter set + if component.CryptoProperties != nil && component.CryptoProperties.AlgorithmProperties != nil { + paramSet := component.CryptoProperties.AlgorithmProperties.ParameterSetIdentifier + if paramSet != "" { + if size, err := strconv.Atoi(paramSet); err == nil { + return size + } + } + } + + return 0 +} + +// extractCurve extracts the elliptic curve from a component +func extractCurve(component *cdx.Component) string { + name := strings.ToUpper(component.Name) + + // Common curve patterns + curves := []string{"P-256", "P-384", "P-521", "SECP256R1", "SECP384R1", "SECP521R1", "ED25519", "ED448", "X25519", "X448"} + for _, curve := range curves { + if strings.Contains(name, curve) { + return curve + } + } + + // Check crypto properties + if component.CryptoProperties != nil && component.CryptoProperties.AlgorithmProperties != nil { + if component.CryptoProperties.AlgorithmProperties.Curve != "" { + return component.CryptoProperties.AlgorithmProperties.Curve + } + } + + return "" +} + +// extractOID extracts the OID from a component +func extractOID(component *cdx.Component) string { + if component.CryptoProperties != nil && component.CryptoProperties.OID != "" { + return component.CryptoProperties.OID + } + return "" +} + +// inferVulnerability infers quantum vulnerability from algorithm patterns +func (plugin *Plugin) inferVulnerability(algName string, keySize int, curve string, component *cdx.Component) *AlgorithmVulnerability { + upperName := strings.ToUpper(algName) + + // RSA patterns + if strings.Contains(upperName, "RSA") { + return plugin.inferRSAVulnerability(keySize) + } + + // ECDSA/ECDH patterns + if strings.Contains(upperName, "ECDSA") || strings.Contains(upperName, "ECDH") || strings.Contains(upperName, "EC") { + return plugin.inferECVulnerability(curve) + } + + // DSA patterns + if strings.Contains(upperName, "DSA") && !strings.Contains(upperName, "ECDSA") { + return plugin.inferDSAVulnerability(keySize) + } + + // DH patterns + if strings.Contains(upperName, "DH") && !strings.Contains(upperName, "ECDH") { + return plugin.inferDHVulnerability(keySize) + } + + // Ed25519/Ed448 + if strings.Contains(upperName, "ED25519") || strings.Contains(upperName, "ED448") { + return &AlgorithmVulnerability{ + AlgorithmName: algName, + AlgorithmFamily: "EdDSA", + QuantumStatus: QuantumVulnerable, + PrimaryThreat: ThreatShor, + ClassicalSecurityBits: 128, + QuantumSecurityBits: 0, + NISTQuantumLevel: 0, + RecommendedReplacement: []string{"ML-DSA-65", "SLH-DSA-SHA2-128f"}, + } + } + + // AES patterns + if strings.Contains(upperName, "AES") { + return plugin.inferAESVulnerability(keySize) + } + + // SHA patterns + if strings.Contains(upperName, "SHA") { + return plugin.inferSHAVulnerability(algName) + } + + // 3DES + if strings.Contains(upperName, "3DES") || strings.Contains(upperName, "TRIPLE") { + return &AlgorithmVulnerability{ + AlgorithmName: algName, + AlgorithmFamily: "3DES", + QuantumStatus: QuantumPartiallySecure, + PrimaryThreat: ThreatGrover, + ClassicalSecurityBits: 112, + QuantumSecurityBits: 56, + NISTQuantumLevel: 0, + Notes: "3DES is deprecated; quantum security is theoretical as algorithm is obsolete", + RecommendedReplacement: []string{"AES-256"}, + } + } + + // ChaCha20 + if strings.Contains(upperName, "CHACHA") { + return &AlgorithmVulnerability{ + AlgorithmName: algName, + AlgorithmFamily: "ChaCha", + QuantumStatus: QuantumResistant, + PrimaryThreat: ThreatGrover, + ClassicalSecurityBits: 256, + QuantumSecurityBits: 128, + NISTQuantumLevel: 3, + } + } + + // Unknown algorithm + return &AlgorithmVulnerability{ + AlgorithmName: algName, + AlgorithmFamily: "unknown", + QuantumStatus: QuantumUnknown, + PrimaryThreat: ThreatUnknown, + Notes: "Unable to determine quantum vulnerability for this algorithm", + } +} + +func (plugin *Plugin) inferRSAVulnerability(keySize int) *AlgorithmVulnerability { + vuln := &AlgorithmVulnerability{ + AlgorithmFamily: "RSA", + QuantumStatus: QuantumVulnerable, + PrimaryThreat: ThreatShor, + QuantumSecurityBits: 0, + NISTQuantumLevel: 0, + RecommendedReplacement: []string{"ML-KEM-768", "ML-DSA-65"}, + } + + switch { + case keySize >= 4096: + vuln.AlgorithmName = fmt.Sprintf("RSA-%d", keySize) + vuln.ClassicalSecurityBits = 152 + case keySize >= 3072: + vuln.AlgorithmName = fmt.Sprintf("RSA-%d", keySize) + vuln.ClassicalSecurityBits = 128 + case keySize >= 2048: + vuln.AlgorithmName = fmt.Sprintf("RSA-%d", keySize) + vuln.ClassicalSecurityBits = 112 + case keySize >= 1024: + vuln.AlgorithmName = fmt.Sprintf("RSA-%d", keySize) + vuln.ClassicalSecurityBits = 80 + vuln.Notes = "RSA-1024 is considered weak even classically" + default: + vuln.AlgorithmName = "RSA" + vuln.ClassicalSecurityBits = 112 // Assume 2048 as default + } + + return vuln +} + +func (plugin *Plugin) inferECVulnerability(curve string) *AlgorithmVulnerability { + vuln := &AlgorithmVulnerability{ + AlgorithmFamily: "ECC", + QuantumStatus: QuantumVulnerable, + PrimaryThreat: ThreatShor, + QuantumSecurityBits: 0, + NISTQuantumLevel: 0, + RecommendedReplacement: []string{"ML-DSA-65", "ML-KEM-768"}, + } + + switch strings.ToUpper(curve) { + case "P-256", "SECP256R1", "PRIME256V1": + vuln.AlgorithmName = "ECDSA-P256" + vuln.ClassicalSecurityBits = 128 + case "P-384", "SECP384R1": + vuln.AlgorithmName = "ECDSA-P384" + vuln.ClassicalSecurityBits = 192 + vuln.RecommendedReplacement = []string{"ML-DSA-87", "ML-KEM-1024"} + case "P-521", "SECP521R1": + vuln.AlgorithmName = "ECDSA-P521" + vuln.ClassicalSecurityBits = 256 + vuln.RecommendedReplacement = []string{"ML-DSA-87", "ML-KEM-1024"} + default: + vuln.AlgorithmName = "ECDSA" + vuln.ClassicalSecurityBits = 128 // Assume P-256 as default + } + + return vuln +} + +func (plugin *Plugin) inferDSAVulnerability(keySize int) *AlgorithmVulnerability { + vuln := &AlgorithmVulnerability{ + AlgorithmFamily: "DSA", + QuantumStatus: QuantumVulnerable, + PrimaryThreat: ThreatShor, + QuantumSecurityBits: 0, + NISTQuantumLevel: 0, + RecommendedReplacement: []string{"ML-DSA-65"}, + } + + switch { + case keySize >= 3072: + vuln.AlgorithmName = fmt.Sprintf("DSA-%d", keySize) + vuln.ClassicalSecurityBits = 128 + case keySize >= 2048: + vuln.AlgorithmName = fmt.Sprintf("DSA-%d", keySize) + vuln.ClassicalSecurityBits = 112 + default: + vuln.AlgorithmName = "DSA" + vuln.ClassicalSecurityBits = 112 + } + + return vuln +} + +func (plugin *Plugin) inferDHVulnerability(keySize int) *AlgorithmVulnerability { + vuln := &AlgorithmVulnerability{ + AlgorithmFamily: "DH", + QuantumStatus: QuantumVulnerable, + PrimaryThreat: ThreatShor, + QuantumSecurityBits: 0, + NISTQuantumLevel: 0, + RecommendedReplacement: []string{"ML-KEM-768"}, + } + + switch { + case keySize >= 4096: + vuln.AlgorithmName = fmt.Sprintf("DH-%d", keySize) + vuln.ClassicalSecurityBits = 152 + case keySize >= 3072: + vuln.AlgorithmName = fmt.Sprintf("DH-%d", keySize) + vuln.ClassicalSecurityBits = 128 + case keySize >= 2048: + vuln.AlgorithmName = fmt.Sprintf("DH-%d", keySize) + vuln.ClassicalSecurityBits = 112 + default: + vuln.AlgorithmName = "DH" + vuln.ClassicalSecurityBits = 112 + } + + return vuln +} + +func (plugin *Plugin) inferAESVulnerability(keySize int) *AlgorithmVulnerability { + vuln := &AlgorithmVulnerability{ + AlgorithmFamily: "AES", + QuantumStatus: QuantumPartiallySecure, + PrimaryThreat: ThreatGrover, + } + + switch { + case keySize >= 256: + vuln.AlgorithmName = "AES-256" + vuln.ClassicalSecurityBits = 256 + vuln.QuantumSecurityBits = 128 + vuln.NISTQuantumLevel = 3 + vuln.QuantumStatus = QuantumResistant + case keySize >= 192: + vuln.AlgorithmName = "AES-192" + vuln.ClassicalSecurityBits = 192 + vuln.QuantumSecurityBits = 96 + vuln.NISTQuantumLevel = 2 + case keySize >= 128: + vuln.AlgorithmName = "AES-128" + vuln.ClassicalSecurityBits = 128 + vuln.QuantumSecurityBits = 64 + vuln.NISTQuantumLevel = 1 + vuln.Notes = "Consider upgrading to AES-256 for post-quantum security" + vuln.RecommendedReplacement = []string{"AES-256"} + default: + vuln.AlgorithmName = "AES" + vuln.ClassicalSecurityBits = 128 + vuln.QuantumSecurityBits = 64 + vuln.NISTQuantumLevel = 1 + } + + return vuln +} + +func (plugin *Plugin) inferSHAVulnerability(algName string) *AlgorithmVulnerability { + upperName := strings.ToUpper(algName) + + vuln := &AlgorithmVulnerability{ + AlgorithmFamily: "SHA", + QuantumStatus: QuantumResistant, + PrimaryThreat: ThreatGrover, + } + + switch { + case strings.Contains(upperName, "SHA3-512") || strings.Contains(upperName, "SHA-3-512"): + vuln.AlgorithmName = "SHA3-512" + vuln.ClassicalSecurityBits = 256 + vuln.QuantumSecurityBits = 256 + vuln.NISTQuantumLevel = 5 + case strings.Contains(upperName, "SHA3-384") || strings.Contains(upperName, "SHA-3-384"): + vuln.AlgorithmName = "SHA3-384" + vuln.ClassicalSecurityBits = 192 + vuln.QuantumSecurityBits = 192 + vuln.NISTQuantumLevel = 3 + case strings.Contains(upperName, "SHA3-256") || strings.Contains(upperName, "SHA-3-256"): + vuln.AlgorithmName = "SHA3-256" + vuln.ClassicalSecurityBits = 128 + vuln.QuantumSecurityBits = 128 + vuln.NISTQuantumLevel = 1 + case strings.Contains(upperName, "SHA512") || strings.Contains(upperName, "SHA-512"): + vuln.AlgorithmName = "SHA-512" + vuln.ClassicalSecurityBits = 256 + vuln.QuantumSecurityBits = 256 + vuln.NISTQuantumLevel = 5 + case strings.Contains(upperName, "SHA384") || strings.Contains(upperName, "SHA-384"): + vuln.AlgorithmName = "SHA-384" + vuln.ClassicalSecurityBits = 192 + vuln.QuantumSecurityBits = 192 + vuln.NISTQuantumLevel = 3 + case strings.Contains(upperName, "SHA256") || strings.Contains(upperName, "SHA-256"): + vuln.AlgorithmName = "SHA-256" + vuln.ClassicalSecurityBits = 128 + vuln.QuantumSecurityBits = 128 + vuln.NISTQuantumLevel = 1 + case strings.Contains(upperName, "SHA1") || strings.Contains(upperName, "SHA-1"): + vuln.AlgorithmName = "SHA-1" + vuln.ClassicalSecurityBits = 80 + vuln.QuantumSecurityBits = 80 + vuln.NISTQuantumLevel = 0 + vuln.Notes = "SHA-1 is deprecated for cryptographic use" + vuln.RecommendedReplacement = []string{"SHA-256", "SHA3-256"} + case strings.Contains(upperName, "MD5"): + vuln.AlgorithmName = "MD5" + vuln.AlgorithmFamily = "MD5" + vuln.ClassicalSecurityBits = 64 + vuln.QuantumSecurityBits = 64 + vuln.NISTQuantumLevel = 0 + vuln.Notes = "MD5 is broken and should not be used" + vuln.RecommendedReplacement = []string{"SHA-256", "SHA3-256"} + default: + vuln.AlgorithmName = algName + vuln.ClassicalSecurityBits = 128 + vuln.QuantumSecurityBits = 128 + vuln.NISTQuantumLevel = 1 + } + + return vuln +} diff --git a/scanner/plugins/pqcreadiness/risk_scoring.go b/scanner/plugins/pqcreadiness/risk_scoring.go new file mode 100644 index 0000000..5e93892 --- /dev/null +++ b/scanner/plugins/pqcreadiness/risk_scoring.go @@ -0,0 +1,484 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package pqcreadiness + +import ( + "fmt" + "path/filepath" + "strings" + "time" + + cdx "github.com/CycloneDX/cyclonedx-go" +) + +// RiskCategory represents the severity of HNDL risk +type RiskCategory string + +const ( + RiskCritical RiskCategory = "critical" + RiskHigh RiskCategory = "high" + RiskMedium RiskCategory = "medium" + RiskLow RiskCategory = "low" +) + +// HNDLRiskScore represents the "Harvest Now, Decrypt Later" risk assessment +type HNDLRiskScore struct { + OverallScore float64 // 0.0 - 10.0 + Category RiskCategory // critical, high, medium, low + DataSensitivity float64 // 0.0 - 1.0 + CryptoLifetime float64 // Normalized lifetime score + VulnerabilityLevel float64 // 0.0 - 1.0 (1.0 = completely broken) + ExposureLevel float64 // 0.0 - 1.0 (network-facing vs internal) + Factors []RiskFactor // Detailed breakdown +} + +// RiskFactor represents a single risk factor contribution +type RiskFactor struct { + Name string + Value float64 + Weight float64 + Description string +} + +// MigrationPriority represents the urgency of migration to PQC +type MigrationPriority struct { + Priority RiskCategory + Score float64 // 0.0 - 100.0 + Deadline *time.Time // Compliance deadline if applicable + BlockingFactors []string // What's preventing migration + RecommendedPath string // Suggested migration approach +} + +// calculateHNDLRisk calculates the Harvest Now, Decrypt Later risk score +func (plugin *Plugin) calculateHNDLRisk(component *cdx.Component, bom *cdx.BOM) *HNDLRiskScore { + risk := &HNDLRiskScore{ + Factors: []RiskFactor{}, + } + + // Calculate data sensitivity + risk.DataSensitivity = plugin.calculateDataSensitivity(component) + risk.Factors = append(risk.Factors, RiskFactor{ + Name: "data_sensitivity", + Value: risk.DataSensitivity, + Weight: plugin.config.RiskWeights.DataSensitivity, + Description: "Estimated sensitivity of data protected by this cryptography", + }) + + // Calculate crypto lifetime + risk.CryptoLifetime = plugin.calculateCryptoLifetime(component) + risk.Factors = append(risk.Factors, RiskFactor{ + Name: "crypto_lifetime", + Value: risk.CryptoLifetime, + Weight: plugin.config.RiskWeights.CryptoLifetime, + Description: "Duration the cryptographic protection needs to remain secure", + }) + + // Calculate vulnerability level + risk.VulnerabilityLevel = plugin.calculateVulnerabilityLevel(component) + risk.Factors = append(risk.Factors, RiskFactor{ + Name: "vulnerability_level", + Value: risk.VulnerabilityLevel, + Weight: plugin.config.RiskWeights.VulnerabilityLevel, + Description: "Degree to which algorithm is vulnerable to quantum attacks", + }) + + // Calculate exposure level + risk.ExposureLevel = plugin.calculateExposureLevel(component) + risk.Factors = append(risk.Factors, RiskFactor{ + Name: "exposure_level", + Value: risk.ExposureLevel, + Weight: plugin.config.RiskWeights.ExposureLevel, + Description: "Network exposure and attack surface", + }) + + // Calculate overall score (weighted sum normalized to 0-10) + weightedSum := risk.DataSensitivity*plugin.config.RiskWeights.DataSensitivity + + risk.CryptoLifetime*plugin.config.RiskWeights.CryptoLifetime + + risk.VulnerabilityLevel*plugin.config.RiskWeights.VulnerabilityLevel + + risk.ExposureLevel*plugin.config.RiskWeights.ExposureLevel + + totalWeight := plugin.config.RiskWeights.DataSensitivity + + plugin.config.RiskWeights.CryptoLifetime + + plugin.config.RiskWeights.VulnerabilityLevel + + plugin.config.RiskWeights.ExposureLevel + + risk.OverallScore = (weightedSum / totalWeight) * 10.0 + + // Determine category + risk.Category = categorizeRisk(risk.OverallScore) + + return risk +} + +// calculateDataSensitivity infers data sensitivity from component properties +func (plugin *Plugin) calculateDataSensitivity(component *cdx.Component) float64 { + sensitivity := 0.5 // Default medium sensitivity + + // Check file path for sensitivity indicators + if component.Evidence != nil && component.Evidence.Occurrences != nil { + for _, occ := range *component.Evidence.Occurrences { + path := strings.ToLower(occ.Location) + + // Check against configured sensitivity rules + for _, rule := range plugin.config.SensitivityRules { + if rule.Pattern != "" { + matched, _ := filepath.Match(strings.ToLower(rule.Pattern), filepath.Base(path)) + if matched || strings.Contains(path, strings.Trim(rule.Pattern, "*")) { + if rule.Sensitivity > sensitivity { + sensitivity = rule.Sensitivity + } + } + } + } + + // Additional path-based heuristics + if strings.Contains(path, "pki") || strings.Contains(path, "ca") { + sensitivity = maxFloat(sensitivity, 0.85) + } + if strings.Contains(path, "signing") { + sensitivity = maxFloat(sensitivity, 0.8) + } + if strings.Contains(path, "auth") { + sensitivity = maxFloat(sensitivity, 0.75) + } + } + } + + // Check for key usage indicators in properties + if component.Properties != nil { + for _, prop := range *component.Properties { + if strings.Contains(prop.Name, "key-usage") || strings.Contains(prop.Name, "keyUsage") { + for _, rule := range plugin.config.SensitivityRules { + if rule.KeyUsageContains != "" && strings.Contains(prop.Value, rule.KeyUsageContains) { + sensitivity = maxFloat(sensitivity, rule.Sensitivity) + } + } + } + } + } + + // Certificate signing keys are high sensitivity + if component.CryptoProperties != nil { + if component.CryptoProperties.AssetType == cdx.CryptoAssetTypeCertificate { + sensitivity = maxFloat(sensitivity, 0.7) + } + } + + return sensitivity +} + +// calculateCryptoLifetime estimates how long the cryptography needs to protect data +func (plugin *Plugin) calculateCryptoLifetime(component *cdx.Component) float64 { + // Default: assume 10 years of protection needed + lifetimeYears := 10.0 + + // Check certificate validity period + if component.CryptoProperties != nil && component.CryptoProperties.CertificateProperties != nil { + certProps := component.CryptoProperties.CertificateProperties + if certProps.NotValidAfter != "" && certProps.NotValidBefore != "" { + notAfter, err1 := time.Parse(time.RFC3339, certProps.NotValidAfter) + notBefore, err2 := time.Parse(time.RFC3339, certProps.NotValidBefore) + if err1 == nil && err2 == nil { + validityPeriod := notAfter.Sub(notBefore) + lifetimeYears = validityPeriod.Hours() / (24 * 365.25) + } + } + } + + // Normalize to 0-1 scale (assuming 20 years is maximum concern) + normalized := lifetimeYears / 20.0 + if normalized > 1.0 { + normalized = 1.0 + } + + return normalized +} + +// calculateVulnerabilityLevel determines how vulnerable the algorithm is +func (plugin *Plugin) calculateVulnerabilityLevel(component *cdx.Component) float64 { + // Check quantum status from properties + if component.Properties != nil { + for _, prop := range *component.Properties { + if prop.Name == "theia:pqc:quantum-status" { + switch QuantumVulnerabilityStatus(prop.Value) { + case QuantumVulnerable: + return 1.0 // Completely broken by Shor's algorithm + case QuantumPartiallySecure: + return 0.5 // Reduced security (Grover's) + case HybridTransitional: + return 0.2 // Hybrid provides some protection + case QuantumResistant, QuantumSafe: + return 0.0 // Not vulnerable + } + } + } + } + + // Default to high vulnerability for unclassified crypto + return 0.8 +} + +// calculateExposureLevel determines network exposure +func (plugin *Plugin) calculateExposureLevel(component *cdx.Component) float64 { + exposure := 0.5 // Default medium exposure + + // Check file path for exposure indicators + if component.Evidence != nil && component.Evidence.Occurrences != nil { + for _, occ := range *component.Evidence.Occurrences { + path := strings.ToLower(occ.Location) + + // Network-facing indicators + for _, indicator := range plugin.config.ExposureRules.NetworkFacingIndicators { + if strings.Contains(path, strings.ToLower(indicator)) { + exposure = maxFloat(exposure, 0.9) + } + } + + // Internal indicators + for _, indicator := range plugin.config.ExposureRules.InternalIndicators { + if strings.Contains(path, strings.ToLower(indicator)) { + exposure = minFloat(exposure, 0.3) + } + } + } + } + + // Check for server authentication (TLS) indicators + if component.Properties != nil { + for _, prop := range *component.Properties { + if strings.Contains(prop.Value, "serverAuth") || strings.Contains(prop.Value, "TLS") { + exposure = maxFloat(exposure, 0.85) + } + if strings.Contains(prop.Value, "clientAuth") { + exposure = minFloat(exposure, 0.4) + } + } + } + + return exposure +} + +// calculateMigrationPriority determines migration urgency +func (plugin *Plugin) calculateMigrationPriority(component *cdx.Component, risk *HNDLRiskScore) *MigrationPriority { + priority := &MigrationPriority{ + BlockingFactors: []string{}, + } + + // Base priority on risk score + priority.Score = risk.OverallScore * 10.0 // Convert to 0-100 + + // Adjust based on compliance deadlines + if plugin.config.Compliance.CNSA20.Enabled { + deadline := plugin.getApplicableCNSA20Deadline(component) + if deadline != nil { + priority.Deadline = deadline + daysRemaining := DaysUntil(deadline) + if daysRemaining < 365 { + priority.Score = maxFloat(priority.Score, 90.0) + } else if daysRemaining < 730 { + priority.Score = maxFloat(priority.Score, 75.0) + } + } + } + + // Determine priority category + switch { + case priority.Score >= 80: + priority.Priority = RiskCritical + case priority.Score >= 60: + priority.Priority = RiskHigh + case priority.Score >= 40: + priority.Priority = RiskMedium + default: + priority.Priority = RiskLow + } + + // Determine recommended migration path + priority.RecommendedPath = plugin.determineRecommendedPath(component, risk) + + // Identify blocking factors + priority.BlockingFactors = plugin.identifyBlockingFactors(component) + + return priority +} + +func (plugin *Plugin) getApplicableCNSA20Deadline(component *cdx.Component) *time.Time { + // Determine which CNSA 2.0 category applies + cfg := plugin.config.Compliance.CNSA20 + + // Check for signing usage + if component.Evidence != nil && component.Evidence.Occurrences != nil { + for _, occ := range *component.Evidence.Occurrences { + path := strings.ToLower(occ.Location) + if strings.Contains(path, "signing") || strings.Contains(path, "codesign") { + if deadline, err := ParseDeadline(cfg.SoftwareSigningDeadline); err == nil { + return deadline + } + } + } + } + + // Default to networking deadline for TLS-related crypto + if deadline, err := ParseDeadline(cfg.NetworkingDeadline); err == nil { + return deadline + } + + return nil +} + +func (plugin *Plugin) determineRecommendedPath(component *cdx.Component, risk *HNDLRiskScore) string { + // Check quantum status + if component.Properties != nil { + for _, prop := range *component.Properties { + if prop.Name == "theia:pqc:quantum-status" { + switch QuantumVulnerabilityStatus(prop.Value) { + case QuantumVulnerable: + if risk.OverallScore >= 7.0 { + return "hybrid-transition" // Immediate action needed + } + return "direct" // Can migrate directly + case HybridTransitional: + return "complete-transition" // Move from hybrid to pure PQC + } + } + } + } + + return "requires-analysis" +} + +func (plugin *Plugin) identifyBlockingFactors(component *cdx.Component) []string { + var factors []string + + // Check for legacy indicators + if component.Properties != nil { + for _, prop := range *component.Properties { + if prop.Name == "theia:pqc:classical-security-bits" { + if prop.Value == "80" || prop.Value == "64" { + factors = append(factors, "legacy-key-size") + } + } + } + } + + // Check for CA certificates (harder to migrate) + if component.CryptoProperties != nil && component.CryptoProperties.AssetType == cdx.CryptoAssetTypeCertificate { + if component.Properties != nil { + for _, prop := range *component.Properties { + if strings.Contains(prop.Name, "key-usage") && strings.Contains(prop.Value, "keyCertSign") { + factors = append(factors, "ca-certificate") + } + } + } + } + + return factors +} + +// enrichWithRisk adds risk scoring properties to a component +func (plugin *Plugin) enrichWithRisk(component *cdx.Component, risk *HNDLRiskScore) { + if component.Properties == nil { + component.Properties = &[]cdx.Property{} + } + + props := []cdx.Property{ + { + Name: "theia:pqc:hndl-risk-score", + Value: fmt.Sprintf("%.1f", risk.OverallScore), + }, + { + Name: "theia:pqc:hndl-risk-category", + Value: string(risk.Category), + }, + { + Name: "theia:pqc:risk-data-sensitivity", + Value: fmt.Sprintf("%.2f", risk.DataSensitivity), + }, + { + Name: "theia:pqc:risk-crypto-lifetime", + Value: fmt.Sprintf("%.2f", risk.CryptoLifetime), + }, + { + Name: "theia:pqc:risk-vulnerability-level", + Value: fmt.Sprintf("%.2f", risk.VulnerabilityLevel), + }, + { + Name: "theia:pqc:risk-exposure-level", + Value: fmt.Sprintf("%.2f", risk.ExposureLevel), + }, + } + + *component.Properties = append(*component.Properties, props...) +} + +// enrichWithMigrationPriority adds migration priority properties to a component +func (plugin *Plugin) enrichWithMigrationPriority(component *cdx.Component, priority *MigrationPriority) { + if component.Properties == nil { + component.Properties = &[]cdx.Property{} + } + + props := []cdx.Property{ + { + Name: "theia:pqc:migration-priority", + Value: string(priority.Priority), + }, + { + Name: "theia:pqc:migration-priority-score", + Value: fmt.Sprintf("%.1f", priority.Score), + }, + { + Name: "theia:pqc:migration-path", + Value: priority.RecommendedPath, + }, + } + + if len(priority.BlockingFactors) > 0 { + props = append(props, cdx.Property{ + Name: "theia:pqc:blocking-factors", + Value: strings.Join(priority.BlockingFactors, ","), + }) + } + + *component.Properties = append(*component.Properties, props...) +} + +func categorizeRisk(score float64) RiskCategory { + switch { + case score >= 8.0: + return RiskCritical + case score >= 6.0: + return RiskHigh + case score >= 4.0: + return RiskMedium + default: + return RiskLow + } +} + +func maxFloat(a, b float64) float64 { + if a > b { + return a + } + return b +} + +func minFloat(a, b float64) float64 { + if a < b { + return a + } + return b +} diff --git a/scanner/plugins/pqcreadiness/security_levels.go b/scanner/plugins/pqcreadiness/security_levels.go new file mode 100644 index 0000000..0d1d085 --- /dev/null +++ b/scanner/plugins/pqcreadiness/security_levels.go @@ -0,0 +1,204 @@ +// Copyright 2024 PQCA +// +// Licensed 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package pqcreadiness + +import ( + "fmt" + "strconv" + + cdx "github.com/CycloneDX/cyclonedx-go" +) + +// SecurityLevel represents computed security levels for a cryptographic component +type SecurityLevel struct { + ClassicalBits int // Classical (pre-quantum) security bits + QuantumBits int // Post-quantum security bits + NISTLevel int // NIST quantum security level (0-5) + EffectiveSecurityBits int // Minimum of classical/quantum considering threats + Confidence float64 // How confident we are in this assessment +} + +// calculateSecurityLevel computes security levels for a component +func (plugin *Plugin) calculateSecurityLevel(component *cdx.Component) *SecurityLevel { + level := &SecurityLevel{ + Confidence: 1.0, + } + + // First check if we already have security bits from vulnerability classification + if component.Properties != nil { + for _, prop := range *component.Properties { + switch prop.Name { + case "theia:pqc:classical-security-bits": + if bits, err := strconv.Atoi(prop.Value); err == nil { + level.ClassicalBits = bits + } + case "theia:pqc:quantum-security-bits": + if bits, err := strconv.Atoi(prop.Value); err == nil { + level.QuantumBits = bits + } + case "theia:pqc:nist-quantum-level": + if l, err := strconv.Atoi(prop.Value); err == nil { + level.NISTLevel = l + } + } + } + } + + // If we already have computed values, just calculate effective security + if level.ClassicalBits > 0 || level.QuantumBits > 0 { + level.calculateEffectiveSecurityBits() + return level + } + + // Otherwise, try to compute from algorithm information + algName := extractAlgorithmName(component) + keySize := extractKeySize(component) + curve := extractCurve(component) + + // Look up in database + if vuln := plugin.algorithmDB.Lookup(algName, "", keySize, curve); vuln != nil { + level.ClassicalBits = vuln.ClassicalSecurityBits + level.QuantumBits = vuln.QuantumSecurityBits + level.NISTLevel = vuln.NISTQuantumLevel + level.calculateEffectiveSecurityBits() + return level + } + + // Use inference + inferredVuln := plugin.inferVulnerability(algName, keySize, curve, component) + if inferredVuln != nil { + level.ClassicalBits = inferredVuln.ClassicalSecurityBits + level.QuantumBits = inferredVuln.QuantumSecurityBits + level.NISTLevel = inferredVuln.NISTQuantumLevel + level.Confidence = 0.7 // Lower confidence for inferred values + } + + level.calculateEffectiveSecurityBits() + return level +} + +// calculateEffectiveSecurityBits determines the effective security level +func (level *SecurityLevel) calculateEffectiveSecurityBits() { + // Effective security is the minimum of classical and quantum + // For quantum-vulnerable algorithms, quantum bits is 0 + if level.QuantumBits == 0 { + level.EffectiveSecurityBits = 0 // Quantum-vulnerable + } else if level.ClassicalBits == 0 { + level.EffectiveSecurityBits = level.QuantumBits + } else { + level.EffectiveSecurityBits = min(level.ClassicalBits, level.QuantumBits) + } +} + +// setSecurityLevels adds security level properties to a component +func (plugin *Plugin) setSecurityLevels(component *cdx.Component, level *SecurityLevel) { + if component.Properties == nil { + component.Properties = &[]cdx.Property{} + } + + // Check if properties already exist and update them + existingProps := make(map[string]bool) + for _, prop := range *component.Properties { + existingProps[prop.Name] = true + } + + props := []cdx.Property{} + + if !existingProps["theia:pqc:classical-security-bits"] && level.ClassicalBits > 0 { + props = append(props, cdx.Property{ + Name: "theia:pqc:classical-security-bits", + Value: fmt.Sprintf("%d", level.ClassicalBits), + }) + } + + if !existingProps["theia:pqc:quantum-security-bits"] { + props = append(props, cdx.Property{ + Name: "theia:pqc:quantum-security-bits", + Value: fmt.Sprintf("%d", level.QuantumBits), + }) + } + + if !existingProps["theia:pqc:nist-quantum-level"] { + props = append(props, cdx.Property{ + Name: "theia:pqc:nist-quantum-level", + Value: fmt.Sprintf("%d", level.NISTLevel), + }) + } + + // Always add effective security bits + props = append(props, cdx.Property{ + Name: "theia:pqc:effective-security-bits", + Value: fmt.Sprintf("%d", level.EffectiveSecurityBits), + }) + + // Add confidence if not perfect + if level.Confidence < 1.0 { + props = append(props, cdx.Property{ + Name: "theia:pqc:security-level-confidence", + Value: fmt.Sprintf("%.2f", level.Confidence), + }) + } + + *component.Properties = append(*component.Properties, props...) +} + +// GetSecurityClassification returns a human-readable security classification +func (level *SecurityLevel) GetSecurityClassification() string { + switch { + case level.EffectiveSecurityBits == 0: + return "quantum-vulnerable" + case level.EffectiveSecurityBits < 80: + return "weak" + case level.EffectiveSecurityBits < 112: + return "legacy" + case level.EffectiveSecurityBits < 128: + return "acceptable" + case level.EffectiveSecurityBits < 192: + return "strong" + case level.EffectiveSecurityBits < 256: + return "very-strong" + default: + return "maximum" + } +} + +// GetNISTLevelDescription returns a description for the NIST security level +func GetNISTLevelDescription(level int) string { + switch level { + case 0: + return "Not quantum-safe (broken by quantum computers)" + case 1: + return "At least as hard to break as AES-128 (NIST Level 1)" + case 2: + return "At least as hard to break as SHA-256/AES-128 (NIST Level 2)" + case 3: + return "At least as hard to break as AES-192 (NIST Level 3)" + case 4: + return "At least as hard to break as SHA-384/AES-192 (NIST Level 4)" + case 5: + return "At least as hard to break as AES-256 (NIST Level 5)" + default: + return "Unknown NIST level" + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/scanner/scanner.go b/scanner/scanner.go index fdba230..d661ce2 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -28,6 +28,7 @@ import ( "github.com/cbomkit/cbomkit-theia/scanner/plugins/certificates" "github.com/cbomkit/cbomkit-theia/scanner/plugins/javasecurity" "github.com/cbomkit/cbomkit-theia/scanner/plugins/opensslconf" + "github.com/cbomkit/cbomkit-theia/scanner/plugins/pqcreadiness" "github.com/cbomkit/cbomkit-theia/scanner/plugins/problematicca" "github.com/cbomkit/cbomkit-theia/scanner/plugins/secrets" log "github.com/sirupsen/logrus" @@ -62,6 +63,7 @@ func GetAllPluginConstructors() map[string]pluginpackage.PluginConstructor { "secrets": secrets.NewSecretsPlugin, "opensslconf": opensslconf.NewOpenSSLConfPlugin, "problematicca": problematicca.NewProblematicCAPlugin, + "pqcreadiness": pqcreadiness.NewPQCReadinessPlugin, } }