DeepViolet

March 26, 2026 · View on GitHub

Table of Contents

Documentation Notes

This document contains architecture and data flow diagrams written in Mermaid syntax. For the best viewing experience, use a Markdown renderer with Mermaid support.

Viewing Mermaid Diagrams in IntelliJ IDEA

  • IntelliJ IDEA 2023.1+ renders Mermaid diagrams natively in the Markdown preview.
  • For older versions, install the Mermaid plugin from Settings > Plugins > Marketplace (search "Mermaid").
  • Open any .md file and use the split editor (editor + preview) to see rendered diagrams alongside the source.

Viewing Mermaid Diagrams in Eclipse

  • Install the Markdown Text Editor plugin from the Eclipse Marketplace, which includes Mermaid rendering support.
  • Alternatively, install the Mylyn Docs Markdown editor and pair it with a browser-based preview that supports Mermaid (e.g., the Markdown Preview view).
  • If diagrams still appear as raw code blocks, copy the fenced mermaid block into the Mermaid Live Editor to view and export the diagram.

Overview

DeepViolet is a TLS/SSL scanning API written in Java that provides programmatic introspection of TLS/SSL connections. The library enables developers to:

  • Enumerate supported cipher suites on remote servers
  • Analyze X.509 certificate chains and trust status
  • Assess cipher suite encryption strength
  • Support multiple cipher suite naming conventions (IANA, OpenSSL, GnuTLS, NSS)

While established tools like Qualys Labs, Mozilla Observatory, and OpenSSL already provide TLS/SSL scanning capabilities, few open-source Java APIs offer straightforward scanning solutions. DeepViolet fills that gap, providing a developer-friendly alternative that can be integrated directly into Java applications.

Reference implementations are available through the DeepVioletTools project for both command-line and graphical usage.

Features

Information Gathering

  • TLS Connection Characteristics -- Socket and protocol details including SO_KEEPALIVE, SO_RCVBUF, SO_LINGER, SO_TIMEOUT, SO_REUSEADDR, SO_SENDBUFF, CLIENT_AUTH_REQ, CLIENT_AUTH_WANT, TRAFFIC_CLASS, TCP_NODELAY, ENABLED_PROTOCOLS, DEFLATE_COMPRESSION
  • X.509 Certificate Metadata -- Validity, SubjectDN, IssuerDN, Serial Number, Signature Algorithm, Signature Algorithm OID, Certificate Version, Certificate Fingerprint, Critical/Non-Critical OID sections
  • TLS Cipher Suite Naming -- Returns readable cipher suite names in GnuTLS, NSS, OpenSSL, and IANA conventions
  • Web Server Cipher Suite Identification -- Enumerates server cipher suites with strength assessment (built into the project, not fetched at runtime)
  • Certificate Validity Assessment -- Status on certificate validity and expiration date ranges
  • Trust Chain Verification -- Confirms whether certificates chain back to trusted roots

Server Analysis

  • Encryption Strength Evaluation -- Measures minimal and achievable encryption strength
  • TLS Risk Scoring -- Quantitative risk assessment across 7 categories with configurable scoring policy, letter grades, and per-category breakdowns
  • TLS Fingerprinting -- 30-character behavioral fingerprint based on 10 probe responses, characterizing cipher selection, extension ordering, and version negotiation patterns
  • Post-Quantum Key Exchange Analysis -- Probes server support for ML-KEM hybrid and pure post-quantum key exchange groups (X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024, MLKEM768, MLKEM1024), detects PQ preference, and reports negotiated PQ group
  • DNS Security Checks -- CAA (Certificate Authority Authorization) and DANE/TLSA record lookups
  • Certificate Revocation Checking -- OCSP, CRL, OCSP stapling, Must-Staple, OneCRL, and Certificate Transparency (SCT) verification across embedded, TLS extension, and OCSP stapling delivery methods
  • TLS_FALLBACK_SCSV Detection -- Tests for RFC 7507 downgrade protection

Scanning

  • Multi-Host Parallel Scanning -- Scan multiple targets concurrently using a cached thread pool with configurable concurrency, per-host timeouts, and section-level delays
  • Flexible Target Parsing -- Accepts hostnames, IPs, IPv6, host:port, CIDR notation (e.g., 10.0.0.0/24), and IP ranges (e.g., 192.168.1.1-192.168.1.10)
  • Section-Based Execution -- Scans broken into 7 discrete phases (session init, cipher enumeration, certificate retrieval, risk scoring, TLS fingerprinting, DNS security, revocation check) that can be individually enabled or disabled
  • Event-Driven Monitoring -- Callback listener (IScanListener) for host-started, section-started, section-completed, host-completed, and scan-completed events
  • Polling-Based Monitoring -- Thread-safe monitor (IScanMonitor) exposing active/sleeping/idle thread counts and per-thread status snapshots for UI timer integration

Scan Control

  • Cooperative Cancellation -- Cancel in-progress scans via BackgroundTask.cancel(); scan methods check isCancelled() at natural boundaries and bail out gracefully
  • Cooperative Pause -- Pause scans via BackgroundTask.pause(); scan methods check isPaused() at natural boundaries

Requirements

  • Java 17 or higher
  • Apache Maven 3.6.3 or higher

Building from Source

Compile and Test

mvn clean verify

Compile Only

mvn compile

Run Tests

# All tests
mvn test

# Single test class
mvn test -Dtest=CipherMapTest

Generate Javadocs

mvn javadoc:javadoc

Generated documentation will be located at docs/javadocs/.

Package JAR

mvn package

This creates two JAR files in the target/ directory:

  • DeepViolet-*-SNAPSHOT.jar -- DeepViolet binary only
  • DeepViolet-*-jar-with-dependencies.jar -- Binary plus all project dependencies

Projects that consume the DeepViolet API (such as DeepVioletTools) use the locally compiled JAR.

Build Validation Tool

mvn package -Pvalidate

This creates an additional JAR: DeepViolet-*-validate.jar — a standalone tool that compares DV API results against openssl for accuracy verification. See the API Validation Tool section at the end of this document.

Reference Tools

GUI and command-line tools that consume this API are available in the DeepVioletTools project.

Architecture

Architecture Diagram

flowchart TB
    subgraph Client["Client Application"]
        APP[Application Code]
    end

    subgraph ScanAPI["Scanning"]
        BS[TlsScanner]
        BC[ScanConfig]
        BTS[TargetSpec]
        BM[IScanMonitor]
        BL[IScanListener]
    end

    subgraph API["DeepViolet API"]
        DVF[DeepVioletFactory]
        DVE[DeepVioletEngine]
        DVS[ISession]
        DVC[IX509Certificate]
        DVCS[ICipherSuite]
        DBT[BackgroundTask]
    end

    subgraph Core["Core Components"]
        CSU[CipherSuiteUtil]
        SM[ServerMetadata]
        HD[HostData]
    end

    subgraph Checks["Security Checks"]
        RS[RiskScorer]
        FP[TlsServerFingerprint]
        RC[RevocationChecker]
        DC[DnsSecurityChecker]
    end

    subgraph TlsProto["Raw TLS Protocol"]
        DTS[TlsSocket]
        DTM[TlsMetadata]
    end

    subgraph Resources["Resources"]
        CM[ciphermap.yaml]
        RY[risk-scoring-rules.yaml]
    end

    subgraph Validate["Validation Tool"]
        AV[ApiValidator]
        OR[OpensslRunner]
        FN[FieldNormalizer]
    end

    subgraph Target["Target Server"]
        TLS[TLS/SSL Endpoint]
        DNS[DNS Server]
    end

    APP --> DVF
    APP --> BS
    AV --> DVF
    AV --> OR
    AV --> FN
    OR --> TLS
    BS --> BTS
    BS --> BC
    BS --> DVF
    BS --> BM
    BS --> BL
    DVF --> DVS
    DVF --> DVE
    DVE --> DVCS
    DVE --> DVC
    DVE --> DBT
    DVE --> RS
    DVE --> FP
    DVE --> RC
    DVE --> DC
    DVE --> CSU
    RS --> RY
    FP --> DTS
    DTS --> DTM
    CSU --> SM
    CSU --> HD
    CSU --> CM
    CSU --> TLS
    DTS --> TLS
    DC --> DNS
    RC --> TLS

Core Components

DeepVioletFactory

Location: src/main/java/com/mps/deepviolet/api/DeepVioletFactory.java

The factory class serves as the entry point for all DeepViolet API operations.

MethodDescription
initializeSession(URL)Creates an immutable session by connecting to a target host; captures OCSP stapled response and SCTs during handshake
getEngine(ISession)Returns an engine instance for TLS analysis
getEngine(ISession, CIPHER_NAME_CONVENTION)Returns engine with specific naming convention
getEngine(ISession, CIPHER_NAME_CONVENTION, BackgroundTask)Returns engine with progress callback
getEngine(ISession, CIPHER_NAME_CONVENTION, BackgroundTask, Set<Integer>)Returns engine with progress callback and protocol version filtering
loadCipherMap(InputStream)Load a custom cipher map from a stream, fully replacing the built-in map
resetCipherMap()Reset the cipher map to the built-in default

ISession

Location: src/main/java/com/mps/deepviolet/api/ISession.java

The session interface represents an immutable connection context containing:

  • Target URL and host information
  • Socket configuration properties
  • Negotiated protocol and cipher suite
  • OCSP stapled response (captured during TLS handshake)
  • HTTP response headers

Key Methods:

MethodDescription
getHostInterfaces()All host interfaces for the target
getSessionPropertyValue(SESSION_PROPERTIES)Return a session property value
getURL()URL used to initialize the session
getHttpResponseHeaders()HTTP(S) response headers captured during initialization
getStapledOcspResponse()OCSP stapled response bytes from TLS handshake, or null

Key Enums:

EnumValues
SESSION_PROPERTIESSO_KEEPALIVE, SO_RCVBUF, SO_LINGER, TCP_NODELAY, DEFLATE_COMPRESSION, NEGOTIATED_PROTOCOL, NEGOTIATED_CIPHER_SUITE, etc.
CIPHER_NAME_CONVENTIONIANA, OpenSSL, GnuTLS, NSS

IEngine

Location: src/main/java/com/mps/deepviolet/api/IEngine.java

The engine interface provides TLS scanning functionality:

MethodDescription
getCipherSuites()Returns array of supported cipher suites
getCertificate()Returns server's X.509 certificate
writeCertificate(String)Exports PEM-encoded certificate to file
getRiskScore()Computes risk score using default policy
getRiskScore(String)Computes risk score using custom policy file
getRiskScore(InputStream)Computes risk score merging user rules with defaults
getTlsFingerprint()Computes 30-character TLS server fingerprint
getSCTs()Returns Signed Certificate Timestamps from all sources
getTlsMetadata()Returns detailed TLS metadata from raw protocol parsing
getFallbackScsvSupported()Tests for TLS_FALLBACK_SCSV support (RFC 7507)
getPqKeyExchangeSupported()Tests whether the server supports post-quantum key exchange
getPqKeyExchangeGroups()Returns list of PQ key exchange group names the server supports
getPqKeyExchangePreferred()Tests whether the server prefers PQ over classical key exchange
getPqPreferredGroup()Returns the PQ group name the server prefers
getDnsStatus()Returns DNS security status (CAA, DANE/TLSA records)
buildRuleContext()Build a RuleContext snapshot for persistence or offline re-scoring
getRiskScore(RuleContext)Compute risk score from a pre-built RuleContext (offline)
getRiskScore(RuleContext, InputStream)Offline re-scoring with user rules merged
getRiskScore(Set<ScanSection>)Compute risk score with knowledge of which sections failed
getDeepVioletMajorVersion()Returns API major version

IX509Certificate

Location: src/main/java/com/mps/deepviolet/api/IX509Certificate.java

Comprehensive X.509 certificate representation providing:

  • Certificate metadata (subject DN, issuer DN, serial number)
  • Validity status (valid, expired, not yet valid)
  • Trust status (trusted, untrusted, unknown)
  • Certificate chain access
  • OID extraction (critical and non-critical)
  • Fingerprint generation

CipherSuiteUtil

Location: src/main/java/com/mps/deepviolet/api/CipherSuiteUtil.java

Internal utility class handling:

  • TLS handshake protocol implementation
  • Cipher suite enumeration via iterative probing
  • Certificate chain retrieval
  • Cipher strength classification

TlsScanner

Location: src/main/java/com/mps/deepviolet/api/TlsScanner.java

TLS scanner that scans multiple hosts in parallel using a cached thread pool with a semaphore to cap concurrency.

MethodDescription
scan(Collection<String>)Scan targets with default configuration
scan(Collection<String>, ScanConfig)Scan targets with custom configuration
scan(Collection<String>, ScanConfig, IScanListener)Scan with configuration and event listener
scan(List<URL>, ScanConfig, IScanListener)Scan pre-parsed URLs
scanAsync(Collection<String>, ScanConfig, IScanListener)Async variant returning CompletableFuture
getMonitor()Get the global IScanMonitor for polling progress

Each host scan executes up to 7 sections (see ScanSection):

SectionDescription
SESSION_INITSession initialization
CIPHER_ENUMERATIONCipher suite enumeration
CERTIFICATE_RETRIEVALCertificate retrieval
RISK_SCORINGRisk scoring
TLS_FINGERPRINTTLS fingerprinting
DNS_SECURITYDNS security check
REVOCATION_CHECKRevocation check

ScanConfig

Location: src/main/java/com/mps/deepviolet/api/ScanConfig.java

Configuration for scanning, created via ScanConfig.builder():

SettingDefaultDescription
threadCount10Number of concurrent virtual threads
sectionDelayMs200Milliseconds to sleep between sections on the same host
perHostTimeoutMs60000Max time for a single host scan
cipherNameConventionIANACipher suite naming convention
enabledProtocolsnull (all)TLS protocol versions to probe
enabledSectionsallWhich scan sections to execute
maxRetries3Max retry attempts per section (0 disables)
initialRetryDelayMs500Initial delay before first retry (ms)
maxRetryDelayMs4000Maximum delay between retries (ms)
retryBudgetMs15000Total wall-clock budget for retries per section (ms)

RetryPolicy

Location: src/main/java/com/mps/deepviolet/api/RetryPolicy.java

Configurable retry policy with exponential backoff and jitter for transient IOException failures. Does not retry TlsException or RuntimeException.

MethodDescription
defaults()Create policy with defaults (3 retries, 500ms initial, 4s max, 15s budget)
disabled()Create disabled policy (no retries)
builder()Create a new builder
execute(Callable<T>, BackgroundTask)Execute a task with retry
executeVoid(RunnableWithException, BackgroundTask)Execute a void task with retry

ScanConfig builds a RetryPolicy internally via toRetryPolicy(). You can also create one directly:

RetryPolicy policy = RetryPolicy.builder()
        .maxRetries(5)
        .initialDelayMs(200)
        .maxDelayMs(8000)
        .retryBudgetMs(30000)
        .build();

TargetSpec

Location: src/main/java/com/mps/deepviolet/api/TargetSpec.java

Parses flexible target specification strings into URLs:

FormatExampleResult
Hostnamegithub.comhttps://github.com:443
Hostname + portgithub.com:8443https://github.com:8443
Full URLhttps://example.com/As-is
IPv4192.168.1.1https://192.168.1.1:443
IPv6[::1]:8443https://[::1]:8443
IP range192.168.1.1-192.168.1.1010 targets (up to 65,534 hosts)
CIDR10.0.0.0/24254 targets (.1-.254)
CIDR + port10.0.0.0/24:636254 targets on port 636
MethodDescription
parse(String)Parse a single spec into URLs (default port 443)
parse(String, int)Parse with custom default port
parseAll(Collection<String>)Parse multiple specs, deduplicated

BackgroundTask

Location: src/main/java/com/mps/deepviolet/api/BackgroundTask.java

Background task with cooperative cancel and pause support:

MethodDescription
cancel()Request cooperative cancellation
isCancelled()Check if cancelled
pause()Request cooperative pause
isPaused()Check if paused
setStatusBarMessage(String)Set status bar text for UI
isWorking()Check if the background thread is still running

Scan methods check isCancelled() and isPaused() at natural boundaries and bail out gracefully.

IScanListener

Location: src/main/java/com/mps/deepviolet/api/IScanListener.java

Callback interface for scan events. All methods have default no-op implementations:

MethodDescription
onHostStarted(URL, int, int)A host scan is starting
onSectionStarted(URL, ScanSection)A section is starting on a host
onSectionCompleted(URL, ScanSection)A section completed on a host
onHostCompleted(IScanResult, int, int)A host scan completed (success or failure)
onScanCompleted(List<IScanResult>)All hosts scanned
onSectionFailed(URL, ScanSection, int, Exception)A section failed after all retry attempts
onHostStatus(URL, String)Status text from the scanning engine

IScanMonitor

Location: src/main/java/com/mps/deepviolet/api/IScanMonitor.java

Pollable interface for monitoring scan progress, suitable for UI timer integration:

MethodDescription
getActiveThreadCount()Threads actively executing a section
getSleepingThreadCount()Threads in per-host section delay
getIdleThreadCount()Threads waiting for work
getCompletedHostCount()Hosts completed so far
getTotalHostCount()Total hosts in the scan
isRunning()Whether the scan is still running
getThreadStatuses()Snapshot of per-thread status

NamedGroup

Location: src/main/java/com/mps/deepviolet/api/tls/NamedGroup.java

Constants and helpers for IANA TLS Named Groups (RFC 8446 Section 4.2.7), including post-quantum hybrid and pure PQ groups:

GroupCodeType
SECP256R10x0017Classical EC
SECP384R10x0018Classical EC
X255190x001dClassical EC
X25519_MLKEM7680x11ECPQ Hybrid
SECP256R1_MLKEM7680x11EBPQ Hybrid
SECP384R1_MLKEM10240x11EDPQ Hybrid
MLKEM7680x0201Pure PQ
MLKEM10240x0202Pure PQ
MethodDescription
getName(int)Human-readable name for a group code
isPostQuantum(int)Test whether a group code is PQ or hybrid-PQ
classicalFallback(int)Return the classical fallback for a PQ group

DnsSecurityChecker

Location: src/main/java/com/mps/deepviolet/api/DnsSecurityChecker.java

Checks for DNS security records using JNDI DNS lookups:

  • CAA Records -- Certificate Authority Authorization, restricting which CAs can issue certificates
  • TLSA Records -- DANE (DNS-Based Authentication of Named Entities), binding X.509 certificates to DNS via DNSSEC

RevocationChecker

Location: src/main/java/com/mps/deepviolet/api/RevocationChecker.java

Performs comprehensive certificate revocation and transparency checks:

  • OCSP -- Online Certificate Status Protocol queries
  • CRL -- Certificate Revocation List download and lookup
  • OCSP Stapling -- Parses stapled response from TLS handshake
  • Must-Staple -- Detects TLS Feature extension (OID 1.3.6.1.4.1.5.5.7.1.24)
  • OneCRL -- Mozilla's centralized revocation service
  • Certificate Transparency -- Signed Certificate Timestamps (SCTs) from embedded, TLS extension, and OCSP stapling sources

Data Flow

sequenceDiagram
    participant App as Application
    participant DVF as DeepVioletFactory
    participant DVE as DeepVioletEngine
    participant CSU as CipherSuiteUtil
    participant Server as TLS Server

    App->>DVF: initializeSession(URL)
    DVF->>Server: TCP Connect
    DVF->>Server: TLS Handshake
    Server-->>DVF: Certificate + Session Info
    DVF-->>App: ISession

    App->>DVF: getEngine(session)
    DVF->>CSU: getServerMetadataInstance()

    loop For each TLS version (SSLv3 to TLS 1.3)
        CSU->>Server: ClientHello
        Server-->>CSU: ServerHello + Cipher
    end

    CSU-->>DVF: ServerMetadata
    DVF-->>App: IEngine

    App->>DVE: getCipherSuites()
    DVE-->>App: ICipherSuite[]

    App->>DVE: getCertificate()
    DVE->>CSU: getServerCertificate()
    CSU-->>DVE: X509Certificate
    DVE-->>App: IX509Certificate

Scanning Data Flow

sequenceDiagram
    participant App as Application
    participant BS as TlsScanner
    participant TS as TargetSpec
    participant DVF as DeepVioletFactory
    participant DVE as DeepVioletEngine
    participant Mon as IScanMonitor
    participant Lst as IScanListener

    App->>BS: scan(targets, config, listener)
    BS->>TS: parseAll(targets)
    TS-->>BS: List<URL>

    par For each host (virtual threads)
        BS->>Lst: onHostStarted(url)
        BS->>DVF: initializeSession(url)
        DVF-->>BS: ISession
        BS->>Lst: onSectionCompleted(url, SESSION_INIT)
        BS->>DVF: getEngine(session)
        DVF-->>BS: IEngine
        BS->>Lst: onSectionCompleted(url, CIPHER_ENUMERATION)
        BS->>DVE: getRiskScore()
        BS->>DVE: getTlsFingerprint()
        BS->>DVE: getDnsStatus()
        BS->>Lst: onHostCompleted(result)
        BS->>Mon: incrementCompleted()
    end

    BS->>Lst: onScanCompleted(results)
    BS-->>App: List<IScanResult>

Configuration Resources

ciphermap.yaml

Location: src/main/resources/ciphermap.yaml

Unified cipher suite map containing hex IDs, name mappings (IANA, OpenSSL, GnuTLS, NSS), strength evaluations, and TLS version annotations.

metadata:
  version: "1.0"
  description: "DeepViolet unified cipher suite map"
  last_updated: "2026-02-11"

cipher_suites:
  # TLS 1.3 ciphers
  - id: "0x13,0x01"
    names:
      IANA: "TLS_AES_128_GCM_SHA256"
      OpenSSL: "TLS_AES_128_GCM_SHA256"
      GnuTLS: "TLS_AES_128_GCM_SHA256"
      NSS: "TLS_AES_128_GCM_SHA256"
    strength: STRONG
    tls_versions: ["TLSv1.3"]

Strength values: STRONG, MEDIUM, WEAK, CLEAR (no encryption), UNASSIGNED.

Modifying a Cipher Suite's Strength

If your organization's security policy requires different strength classifications, edit the strength field for the relevant cipher suites in ciphermap.yaml directly. The file is designed to be user-editable. No code changes are required -- CipherSuiteUtil reads strength values from the YAML at runtime.

For example, to reclassify TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 from MEDIUM to STRONG:

  - id: "0x00,0x9E"
    names:
      IANA: "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
      OpenSSL: "DHE-RSA-AES128-GCM-SHA256"
      GnuTLS: "TLS_DHE_RSA_AES_128_GCM_SHA256"
      NSS: "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"
    strength: STRONG          # changed from MEDIUM
    tls_versions: ["TLSv1.2"]

Adding a New Cipher Suite

Append a new entry to the cipher_suites list. Each entry requires an id (hex pair), names (with at least the IANA name), strength, and tls_versions:

  # Example: adding a hypothetical new cipher
  - id: "0xCA,0xFE"
    names:
      IANA: "TLS_EXAMPLE_WITH_AES_256_GCM_SHA384"
      OpenSSL: "EXAMPLE-AES256-GCM-SHA384"
      GnuTLS: ""
      NSS: ""
    strength: STRONG
    tls_versions: ["TLSv1.3"]

If a naming convention has no known name for a cipher, use an empty string ("").

Proposing Changes to Default Strengths

If you believe a default classification is incorrect, open an issue explaining:

  1. Which cipher suite(s) should change and to what strength
  2. Your technical rationale with supporting references (RFCs, CVEs, industry guidance)

If the proposal is approved, submit a pull request to ciphermap.yaml and include a reference to the approved issue. Pull requests that modify ciphermap.yaml strength values without a corresponding approved issue will be denied.

Protocol Support

ProtocolVersion CodeStatus
SSLv20x0200Legacy (deprecated)
SSLv30x0300Legacy (deprecated)
TLS 1.00x0301Legacy
TLS 1.10x0302Legacy
TLS 1.20x0303Current
TLS 1.30x0304Current

Extension Points

Custom Background Tasks

Implement BackgroundTask to receive progress callbacks during scanning:

BackgroundTask task = new BackgroundTask() {
    @Override
    public void setStatusBarMessage(String message) {
        // Handle progress updates
    }
};
IEngine eng = DeepVioletFactory.getEngine(session, IANA, task);

Cancelling and Pausing Scans

Use the cooperative cancel and pause methods on BackgroundTask:

BackgroundTask task = new BackgroundTask() { ... };
IEngine eng = DeepVioletFactory.getEngine(session, IANA, task);

// Cancel the scan from another thread
task.cancel();

// Or pause it
task.pause();

Scan methods check isCancelled() and isPaused() at natural boundaries and stop gracefully.

Scanning

Scan multiple hosts in parallel with configurable concurrency and event monitoring:

ScanConfig config = ScanConfig.builder()
    .threadCount(5)
    .perHostTimeoutMs(30000)
    .sectionDelayMs(100)
    .build();

List<IScanResult> results = TlsScanner.scan(
    List.of("github.com", "google.com", "10.0.0.0/24:636"),
    config,
    new IScanListener() {
        @Override
        public void onHostCompleted(IScanResult result, int done, int total) {
            System.out.printf("[%d/%d] %s%n", done, total, result.getURL());
        }
    }
);

Poll progress during a scan using the monitor:

IScanMonitor monitor = TlsScanner.getMonitor();
while (monitor.isRunning()) {
    System.out.printf("Progress: %d/%d  Active: %d  Sleeping: %d%n",
        monitor.getCompletedHostCount(), monitor.getTotalHostCount(),
        monitor.getActiveThreadCount(), monitor.getSleepingThreadCount());
    Thread.sleep(1000);
}

Cipher Naming Conventions

Select the cipher suite naming convention via the CIPHER_NAME_CONVENTION enum:

IEngine eng = DeepVioletFactory.getEngine(session, CIPHER_NAME_CONVENTION.OpenSSL);

Risk Scoring

Scoring Overview

DeepViolet provides a quantitative TLS risk scoring system that evaluates a server's TLS configuration using an average-based model with severity floors. The score is mapped to a letter grade (A+ through F) and a risk level (LOW, MEDIUM, HIGH, CRITICAL).

Each rule has a normalized score (0.0-1.0) indicating its severity. When rules match, their scores are averaged, converted to a 0-100 scale, and capped by a severity floor derived from the highest-scoring matched rule. A perfect server configuration with no findings scores 100/100 (grade A+).

Algorithm:

  1. Collect all matched rules and their scores (0.0-1.0)
  2. Average the scores: avg = sum(scores) / count(scores)
  3. Find the highest score among matched rules and look up its severity floor
  4. Convert to 0-100: numeric_score = 100 * (1.0 - avg)
  5. Apply floor: final_score = min(numeric_score, floor)
  6. Map final_score to a letter grade via grade_mapping

If no rules match, the score is 100 (perfect). Categories compute their own sub-averages for display (same formula, steps 1-4, no floor). Adding new rules to a category never requires rebalancing existing score values.

All scoring rules -- including their conditions, score values, and grade boundaries -- are defined in a single YAML file (risk-scoring-rules.yaml). Severity is derived at runtime from the severity_mapping section. No code changes are required to add, modify, disable, or remove rules.

Rules file: src/main/resources/risk-scoring-rules.yaml Scoring engine: src/main/java/com/mps/deepviolet/api/scoring/

Rule Identifiers

Every rule has a stable identifier in the format SYS-NNNNNNN (system rules shipped in the JAR) or USR-NNNNNNN (user-defined rules). For example, SYS-0000100 is the SSLv2 detection rule. These IDs are permanent and must never be reassigned to a different rule. IDs increment by 100 to allow inserting new rules between existing ones. When adding new rules, use the next available ID noted in the metadata section of risk-scoring-rules.yaml or an unused ID between existing rules within the same category. Rule IDs appear in deduction output via IDeduction.getRuleId(), making it easy to reference specific findings unambiguously.

Each rule has a score field (0.0-1.0) that indicates the normalized severity of the finding. Severity labels (CRITICAL, HIGH, MEDIUM, LOW, INFO) are derived at runtime from the severity_mapping section rather than being hardcoded per rule.

Severity Mapping

The severity_mapping section maps rule score ranges to severity labels and overall score floors:

SeverityMin ScoreFloorDescription
CRITICAL0.865Scores >= 0.8 cap the total at 65
HIGH0.575Scores >= 0.5 cap the total at 75
MEDIUM0.285Scores >= 0.2 cap the total at 85
LOW0.01100No cap applied
INFO0.0100Informational, no cap applied

The floor ensures that a single critical finding (e.g., an expired certificate with score 1.0) prevents the overall score from exceeding 65, regardless of how many other rules pass.

Grade Mapping

GradeMinimum ScoreRisk Level
A+95LOW
A90LOW
B80MEDIUM
C70HIGH
D60CRITICAL
F0CRITICAL

Scoring Categories

The system evaluates 7 categories. Each category computes its own sub-average independently:

CategoryDescription
Protocols & ConnectionsTLS/SSL protocol version support
Cipher SuitesCipher strength and selection
Certificate & ChainCertificate validity, trust, key strength
Revocation & TransparencyOCSP, CRL, Certificate Transparency
Security HeadersHSTS, CSP, X-Frame-Options
DNS SecurityCAA records, DANE/TLSA
OtherCompression, SAN exposure, fingerprinting

Protocols & Connections

IDRuleScoreMotivationCondition
SYS-0000100SSLv2 supported1.0Prohibited by RFC 6176. Fundamental flaws including unprotected handshakes enabling MITM cipher downgrade.protocols contains "SSLv2"
SYS-0000200SSLv3 supported0.9Deprecated by RFC 7568 ("MUST NOT be used"). POODLE attack (CVE-2014-3566) enables plaintext recovery via non-deterministic CBC padding; RC4 (its only stream cipher) has fatal biases.protocols contains "SSLv3"
SYS-0000300TLS 1.0 supported0.6Deprecated by RFC 8996. No AEAD cipher support, SHA-1 handshake integrity. NIST SP 800-52r2 Section 3.1 prohibits for federal systems; PCI DSS required migration by June 2018.protocols contains "TLSv1.0"
SYS-0000400TLS 1.1 supported0.5Deprecated by RFC 8996 alongside TLS 1.0 for the same reasons. NIST SP 800-52r2 Section 3.1 likewise prohibits.protocols contains "TLSv1.1"
SYS-0000500TLS 1.3 not supported0.5RFC 8446 mandates AEAD-only ciphers, removes static RSA (guaranteeing PFS), encrypts handshake messages, eliminates compression/renegotiation/SHA-1/CBC. NIST SP 800-52r2 requires TLS 1.3 support by January 2024.protocols not contains "TLSv1.3"
SYS-0000600TLS 1.2 negotiated instead of 1.30.2RFC 9325 Section 3.1 recommends TLS 1.3 as preferred version. TLS 1.3 eliminates entire attack classes present in 1.2 (CBC padding oracles, static RSA, renegotiation). Negotiating 1.2 when 1.3 is available may indicate misconfiguration.protocols contains "TLSv1.3" and contains(session.negotiated_protocol, "TLSv1.2")
SYS-0000700Secure renegotiation not supported on TLS 1.20.7RFC 5746 fixes CVE-2009-3555 (MITM injection via unbound renegotiation). RFC 9325 Section 3.5 mandates renegotiation_info. Not applicable to TLS 1.3 which removed renegotiation entirely.session.tls_metadata_available == true and session.renegotiation_info_present == false and not contains(session.negotiated_protocol, "TLSv1.3")
SYS-0000800TLS 1.3 early data (0-RTT) accepted0.4RFC 8446 Section 8.1 warns 0-RTT has "no guarantee of non-replay between connections." RFC 8470 details HTTP replay risks.session.tls_metadata_available == true and session.early_data_accepted == true
SYS-0000900ALPN not negotiated0.1RFC 7301 defines ALPN; RFC 9113 Section 3.2 requires it for HTTP/2. Absence prevents HTTP/2 negotiation and indicates a less capable TLS stack.session.tls_metadata_available == true and session.alpn_negotiated == null
SYS-0001000Server does not support TLS_FALLBACK_SCSV0.3RFC 7507 defines TLS_FALLBACK_SCSV to prevent protocol downgrade attacks, most notably POODLE (CVE-2014-3566). Servers reject downgraded connections with inappropriate_fallback alert.session.fallback_scsv_supported == false
SYS-0001100Post-quantum key exchange supported but server prefers classical0.2Server supports PQ groups but negotiates classical when both are offered. RFC-ietf-tls-ecdhe-mlkem-04. Servers should prefer PQ to gain quantum-resistance benefits.session.pq_kex_supported == true and session.pq_kex_preferred == false
SYS-0001200Server does not support post-quantum key exchange0.3ML-KEM hybrids (X25519MLKEM768, etc.) provide quantum-resistance for TLS 1.3 connections. NIST FIPS 203 standardized ML-KEM in August 2024. Absence means no protection against harvest-now-decrypt-later attacks.session.pq_kex_supported == false
SYS-0001300Server supports post-quantum key exchange0.0Informational — server supports PQ key exchange groups. Score 0.0 (positive finding).session.pq_kex_supported == true
SYS-0001400Server negotiated post-quantum key exchange0.0Informational — server prefers PQ when offered both PQ and classical. Score 0.0 (positive finding).session.pq_kex_preferred == true
SYS-0001500Post-quantum probe failed0.15PQ key exchange could not be evaluated (probe failed after retries). Inconclusive diagnostic.session.pq_kex_probe_failed == true

Cipher Suites

IDRuleScoreMotivationCondition
SYS-0010100NULL/CLEAR ciphers offered0.9RFC 9325 Section 4.1: "MUST NOT negotiate cipher suites with NULL encryption." Zero confidentiality despite TLS wrapper. NIST SP 800-52r2 Section 3.3.1 likewise excludes NULL encryption.count(ciphers, strength == "CLEAR") > 0
SYS-00102006+ WEAK ciphers offered0.7RFC 9325 Section 4.1 prohibits ciphers below 112 bits. A large weak cipher set increases downgrade attack surface. NIST SP 800-131A Rev. 2 disallows below 112 bits after 2023.count(ciphers, strength == "WEAK") >= 6
SYS-00103001-5 WEAK ciphers offered0.3Same basis as SYS-0010200 at reduced severity. Even a few weak ciphers create downgrade attack surface per RFC 9325 Section 4.1 and NIST SP 800-52r2.count(ciphers, strength == "WEAK") > 0 and count(ciphers, strength == "WEAK") < 6
SYS-0010400No STRONG ciphers available0.6NIST SP 800-52r2 Section 3.3.1 requires AES-128/256 with GCM or CCM. RFC 9325 Section 4.2 recommends AEAD with 128+ bit keys as baseline.count(ciphers) > 0 and count(ciphers, strength == "STRONG") == 0
SYS-0010500Negotiated cipher is WEAK0.6RFC 9325 Section 4.1 MUST NOT language applies to the negotiated result. NIST SP 800-131A Rev. 2 classifies below 112 bits as "disallowed."session.negotiated_cipher_strength == "WEAK"
SYS-0010600Negotiated cipher is MEDIUM0.2NIST SP 800-131A Rev. 2 deprecates medium-strength ciphers (e.g., 3DES). SWEET32 birthday attack (CVE-2016-2183) exploits 64-bit block ciphers at ~32 GB of data.session.negotiated_cipher_strength == "MEDIUM"
SYS-0010700RC4 ciphers offered0.8RFC 7465 categorically bans RC4: "MUST NOT include" / "MUST NOT select." Multiple statistical biases enable plaintext recovery. NIST SP 800-52r2 also excludes RC4.count(ciphers, name contains "RC4") > 0
SYS-0010800DES ciphers offered0.7DES uses 56-bit key, far below NIST SP 800-131A Rev. 2 minimum of 112 bits. 3DES vulnerable to SWEET32 (CVE-2016-2183) via 64-bit block size.count(ciphers, name contains "_DES_") > 0
SYS-0010900EXPORT ciphers offered0.9RFC 9325 Section 4.1 prohibits export-level encryption (40/56 bits). Central to FREAK (CVE-2015-0204) and Logjam (CVE-2015-4000) downgrade attacks.count(ciphers, name contains "EXPORT") > 0
SYS-0011000Only CBC mode ciphers (no AEAD)0.3RFC 9325 Section 4.2: CBC "SHOULD NOT be used" without encrypt_then_mac (RFC 7366), due to padding oracle history (Lucky Thirteen, POODLE, GOLDENDOODLE). AEAD modes are immune by design. NIST SP 800-52r2 recommends GCM.count(ciphers) > 0 and count(ciphers, name contains "GCM") == 0 and count(ciphers, name contains "CCM") == 0 and count(ciphers, name contains "CHACHA20") == 0
SYS-0011100No forward secrecy ciphers (TLS 1.2)0.6RFC 9325 Section 4.1: "MUST support and prefer cipher suites offering forward secrecy." Without ephemeral key exchange, server key compromise decrypts all past sessions. NIST SP 800-52r2 Section 3.3.1 requires ephemeral key exchange.count(ciphers, protocol == "TLSv1.2") > 0 and count(ciphers, name contains "ECDHE") == 0 and count(ciphers, name contains "DHE") == 0
SYS-0011200Negotiated cipher lacks PFS0.5Same basis as SYS-0011100. RFC 9325 Section 4.1 requires forward secrecy; static RSA key transport explicitly lacks it. Applied to the active session's negotiated cipher.session.negotiated_cipher_suite != null and not contains(session.negotiated_protocol, "TLSv1.3") and not contains(session.negotiated_cipher_suite, "ECDHE") and not contains(session.negotiated_cipher_suite, "DHE")
SYS-0011300Negotiated cipher is not AEAD0.3RFC 9325 Section 4.2 discourages CBC in favor of AEAD. Active session susceptible to CBC padding oracle attacks (Lucky Thirteen, POODLE CVE-2014-3566, variants).session.negotiated_cipher_suite != null and not contains(session.negotiated_protocol, "TLSv1.3") and not contains(session.negotiated_cipher_suite, "GCM") and not contains(session.negotiated_cipher_suite, "CCM") and not contains(session.negotiated_cipher_suite, "CHACHA20")
SYS-0011400DH prime < 2048 bits0.6NIST SP 800-56A Rev. 3 Section 5.5.1 requires 2048-bit DH minimum (112-bit security). RFC 9325 Section 4.1 mandates at least 112-bit security for all key exchange.session.tls_metadata_available == true and session.kex_type == "DHE" and session.dh_param_size > 0 and session.dh_param_size < 2048
SYS-0011500DH prime < 1024 bits (Logjam)0.9Logjam attack (CVE-2015-4000, weakdh.org) showed 512-bit DH primes trivially factorable and 1024-bit primes plausibly precomputable by nation-state adversaries.session.tls_metadata_available == true and session.kex_type == "DHE" and session.dh_param_size > 0 and session.dh_param_size < 1024
SYS-0011600Server honors client cipher preference0.3RFC 9325 Section 4.2.1 recommends server-enforced cipher preference. NIST SP 800-52r2 Section 3.3.1 recommends server-side ordering. Without it, a malicious or misconfigured client can force the weakest mutually-supported cipher. Best practice, not a protocol mandate — RFC 5246 leaves server selection discretionary.session.honors_client_cipher_preference == true

Certificate & Chain

IDRuleScoreMotivationCondition
SYS-0020100Certificate expired1.0RFC 5280 Section 4.1.2.5 — certificate MUST be rejected after notAfter. CA/B Forum BR Section 4.9.1.1.cert.validity_state == "EXPIRED"
SYS-0020200Certificate not yet valid1.0RFC 5280 Section 6.1.3(a)(2) — current time must fall within validity period. NIST SP 800-52r2 Section 3.5.cert.validity_state == "NOT_YET_VALID"
SYS-0020300Certificate not trusted0.9RFC 5280 Section 6 — chain must terminate at a trusted root. CA/B Forum BR Section 7.1.cert.trust_state == "UNTRUSTED"
SYS-0020400Certificate trust unknown0.5RFC 5280 Section 6.1.1(d) — trust anchors required for path validation. Indeterminate trust status when chain cannot be validated.cert.trust_state == "UNKNOWN"
SYS-0020500Self-signed certificate0.7RFC 5280 Section 6.1 — path validation fails when self-signed cert not in trust store. CA/B Forum BR Section 7.1.2 requires CA-issued end-entity certificates.cert.self_signed == true and cert.java_root == false
SYS-0020600RSA key < 2048 bits0.6NIST SP 800-57 Part 1 Rev. 5, Table 2 — RSA < 2048 provides < 112 bits of security, disallowed since 2014. CA/B Forum BR Section 6.1.5 mandates 2048-bit minimum.cert.key_algorithm == "RSA" and cert.key_size > 0 and cert.key_size < 2048
SYS-0020700EC key < 256 bits0.6NIST SP 800-57 Part 1 Rev. 5 — 256-bit ECC minimum for 128-bit security. CA/B Forum BR Section 6.1.5 requires P-256, P-384, or P-521.(cert.key_algorithm == "EC" or cert.key_algorithm == "ECDSA") and cert.key_size > 0 and cert.key_size < 256
SYS-0020800Weak signature algorithm (SHA-1 or MD5)0.3NIST SP 800-131A Rev. 2 disallowed SHA-1 for signatures after 2013. SHAttered attack (2017) demonstrated full SHA-1 collision. CA/B Forum Ballot 152 prohibited SHA-1 certificates from January 2016.contains(upper(cert.signing_algorithm), "SHA1") or contains(upper(cert.signing_algorithm), "SHA-1") or contains(upper(cert.signing_algorithm), "MD5")
SYS-0020900Expires in < 30 days0.8CA/B Forum BR Section 4.9.1.1 and ACME (RFC 8555) renewal best practices. Let's Encrypt recommends renewal at 30 days remaining. NIST SP 800-52r2 Section 3.5 advises proactive renewal.cert.days_until_expiration >= 0 and cert.days_until_expiration < 30
SYS-0021000Expires in < 90 days0.4Standard warning threshold aligned with Let's Encrypt 90-day certificate lifetime. NIST SP 800-52r2 Section 3.5 recommends proactive renewal monitoring.cert.days_until_expiration >= 30 and cert.days_until_expiration < 90
SYS-0021100No intermediates in chain0.2RFC 8446 Section 4.4.2 — server SHOULD send complete chain (excluding root). Missing intermediates cause path validation failures per RFC 5280 Section 6.1.cert.chain_length == 1
SYS-0021200Wildcard certificate in use0.1CA/B Forum BR Section 3.2.2.6. NIST SP 800-52r2 Section 3.5 notes wildcard certificates increase blast radius — one key compromise affects all subdomains.cert.has_wildcard_san == true
SYS-0021300Certificate validity exceeds 398 days0.2CA/B Forum Ballot SC31 (September 2020) limits validity to 398 days. Enforced by Apple, Google, and Mozilla root programs. Limits window of exposure for compromised keys.cert.days_until_expiration > 398
SYS-0021400Certificate version < 3 (X.509v3)0.3RFC 5280 Section 4.1.2.1 — v3 required for extensions. CA/B Forum BR Section 7.1.1 requires X.509v3. Without v3, SANs, Key Usage, and Basic Constraints cannot be expressed.cert.version < 3
SYS-0021500MD5 signature algorithm0.8RFC 6151 documents MD5 collision vulnerabilities. Flame malware (2012) exploited MD5 collision to forge a Microsoft certificate. NIST SP 800-131A Rev. 2 disallows MD5 for signatures.contains(lower(cert.signing_algorithm), "md5")
SYS-0021600MD2 signature algorithm0.9RFC 6149 formally deprecates MD2. CVE-2004-2761. NIST removed MD2 from approved algorithms decades ago; effectively no collision resistance.contains(lower(cert.signing_algorithm), "md2")
SYS-0021700Certificate chain could not be evaluated0.2Certificate retrieval failed after retries. Inconclusive diagnostic — chain validity, trust, key strength, and expiration cannot be assessed.scan.certificate_retrieval_failed == true

Revocation & Transparency

IDRuleScoreMotivationCondition
SYS-0030100Certificate is revoked1.0RFC 5280 Section 6.1.3(a)(4) mandates rejection of revoked certificates. RFC 6960 (OCSP), RFC 5280 Section 5 (CRLs). CA/B Forum BR Section 4.9.1.1.revocation.available == true and (revocation.ocsp_status == "REVOKED" or revocation.crl_status == "REVOKED")
SYS-0030200OCSP stapling not present0.3RFC 6066 Section 8 defines OCSP stapling. NIST SP 800-52r2 Section 3.5 recommends it. Without stapling, clients must contact the OCSP responder directly, leaking browsing data and creating latency/availability dependency.revocation.available == true and revocation.ocsp_stapling_present == false
SYS-0030300Must-Staple not present0.1RFC 7633 defines Must-Staple (id-pe-tlsFeature). Without it, a compromised key holder can suppress revocation information by not stapling, enabling continued use of a revoked certificate.revocation.available == true and revocation.must_staple_present == false
SYS-0030400No CT SCTs found0.3RFC 6962 / RFC 9162 (Certificate Transparency). Chrome requires CT compliance since April 2018; Apple since October 2018. SCTs prove certificate logging for public accountability.revocation.available == true and revocation.sct_count == 0
SYS-0030500Only 1 SCT (2+ recommended)0.1Google CT Policy requires 2+ SCTs from distinct logs (3+ for long-lived certs). Apple CT Policy similar. Single SCT provides insufficient diversity if that log is compromised.revocation.available == true and revocation.sct_count > 0 and revocation.sct_count < 2
SYS-0030600Both OCSP and CRL failed0.5RFC 5280 Section 6.3 — revocation checking is critical to path validation. When both OCSP and CRL fail, revocation status is unknown. NIST SP 800-52r2 Section 3.5.revocation.available == true and revocation.ocsp_status == "ERROR" and revocation.crl_status == "ERROR"
SYS-0030700Revocation status could not be evaluated0.2Revocation check failed after retries. Inconclusive diagnostic — OCSP, CRL, stapling, and CT status cannot be assessed.scan.revocation_check_failed == true

Security Headers

IDRuleScoreMotivationCondition
SYS-0040100HSTS header missing0.6RFC 6797 (HSTS) prevents protocol downgrade and cookie hijacking (SSLstrip attack). OWASP security misconfiguration; PCI DSS v4.0 Requirement 4.2.1.session.headers_available and header("Strict-Transport-Security") == null
SYS-0040200HSTS max-age < 1 year0.2HSTS Preload List (hstspreload.org) requires min 31536000s (1 year). Short max-age leaves windows where HTTPS is not enforced after policy expiry.header("Strict-Transport-Security") != null and parse_max_age(header("Strict-Transport-Security")) >= 0 and parse_max_age(header("Strict-Transport-Security")) < 31536000
SYS-0040300HSTS missing includeSubDomains0.1RFC 6797 Section 6.1.2. Without includeSubDomains, subdomains remain vulnerable to downgrade. Required for HSTS Preload List eligibility.header("Strict-Transport-Security") != null and not contains(lower(header("Strict-Transport-Security")), "includesubdomains")
SYS-0040400X-Content-Type-Options missing0.1WHATWG Fetch Standard. Prevents MIME-type sniffing attacks. OWASP Secure Headers Project recommended header.session.headers_available and header("X-Content-Type-Options") == null
SYS-0040500X-Frame-Options missing0.05RFC 7034. Prevents clickjacking (CWE-1021). CSP frame-ancestors is the modern replacement but X-Frame-Options provides backward compatibility.session.headers_available and header("X-Frame-Options") == null
SYS-0040600Content-Security-Policy missing0.1W3C Content Security Policy Level 3. Mitigates XSS (CWE-79) and data injection. OWASP Top 10 2021 A03 (Injection).session.headers_available and header("Content-Security-Policy") == null
SYS-0040700Permissions-Policy missing0.1W3C Permissions Policy. Restricts browser API access (camera, microphone, geolocation). OWASP Secure Headers Project. Enforces least privilege for browser features.session.headers_available and not header_present("Permissions-Policy")
SYS-0040800Referrer-Policy missing0.1W3C Referrer Policy. Without it, full URLs with sensitive query parameters may leak to third parties via Referer header (CWE-200).session.headers_available and not header_present("Referrer-Policy")
SYS-0040900Cross-Origin-Opener-Policy missing0.1WHATWG HTML Living Standard. Mitigates Spectre-class cross-origin attacks (CVE-2017-5753/5715) via process isolation. Required for SharedArrayBuffer.session.headers_available and not header_present("Cross-Origin-Opener-Policy")
SYS-0041000HSTS missing preload directive0.1HSTS Preload List (hstspreload.org). Eliminates trust-on-first-use (TOFU) vulnerability where the initial HTTP request is unprotected (RFC 6797 Section 12.4).header("Strict-Transport-Security") != null and not contains(lower(header("Strict-Transport-Security")), "preload")

DNS Security

IDRuleScoreMotivationCondition
SYS-0050100No CAA records (any CA can issue certificates)0.3RFC 8659 (CAA). The CA/Browser Forum -- a consortium of browser vendors and certificate authorities -- publishes Baseline Requirements that influence how browsers and CAs behave but do not mandate what developers must do. BR Section 3.2.2.8 requires CAs to check CAA before issuance (mandatory since September 2017). Without CAA, any CA can issue certificates for the domain.dns.available == true and dns.has_caa_records == false
SYS-0050200No DANE/TLSA records0.1RFC 6698 / RFC 7671 (DANE/TLSA). Binds certificates to domain names via DNSSEC, supplementing the CA trust model. Absence means full reliance on CA/PKI trust without DNS-based pinning.dns.available == true and dns.has_tlsa_records == false
SYS-0050300DNS security could not be evaluated0.15DNS lookup failed after retries. Inconclusive diagnostic — CAA and DANE/TLSA status cannot be assessed.scan.dns_security_failed == true

Other

IDRuleScoreMotivationCondition
SYS-0060100TLS compression enabled0.5CRIME attack (CVE-2012-4929) — TLS compression leaks secret data through ciphertext length changes. RFC 7525 Section 3.3: "SHOULD disable TLS-level compression." TLS 1.3 (RFC 8446) removes compression entirely.session.compression_enabled == true
SYS-0060200Client auth required0.05RFC 8446 Section 4.3.2. Mutual TLS is appropriate for internal APIs (NIST SP 800-52r2 Section 3.5.2) but unusual on public-facing servers; may indicate misconfiguration or limit accessibility. Informational.session.client_auth_required == true
SYS-0060300TLS fingerprint unavailable0.05Informational diagnostic. Unable to characterize server TLS behavior via fingerprint probes. May indicate incomplete handshake or non-standard TLS stack.session.fingerprint == null
SYS-006040021+ SANs (high exposure)0.6CA/B Forum BR Section 7.1.2.3. NIST SP 800-52r2 Section 3.5 recommends limiting scope. 21+ SANs means key compromise affects many domains; high blast radius.cert.san_count >= 21
SYS-00605006-20 SANs (medium exposure)0.3Same basis as SYS-0060400 at medium severity. NIST SP 800-53 AC-6 (least privilege). Common for CDN/multi-service deployments but broader exposure than single-domain.cert.san_count >= 6 and cert.san_count < 21
SYS-00606002-5 SANs (low exposure)0.1Same basis at lowest severity. Typical for www + apex domain pairs (RFC 5280 Section 4.2.1.6). Flagged informational for audit awareness.cert.san_count >= 2 and cert.san_count < 6
SYS-0060700TLS fingerprint could not be evaluated0.1TLS fingerprint probing failed after retries. Inconclusive diagnostic — server behavior cannot be characterized via fingerprint probes.scan.tls_fingerprint_failed == true

Using Risk Scoring

URL url = new URL("https://github.com/");
ISession session = DeepVioletFactory.initializeSession(url);
IEngine eng = DeepVioletFactory.getEngine(session);

// Compute risk score with default rules
IRiskScore score = eng.getRiskScore();

System.out.println("Score: " + score.getTotalScore() + "/100");
System.out.println("Grade: " + score.getLetterGrade());
System.out.println("Risk:  " + score.getRiskLevel());

// Iterate category breakdowns
for (ICategoryScore cat : score.getCategoryScores()) {
    System.out.println(cat.getDisplayName() + ": "
        + cat.getScore() + "/100"
        + " [" + cat.getRiskLevel() + "]");
    System.out.println("  " + cat.getSummary());

    for (IDeduction d : cat.getDeductions()) {
        System.out.printf("    - [%s] %s (score=%.2f, %s)%s%n",
            d.getRuleId(), d.getDescription(),
            d.getScore(), d.getSeverity(),
            d.isInconclusive() ? " [inconclusive]" : "");
    }
}

See src/main/java/com/mps/deepviolet/samples/PrintRiskScore.java for a complete working example.

Scoring Diagnostics

The scoring engine reports issues encountered during rule evaluation via IScoringDiagnostic. Diagnostics include optional YAML source location (line, column) so rule authors can locate problems in custom rules files.

IRiskScore score = eng.getRiskScore();

for (IRiskScore.IScoringDiagnostic diag : score.getDiagnostics()) {
    System.out.printf("[%s] %s (rule=%s, category=%s, line=%d)%n",
            diag.getLevel(), diag.getMessage(),
            diag.getRuleId(), diag.getCategory(), diag.getLine());
}
MethodDescription
getRuleId()Rule identifier, or null for non-rule diagnostics
getCategory()Category key, or null for global diagnostics
getLevel()WARNING or ERROR
getMessage()Human-readable diagnostic message
getLine()1-based YAML source line (-1 if unknown)
getColumn()1-based YAML source column (-1 if unknown)

Per-category diagnostics are also available via ICategoryScore.getDiagnostics().

Deduction Scope

Each deduction may include structured scope metadata describing the protocol layer, affected TLS versions, and specific mechanism. This is useful for filtering and classifying findings.

for (IDeduction d : cat.getDeductions()) {
    IDeduction.IScope scope = d.getScope();
    if (scope != null) {
        System.out.printf("    Layer: %s, Protocols: %s, Aspect: %s%n",
                scope.getLayer(),
                String.join(", ", scope.getProtocols()),
                scope.getAspect());
    }
}
IScope MethodDescription
getLayer()Service/protocol layer (e.g., "tls", "certificate", "http", "dns")
getProtocols()Affected TLS versions (e.g., ["TLSv1.2", "TLSv1.3"]), empty if not version-specific
getAspect()Specific mechanism (e.g., "renegotiation", "post_quantum", "hsts"), or null

Offline Re-Scoring

Risk scores can be computed offline from a saved RuleContext without re-scanning the server. This is useful for re-scoring persisted scan data when rules are updated.

// Build and save the context during a live scan
RuleContext context = eng.buildRuleContext();
// ... serialize context (e.g., via ScanSnapshot persistence) ...

// Later: re-score offline
IRiskScore score = eng.getRiskScore(context);

// Or with custom user rules merged in
try (InputStream userRules = Files.newInputStream(Path.of("my-rules.yaml"))) {
    IRiskScore customScore = eng.getRiskScore(context, userRules);
}

Configuring the Scoring Rules

All scoring rules are defined in a YAML file that controls which rules are evaluated, their conditions, score values, and grade boundaries. The default rules ship at src/main/resources/risk-scoring-rules.yaml. No code changes are required to customize scoring -- the engine reads all rule definitions from YAML at runtime.

Loading Custom Rules

There are two ways to override the default rules:

Option 1: System property -- Set the dv.scoring.rules system property to point to your custom rules file. This overrides the default for all getRiskScore() calls:

java -Ddv.scoring.rules=/etc/deepviolet/my-rules.yaml -jar myapp.jar

Option 2: Programmatic -- Pass the path to a custom rules file directly:

IRiskScore score = eng.getRiskScore("/etc/deepviolet/my-rules.yaml");

Disabling a Rule

Set enabled: false on any rule to skip it. Disabled rules are not evaluated and will not produce deductions:

      no_hsts:
        id: SYS-0040100
        description: "Strict-Transport-Security header missing"
        score: 0.6
        enabled: false                    # <-- skip this rule
        when: session.headers_available and header("Strict-Transport-Security") == null

Adjusting Score Values

Change the score field (0.0-1.0) to increase or decrease the severity. Higher scores indicate more critical findings. For example, to treat SSLv3 as equally critical to SSLv2:

      sslv3_supported:
        id: SYS-0000200
        description: "SSLv3 supported"
        score: 1.0                        # <-- increased from 0.9
        when: protocols contains "SSLv3"

Because scores are averaged rather than summed, adding new rules to a category does not require adjusting existing score values.

Adjusting Thresholds

Thresholds are part of the rule's when expression. To change when a rule fires, edit the expression directly. For example, to require RSA keys of at least 4096 bits:

      rsa_key_too_small:
        id: SYS-0020600
        description: "RSA key less than 4096 bits"
        score: 0.6
        when: >
          cert.key_algorithm == "RSA"
          and cert.key_size > 0
          and cert.key_size < 4096        # <-- changed from 2048

To change the weak cipher threshold from 6 to 10:

      many_weak_ciphers:
        id: SYS-0010200
        description: "10 or more WEAK ciphers offered"
        score: 0.7
        when: count(ciphers, strength == "WEAK") >= 10   # <-- changed from 6

Adding a New Rule to an Existing Category

To add a rule, pick the next available SYS- or USR- ID (check the metadata section) and add it under the appropriate category's rules:

      # Example: penalize ECDSA keys shorter than 384 bits
      ecdsa_key_short:
        id: USR-0010700                   # <-- next available ID
        description: "ECDSA key less than 384 bits"
        score: 0.3
        when: >
          cert.key_algorithm == "ECDSA"
          and cert.key_size > 0
          and cert.key_size < 384

After adding the rule, update the # Next available rule ID comment in the metadata section.

Adding a Custom Category

You can define entirely new scoring categories beyond the built-in 7. Custom categories appear in getCategoryScores() alongside built-in ones:

  # Custom category for PCI DSS compliance
  PCI_COMPLIANCE:
    display_name: "PCI DSS Compliance"
    rules:
      pci_tls_version:
        id: USR-0100100
        description: "PCI DSS requires TLS 1.2 or higher"
        score: 1.0
        when: protocols contains "TLSv1.0" or protocols contains "TLSv1.1"

Custom categories use getCategoryKey() to return their YAML key (e.g., "PCI_COMPLIANCE") and getCategory() returns null since they are not built-in enum values.

Adjusting Grade Boundaries

The grade_mapping list maps score ranges to letter grades. Entries are evaluated from highest min_score to lowest. To make grading stricter:

grade_mapping:
  - { grade: A_PLUS,  min_score: 98, risk_level: LOW }      # was 95
  - { grade: A,       min_score: 93, risk_level: LOW }      # was 90
  # ...

Adjusting Severity Mapping

The severity_mapping section controls how rule scores map to severity labels and score floors:

severity_mapping:
  - { severity: CRITICAL, min_score: 0.8, floor: 65 }
  - { severity: HIGH,     min_score: 0.5, floor: 75 }
  - { severity: MEDIUM,   min_score: 0.2, floor: 85 }
  - { severity: LOW,      min_score: 0.01, floor: 100 }
  - { severity: INFO,     min_score: 0.0, floor: 100 }

To make the scoring more lenient for HIGH findings, increase the floor:

  - { severity: HIGH, min_score: 0.5, floor: 80 }    # was 75

Inconclusive Findings

When the scoring engine cannot verify a condition (e.g., security headers are unavailable because the server did not return HTTP response headers), the deduction is marked as inconclusive. There are two patterns:

Always inconclusive -- Set inconclusive: true on the rule. The deduction always appears as inconclusive when the condition matches:

      fingerprint_unavailable:
        id: SYS-0060300
        description: "TLS fingerprint unavailable"
        score: 0.05
        inconclusive: true
        when: session.fingerprint == null

Conditional inconclusive -- Use when_inconclusive for rules that are confirmed when data is available but inconclusive when data is missing. The when_inconclusive clause is evaluated first; if it matches, the deduction is recorded as inconclusive and when is skipped:

      no_hsts:
        id: SYS-0040100
        description: "Strict-Transport-Security header missing"
        score: 0.6
        when: session.headers_available and header("Strict-Transport-Security") == null
        when_inconclusive: not session.headers_available

Inconclusive deductions still count toward the score (conservative approach) but are flagged via IDeduction.isInconclusive() so callers can distinguish verified findings from unverified ones.

Expression Language Reference

Rule conditions use a lightweight expression language. Conditions are defined in the when and when_inconclusive fields.

Variables

Variable PathDescription
session.negotiated_protocolNegotiated TLS protocol (e.g., "TLSv1.3")
session.negotiated_cipher_suiteNegotiated cipher suite name
session.negotiated_cipher_strengthStrength of negotiated cipher ("STRONG", "MEDIUM", "WEAK")
session.compression_enabledWhether TLS compression is enabled
session.client_auth_requiredWhether client auth is required
session.fingerprintTLS server fingerprint string, or null
session.headers_availableWhether HTTP response headers were retrieved
session.tls_metadata_availableWhether raw TLS metadata was collected
session.renegotiation_info_presentWhether renegotiation_info extension is present
session.early_data_acceptedWhether TLS 1.3 early data (0-RTT) was accepted
session.alpn_negotiatedNegotiated ALPN protocol string, or null
session.fallback_scsv_supportedWhether TLS_FALLBACK_SCSV is supported, or null
session.honors_client_cipher_preferenceWhether the server follows client cipher order instead of enforcing its own (Boolean, or null if inconclusive)
session.dh_param_sizeDH parameter prime size in bits
session.kex_typeKey exchange type (e.g., "DHE", "ECDHE")
session.pq_kex_supportedWhether the server supports any post-quantum key exchange group (Boolean, or null)
session.pq_kex_preferredWhether the server prefers PQ over classical when both offered (Boolean, or null)
session.pq_kex_groupsComma-separated list of PQ group names the server supports
session.pq_preferred_groupName of the PQ group the server selected in the preference probe
session.pq_kex_probe_failedTrue if PQ probe failed after retries
session.negotiated_group_pqWhether the negotiated key exchange group is post-quantum (Boolean)
protocolsSet of protocol strings (e.g., "TLSv1.2", "TLSv1.3")
ciphersList of cipher maps, each with name, strength, protocol
cert.validity_state"VALID", "EXPIRED", or "NOT_YET_VALID"
cert.trust_state"TRUSTED", "UNTRUSTED", or "UNKNOWN"
cert.self_signedBoolean
cert.java_rootWhether cert is a Java trusted root
cert.key_algorithm"RSA", "EC", "ECDSA", etc.
cert.key_sizeKey size in bits
cert.signing_algorithmSignature algorithm string
cert.days_until_expirationDays until certificate expires
cert.chain_lengthNumber of certificates in chain
cert.san_countNumber of Subject Alternative Names
cert.has_wildcard_sanWhether any SAN is a wildcard
cert.versionX.509 certificate version number
dns.availableWhether DNS lookups were performed
dns.has_caa_recordsWhether CAA records exist
dns.has_tlsa_recordsWhether DANE/TLSA records exist
revocation.availableWhether revocation data was retrieved
revocation.ocsp_status"GOOD", "REVOKED", "ERROR", or null
revocation.crl_status"GOOD", "REVOKED", "ERROR", or null
revocation.ocsp_stapling_presentBoolean
revocation.must_staple_presentBoolean
revocation.sct_countNumber of Certificate Transparency SCTs
scan.certificate_retrieval_failedTrue if certificate retrieval failed after retries
scan.revocation_check_failedTrue if revocation check failed after retries
scan.dns_security_failedTrue if DNS security lookup failed after retries
scan.tls_fingerprint_failedTrue if TLS fingerprint probing failed after retries

Operators

OperatorExample
==, !=cert.trust_state == "TRUSTED"
<, >, <=, >=cert.key_size < 2048
and, orcert.key_algorithm == "RSA" and cert.key_size < 2048
notnot session.headers_available
containsprotocols contains "TLSv1.3"
not containsprotocols not contains "TLSv1.3"

Null safety: null op anything evaluates to false, except == null (true) and != null (false).

Built-in Functions

FunctionDescriptionExample
count(list)Count elementscount(ciphers) > 0
count(list, field op value)Count matching elementscount(ciphers, strength == "WEAK") >= 6
contains(str, substr)String containscontains(upper(cert.signing_algorithm), "SHA1")
starts_with(str, prefix)String starts withstarts_with(cert.key_algorithm, "EC")
upper(str), lower(str)Case conversionupper(cert.signing_algorithm)
header(name)Get HTTP response header valueheader("Strict-Transport-Security")
header_present(name)Check header existsheader_present("X-Frame-Options")
parse_max_age(value)Extract max-age seconds from HSTSparse_max_age(header("Strict-Transport-Security"))

Rules File Reference

A custom rules file must follow this structure:

metadata:
  version: "3.0"
  description: "My organization's TLS scoring rules"
  # Next available rule IDs — see metadata section in risk-scoring-rules.yaml

severity_mapping:
  - { severity: CRITICAL, min_score: 0.8, floor: 65 }
  - { severity: HIGH,     min_score: 0.5, floor: 75 }
  - { severity: MEDIUM,   min_score: 0.2, floor: 85 }
  - { severity: LOW,      min_score: 0.01, floor: 100 }
  - { severity: INFO,     min_score: 0.0, floor: 100 }

grade_mapping:
  - { grade: A_PLUS,  min_score: 95, risk_level: LOW }
  - { grade: A,       min_score: 90, risk_level: LOW }
  - { grade: B,       min_score: 80, risk_level: MEDIUM }
  - { grade: C,       min_score: 70, risk_level: HIGH }
  - { grade: D,       min_score: 60, risk_level: CRITICAL }
  - { grade: F,       min_score: 0,  risk_level: CRITICAL }

categories:
  PROTOCOLS:
    display_name: "Protocols & Connections"
    rules:
      sslv2_supported:
        id: SYS-0000100
        description: "SSLv2 supported"
        score: 1.0
        when: protocols contains "SSLv2"
      # ... more rules ...

  CIPHER_SUITES:
    display_name: "Cipher Suites"
    rules:
      # ...

  CERTIFICATE:
    display_name: "Certificate & Chain"
    rules:
      # ...

  REVOCATION:
    display_name: "Revocation & Transparency"
    rules:
      # ...

  SECURITY_HEADERS:
    display_name: "Security Headers"
    rules:
      # ...

  DNS_SECURITY:
    display_name: "DNS Security"
    rules:
      # ...

  OTHER:
    display_name: "Other"
    rules:
      # ...

Each rule within a category follows this structure:

      rule_key:                     # YAML key name (used as fallback ID)
        id: SYS-0000100             # Stable rule identifier (recommended)
        description: "..."          # Human-readable description
        score: 0.5                  # Normalized severity (0.0-1.0)
        enabled: true               # Optional, defaults to true
        inconclusive: false         # Optional, defaults to false
        when: <expression>          # Condition that triggers the rule
        when_inconclusive: <expr>   # Optional inconclusive condition
        scope:                      # Optional structured scope metadata
          layer: tls                # Protocol layer (tls, certificate, http, dns, etc.)
          protocols: [TLSv1.3]      # Affected TLS versions (optional)
          aspect: post_quantum      # Specific mechanism (optional)
        meta:                       # Optional template variables for description
          key: session.some_prop    # Variable expansion via ${key} in description

The id field is optional but strongly recommended. When present, it is used as the deduction's getRuleId(). When absent, the YAML key name is used instead. The score field determines both the impact on the overall score and the derived severity (via severity_mapping).

Proposing Changes to Default Scoring

If you believe a default rule, point value, or condition should be changed, open an issue explaining:

  1. Which rule(s) should change and to what values
  2. Your technical rationale with supporting references (RFCs, CVEs, industry guidance)

If the proposal is approved, submit a pull request to risk-scoring-rules.yaml and include a reference to the approved issue.

AI Analysis

The AI analysis module sends TLS scan data to a large language model and returns structured security analysis. There are two access paths:

  • Engine one-callIEngine.getAiAnalysis(AiConfig) generates a report from the current engine state and sends it in a single call.
  • Service directDeepVioletFactory.getAiService() returns an IAiAnalysisService that accepts an InputStream of scan data, allowing analysis of saved reports, in-memory strings, or any other stream source.

Supported Providers

ProviderEndpointDefault ModelsAPI KeyNotes
ANTHROPIChttps://api.anthropic.com/v1/messagesclaude-sonnet-4-5-20250929, claude-haiku-4-5-20251001Required (DV_AI_API_KEY)Cloud-hosted
OPENAIhttps://api.openai.com/v1/chat/completionsgpt-4o, gpt-4o-miniRequired (DV_AI_API_KEY)Cloud-hosted
OLLAMAhttp://localhost:11434llama3.2:latest, mistral:latest, gemma2:latestNot requiredLocal — no data leaves the machine

AI Configuration

Use AiConfig.builder() to construct an immutable configuration:

SettingTypeDefaultDescription
providerAiProviderANTHROPICAI provider to use
apiKeyStringnullAPI key (required for Anthropic and OpenAI)
modelStringProvider defaultModel identifier
maxTokensint4096Maximum tokens in the response
temperaturedouble0.3Sampling temperature (0.0–1.0)
systemPromptStringBuilt-in analysis promptCustom system prompt
endpointUrlStringProvider defaultOverride endpoint URL
AiConfig config = AiConfig.builder()
        .provider(AiProvider.ANTHROPIC)
        .apiKey(System.getenv("DV_AI_API_KEY"))
        .model("claude-sonnet-4-5-20250929")
        .maxTokens(4096)
        .temperature(0.3)
        .build();

Using AI Analysis

Option 1: One-call from engine state — the engine generates a report internally and sends it to the AI provider:

URL url = new URL("https://github.com/");
ISession session = DeepVioletFactory.initializeSession(url);
IEngine eng = DeepVioletFactory.getEngine(session);

String analysis = eng.getAiAnalysis(config);
System.out.println(analysis);

Option 2: Analyze from a file or stream — use the service directly with any InputStream:

IAiAnalysisService ai = DeepVioletFactory.getAiService();

// From a saved report file
try (InputStream fileStream = Files.newInputStream(Path.of("saved-report.txt"))) {
    String analysis = ai.analyze(fileStream, config);
    System.out.println(analysis);
}

Option 3: Analyze from an in-memory string:

String reportText = "... scan report text ...";
try (InputStream memStream = new ByteArrayInputStream(
        reportText.getBytes(StandardCharsets.UTF_8))) {
    String analysis = ai.analyze(memStream, config);
    System.out.println(analysis);
}

See PrintAiAnalysis.java for a complete working example.

Multi-Turn Chat

The chat() method supports multi-turn conversations about scan results. Each call sends the full conversation history and returns the assistant's next response.

AiChatMessage is a record with two components: role ("user", "assistant", or "system") and content (the message text).

IAiAnalysisService ai = DeepVioletFactory.getAiService();

AiConfig chatConfig = AiConfig.builder()
        .provider(AiProvider.OLLAMA)
        .model("llama3.2:latest")
        .systemPrompt(AiAnalysisService.DEFAULT_CHAT_SYSTEM_PROMPT)
        .build();

// Seed the conversation with scan context
List<AiChatMessage> history = new ArrayList<>();
history.add(new AiChatMessage("user",
        "Here is a TLS scan report:\n" + scanReport + "\n\nWhat is the biggest risk?"));

String response = ai.chat(history, chatConfig);
System.out.println("AI: " + response);

// Follow-up question
history.add(new AiChatMessage("assistant", response));
history.add(new AiChatMessage("user", "How do I fix it?"));

response = ai.chat(history, chatConfig);
System.out.println("AI: " + response);

See PrintAiChat.java for a complete working example.

Model Discovery

Use fetchModels() to query available models from a provider at runtime:

IAiAnalysisService ai = DeepVioletFactory.getAiService();

// Query models from Ollama (local)
String[] ollamaModels = ai.fetchModels(AiProvider.OLLAMA, null, null);

// Query models from Anthropic (requires API key)
String[] anthropicModels = ai.fetchModels(AiProvider.ANTHROPIC,
        System.getenv("DV_AI_API_KEY"), null);

For built-in defaults without an API call, use AiProvider.getDefaultModels():

String[] defaults = AiProvider.ANTHROPIC.getDefaultModels();
// ["claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001"]

Scanning and Persistence

Scanning Overview

TlsScanner provides parallel multi-host TLS scanning using virtual threads. It accepts hostnames, IPs, CIDR ranges, and IP ranges via the TargetSpec parser.

Key capabilities:

  • Parallel execution — configurable thread pool with virtual threads
  • Flexible targets — hostnames, IPv4/IPv6, CIDR notation (10.0.0.0/24), IP ranges (192.168.1.1-192.168.1.10)
  • Two monitoring modes — event-driven (IScanListener) or polling (IScanMonitor)
  • Cooperative cancel/pause — via BackgroundTask support
  • Selective sections — enable only the scan phases you need
// Simple scan with defaults
List<IScanResult> results = TlsScanner.scan(List.of("github.com", "google.com"));

// Async variant
CompletableFuture<List<IScanResult>> future = TlsScanner.scanAsync(targets, config, listener);

Scan Configuration

Use ScanConfig.builder() to customize scan behavior:

SettingTypeDefaultDescription
threadCountint10Number of virtual threads
sectionDelayMslong200Milliseconds between scan sections
perHostTimeoutMslong60000Max time per host (ms)
cipherNameConventionCIPHER_NAME_CONVENTIONIANACipher suite naming convention
enabledProtocolsSet<Integer>null (all)TLS protocol versions to test
enabledSectionsSet<ScanSection>All sectionsScan phases to execute
maxRetriesint3Max retry attempts per section (0 disables)
initialRetryDelayMslong500Initial retry delay (ms)
maxRetryDelayMslong4000Max retry delay (ms)
retryBudgetMslong15000Wall-clock retry budget per section (ms)
ScanConfig config = ScanConfig.builder()
        .threadCount(4)
        .perHostTimeoutMs(30000)
        .enabledSections(EnumSet.of(
                ScanSection.SESSION_INIT,
                ScanSection.CIPHER_ENUMERATION,
                ScanSection.RISK_SCORING))
        .build();

Scan Sections

The ScanSection enum defines the phases of a scan. Each can be independently enabled or disabled:

SectionDescription
SESSION_INITSession initialization and TLS handshake
CIPHER_ENUMERATIONEnumerate supported cipher suites
CERTIFICATE_RETRIEVALRetrieve X.509 certificate chain
RISK_SCORINGCompute TLS risk score
TLS_FINGERPRINTCompute TLS server fingerprint
DNS_SECURITYCheck CAA and DANE/TLSA DNS records
REVOCATION_CHECKCheck certificate revocation (OCSP, CRL, CT)

Monitoring Scans

Event-Driven: IScanListener

Implement IScanListener to receive callbacks as the scan progresses. All methods have default no-op implementations, so you only need to override what you care about:

IScanListener listener = new IScanListener() {
    @Override
    public void onHostStarted(URL url, int index, int total) {
        System.out.printf("Starting %s (%d/%d)%n", url.getHost(), index + 1, total);
    }

    @Override
    public void onHostCompleted(IScanResult result, int completedCount, int total) {
        System.out.printf("Completed %s [%s] (%d/%d)%n",
                result.getURL().getHost(),
                result.isSuccess() ? "OK" : "ERROR",
                completedCount, total);
    }

    @Override
    public void onScanCompleted(List<IScanResult> results) {
        System.out.println("Scan finished: " + results.size() + " hosts");
    }
};

List<IScanResult> results = TlsScanner.scan(targets, config, listener);

Polling: IScanMonitor

Use IScanMonitor for timer-based progress polling:

CompletableFuture<List<IScanResult>> future =
        TlsScanner.scanAsync(targets, config, null);

IScanMonitor monitor = TlsScanner.getMonitor();
while (monitor.isRunning()) {
    System.out.printf("Progress: %d/%d hosts, %d active threads%n",
            monitor.getCompletedHostCount(),
            monitor.getTotalHostCount(),
            monitor.getActiveThreadCount());
    Thread.sleep(1000);
}
IScanMonitor MethodDescription
getActiveThreadCount()Threads currently executing a scan section
getSleepingThreadCount()Threads sleeping between sections
getIdleThreadCount()Threads waiting for work
getCompletedHostCount()Hosts completed so far
getTotalHostCount()Total hosts in the scan
isRunning()true if the scan is still in progress
getThreadStatuses()Per-thread status snapshot

Scan Results

Each IScanResult provides access to the scanned host's data:

MethodReturn TypeDescription
getURL()URLThe scanned URL
isSuccess()booleantrue if completed without fatal error
getSession()ISessionSession (or null on failure)
getEngine()IEngineEngine (or null on failure)
getError()DeepVioletExceptionError (or null on success)
getStartTime()InstantWhen the scan started
getEndTime()InstantWhen the scan ended
getDuration()DurationHow long the scan took
getCompletedSections()Set<ScanSection>Sections completed successfully
getFailedSections()Set<ScanSection>Sections that failed after all retry attempts

Persistence Overview

Scan results can be saved to .dvscan files and loaded later. The file format is the same one used by the DeepVioletTools GUI workbench, enabling workflows like:

  1. Run scans on a remote server (CI/CD, headless CLI)
  2. Save results to .dvscan files
  3. Transfer to a workstation
  4. Open in the GUI for visual analysis

The persistence model consists of:

  • ScanSnapshot — top-level container with totalTargets, successCount, errorCount, scanId, and a list of HostSnapshot entries.
  • HostSnapshot — per-host data including riskScore, ciphers, securityHeaders, connProperties, httpHeaders, tlsFingerprint, and reportTree.

File Modes

ScanFileMode controls how .dvscan files are written:

ModeEncryptionPortabilityPassword
PLAIN_TEXTNonePortable anywhereNot required
HOST_LOCKEDAES-256-GCM with machine keySame machine onlyNot required
PASSWORD_LOCKEDAES-256-GCM with machine key + passwordPortable across machinesRequired on other machines

Saving and Loading Scans

Saving — use ScanFileIO.save() with the desired mode:

CryptoUtils.ensureEncryptionSeed();
byte[] machineKey = CryptoUtils.getEncryptionSeed();

// Plain text (no encryption)
ScanFileIO.save(file, snapshot, ScanFileMode.PLAIN_TEXT, null, null);

// Host locked (machine key only)
ScanFileIO.save(file, snapshot, ScanFileMode.HOST_LOCKED, machineKey, null);

// Password locked (machine key + password for cross-machine transfer)
char[] password = "transfer-password".toCharArray();
ScanFileIO.save(file, snapshot, ScanFileMode.PASSWORD_LOCKED, machineKey, password);

Convenience methods are also available:

ScanFileIO.savePlainText(file, snapshot);           // Plain text
ScanFileIO.save(file, snapshot, machineKey);         // Host locked
ScanFileIO.save(file, snapshot, machineKey, password); // Password locked

Loadingload() auto-detects the file format (plain JSON, v1 encrypted, or v2 envelope encrypted):

// Plain text — no key needed
ScanSnapshot plain = ScanFileIO.load(file, null);

// Host locked — machine key
ScanSnapshot local = ScanFileIO.load(file, machineKey);

// Password locked on another machine — provide a PasswordCallback
ScanSnapshot remote = ScanFileIO.load(file, machineKey,
        () -> "transfer-password".toCharArray());

The PasswordCallback functional interface is invoked only when the machine key fails to decrypt (indicating the file was created on a different machine). This allows interactive password prompting in CLI or GUI contexts.

See PrintScanPersistence.java for a complete working example, and PrintSaveScan.java for an end-to-end scan-and-save workflow.

Encryption Architecture

The v2 .dvscan envelope format uses dual-slot key encryption:

graph TD
    A[Scan Data JSON] -->|AES-256-GCM| B[Encrypted Payload]
    C[Random DEK] --> B
    C -->|Wrapped by Machine KEK| D[Machine Slot]
    C -->|Wrapped by Password KEK| E[Password Slot]
    F[Machine Key] -->|Direct| D
    G[User Password] -->|PBKDF2-HMAC-SHA256| E
    D -->|HMAC-SHA256| H[Slot Integrity]
    E -->|HMAC-SHA256| H

    subgraph "File Header"
        I[DVSC Magic 4 bytes]
        J[Version byte]
    end

    subgraph "KEK Slots"
        D
        E
        H
    end

    subgraph "Payload"
        B
    end

Key properties:

  • Per-file DEK — each .dvscan file gets a unique data encryption key
  • Dual KEK slots — the DEK is wrapped independently by the machine key and (optionally) a password-derived key
  • HMAC-SHA256 slot integrity — each slot is integrity-checked before unwrapping
  • PBKDF2-HMAC-SHA256 — password-derived keys use 600,000 iterations
  • Backward compatibility — v1 encrypted files are transparently read by load()

Delta Comparison

Two saved .dvscan files can be compared to detect TLS configuration changes over time. This is useful for:

  • Tracking configuration drift
  • Verifying remediation actions
  • Monitoring changes after server updates

The comparison matches hosts by URL and reports changes in grades, category scores, and cipher suites:

ScanSnapshot baseline = ScanFileIO.load(new File("baseline.dvscan"), machineKey);
ScanSnapshot current = ScanFileIO.load(new File("current.dvscan"), machineKey);

for (HostSnapshot curHost : current.getHosts()) {
    HostSnapshot baseHost = findHostByUrl(baseline, curHost.getTargetUrl());
    if (baseHost == null) {
        System.out.println("[NEW] " + curHost.getTargetUrl());
        continue;
    }

    // Compare grades
    IRiskScore baseScore = baseHost.getRiskScore();
    IRiskScore curScore = curHost.getRiskScore();
    int diff = curScore.getTotalScore() - baseScore.getTotalScore();
    System.out.printf("Grade: %s -> %s (%+d)%n",
            baseScore.getLetterGrade().toDisplayString(),
            curScore.getLetterGrade().toDisplayString(), diff);

    // Compare cipher suites
    Set<String> added = new LinkedHashSet<>(curCipherNames);
    added.removeAll(baseCipherNames);
    Set<String> removed = new LinkedHashSet<>(baseCipherNames);
    removed.removeAll(curCipherNames);
}

See PrintScanDelta.java for a complete working example with grade, category score, and cipher suite comparison.

API Reference

Browse the API JavaDoc for detailed documentation of all public interfaces and classes.

API Usage Examples

Explore the samples package at src/main/java/com/mps/deepviolet/samples/ for working examples:

  • PrintCipherSuites.java -- Enumerate server cipher suites
  • PrintCertificateChain.java -- Retrieve and display certificates
  • PrintRiskScore.java -- Compute and display TLS risk score
  • PrintRevocationStatus.java -- Check certificate revocation status
  • PrintSessionInfo.java -- Display TLS session information
  • PrintTlsFingerprint.java -- Compute TLS server fingerprint
  • PrintBackgroundScan.java -- Run a scan with progress callbacks
  • PrintScan.java -- Parallel multi-host scanning with listeners and monitoring
  • PrintAiAnalysis.java -- AI-powered TLS scan analysis (engine state, file, in-memory)
  • PrintAiChat.java -- Multi-turn AI chat about scan results
  • PrintScanPersistence.java -- Save/load encrypted .dvscan files with envelope encryption
  • PrintSaveScan.java -- Scan multiple hosts and save results to .dvscan
  • PrintScanDelta.java -- Compare two saved scan files to detect changes

API Validation Tool

The validate package (com.mps.deepviolet.validate) provides a standalone tool that compares DV API scan results against openssl for the same server in real-time. This is useful for verifying the accuracy of the DV API against an independent source of truth.

Usage

# Build the validation JAR
mvn package -Pvalidate

# Validate against a well-known server (expect all MATCH)
java -jar target/DeepViolet-*-validate.jar google.com

# Validate against a server with an expired certificate (expect PASS — DV correctly rejects)
java -jar target/DeepViolet-*-validate.jar expired.badssl.com

# JSON output
java -jar target/DeepViolet-*-validate.jar --json github.com

# Custom port
java -jar target/DeepViolet-*-validate.jar example.com --port 8443

How It Works

  1. Runs openssl s_client -connect host:port -servername host -showcerts to get connection info and certificate chain PEMs
  2. Pipes each PEM through openssl x509 -text -noout for parsed certificate details
  3. Runs DeepVioletFactory.initializeSession() + getEngine() for DV API results
  4. Normalizes both sides and compares 17 fields field-by-field

Compared Fields

FieldDV API Sourceopenssl Source
subjectDNcert.getSubjectDN()x509 Subject line
issuerDNcert.getIssuerDN()x509 Issuer line
serialNumbercert.getCertificateSerialNumber()x509 Serial Number
versioncert.getCertificateVersion()x509 Version
signingAlgorithmcert.getSigningAlgorithm()x509 Signature Algorithm
publicKeyAlgorithmcert.getPublicKeyAlgorithm()x509 Public Key Algorithm
publicKeySizecert.getPublicKeySize()x509 key size
publicKeyCurvecert.getPublicKeyCurve()x509 ASN1 OID
notValidBeforecert.getNotValidBefore()x509 Not Before
notValidAftercert.getNotValidAfter()x509 Not After
isSelfSignedcert.isSelfSignedCertificate()subject == issuer
sanCountcert.getSubjectAlternativeNames().size()x509 SAN extension
fingerprintcert.getCertificateFingerPrint()x509 -fingerprint -sha256
negotiatedProtocolsession.getSessionPropertyValue(NEGOTIATED_PROTOCOL)s_client Protocol line
negotiatedCiphersession.getSessionPropertyValue(NEGOTIATED_CIPHER_SUITE)s_client Cipher line
chainLengthcert.getCertificateChain().lengthcount of PEM blocks
ocspStaplingsession.getStapledOcspResponse() != nulls_client OCSP response

Normalization

FieldNormalizer handles cross-tool differences:

  • Key algorithms: "rsaEncryption" → "RSA", "id-ecPublicKey" → "EC"
  • Signing algorithms: "ecdsa-with-SHA256" → "sha256withecdsa" (matching DV's "SHA256withECDSA")
  • Distinguished names: Whitespace normalization and order-independent comparison
  • Serial numbers: Hex format normalization (strip colons, leading zeros, uppercase)
  • Dates: Parses multiple date formats (ISO, openssl's MMM dd HH:mm:ss yyyy GMT, Java default)
  • EC curves: "prime256v1" → "secp256r1"
  • Fingerprints: Strip "SHA256:" prefix, colons, spaces; uppercase

Classes

  • ApiValidator — Orchestrator with main() and validate(host, port) API
  • OpensslRunner — ProcessBuilder wrapper for openssl commands with regex parsing
  • FieldNormalizer — Normalization logic for cross-tool comparison
  • ComparisonResult — Structured result with per-field match/mismatch tracking
  • OpensslResult — Parsed openssl output data model

Contributing

You do not have to be a security expert or a programmer to contribute. Areas where help is welcome:

  • Coding -- Unit tests, automated regression tests, new features
  • Testing -- Finding bugs and verifying fixes
  • Localization -- Translating text strings into other languages
  • Build Management -- CI/CD, badges, build tooling

To report bugs or suggest features, open an issue.

Development Rules

  1. Significant pull requests require accompanying documentation with minimum 10 business days advance notice via issues before implementation
  2. Pull requests purely for stylistic reformatting will be rejected; new code may follow the author's preferred style
  3. Deliveries that introduce needless complexity will be rejected
  4. Changes must maintain a cohesive set of modifications; existing functionality cannot be broken
  5. Improvements should include corresponding unit tests where feasible
  6. All third-party code and libraries must be licensed compatibly with Apache v2. Third-party dependencies are heavily scrutinized for vulnerability history, responsiveness to security fixes, past incidents, and overall project health
  7. Original authors must be acknowledged when incorporating their code
  8. Check in code that is cleaner than you checked out

Dependencies

DependencyPurpose
gson 2.13.1JSON processing
snakeyaml-engine 2.8YAML processing (cipher map, scoring rules)
logback 1.5.6Logging framework
JUnit Jupiter 5.10.3Testing
mockito-core 5.14.2Test mocking framework