csaf-crud 0.3.4

CSAF 2.0 / 2.1 advisory CRUD server with HATEOAS JSON API and HTML UI (TLS 1.3, HTTP/1.1 + HTTP/2 + HTTP/3)
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! Static information page route handlers.
//!
//! Provides About, License, System Info, Privacy, and Security pages.

use http::{Response, StatusCode};

use crate::app_state::AppState;
use crate::router::{Body, html_response};
use crate::routes::layout::{Nav, html_escape, wrap_page};

/// Render an info page with the shared layout and a sub-nav for the Info
/// section.
fn render_info_page(title: &str, theme: &str, body: &str) -> Response<Body> {
    let sub_nav = r#"<div class="card" style="padding:0.75rem 1rem;">
  <a href="/info/about">About</a> &middot;
  <a href="/info/license">License</a> &middot;
  <a href="/info/system">System Info</a> &middot;
  <a href="/info/privacy">Privacy</a> &middot;
  <a href="/info/security">Security</a>
</div>"#;

    let content = format!("{sub_nav}{body}");
    html_response(StatusCode::OK, wrap_page(title, theme, Nav::Info, &content))
}

/// `GET /info/about` -- About page.
pub async fn about(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let body = r#"<h1>About CSAF</h1>
<div class="card">
  <p><strong>CSAF</strong> is a web-based management tool for creating, reading,
  updating, and deleting Common Security Advisory Framework (CSAF) documents
  conforming to OASIS CSAF versions 2.0 and 2.1.</p>

  <h2>Features</h2>
  <ul>
    <li>Full CRUD operations for CSAF security advisories, VEX, and informational advisories</li>
    <li>Built-in validation against CSAF 2.0/2.1 schema rules</li>
    <li>CVSS v3.1 and v4.0 scoring display</li>
    <li>SHA-256 and SHA3-512 sidecar hash file generation</li>
    <li>Bulk import/export with filesystem directory scanning</li>
    <li>Audit trail for all document operations</li>
    <li>HATEOAS-compliant REST API</li>
    <li>Embedded storage (redb + SQLite) — no external database required</li>
    <li>TLS 1.3 with automatic certificate generation</li>
  </ul>

  <h2>Standards</h2>
  <ul>
    <li><a href="https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html"
           target="_blank" rel="noopener">OASIS CSAF 2.0</a></li>
    <li><a href="https://docs.oasis-open.org/csaf/csaf/v2.1/csaf-v2.1.html"
           target="_blank" rel="noopener">OASIS CSAF 2.1</a></li>
  </ul>

  <h2>Contact</h2>
  <p>Developed by <strong>ndaal Gesellschaft für Sicherheit in der
  Informationstechnik mbH &amp; Co KG</strong>, Cologne, Germany.</p>
</div>"#;

    render_info_page("About", &state.settings().theme, body)
}

/// `GET /info/license` -- License page.
pub async fn license(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let body = r#"<h1>License</h1>
<div class="card">
  <p>CSAF is licensed under the <strong>Apache License, Version 2.0</strong>.</p>
  <p>SPDX-License-Identifier: <code>Apache-2.0</code></p>
  <p>Copyright (c) 2026 Pierre Gronau, ndaal in Cologne.</p>

  <h2>Apache License 2.0 Summary</h2>
  <ul>
    <li>You may use, modify, and distribute this software freely.</li>
    <li>You must include the copyright notice and license in any distribution.</li>
    <li>The software is provided "AS IS", without warranties.</li>
    <li>Contributors grant a patent license for their contributions.</li>
  </ul>
  <p>Full license text:
    <a href="https://www.apache.org/licenses/LICENSE-2.0"
       target="_blank" rel="noopener">https://www.apache.org/licenses/LICENSE-2.0</a>
  </p>
</div>"#;

    render_info_page("License", &state.settings().theme, body)
}

/// `GET /info/system` -- System information page.
pub async fn system_info(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let mut sys = sysinfo::System::new_all();
    sys.refresh_all();

    let os_name = sysinfo::System::name().unwrap_or_else(|| "Unknown".to_owned());
    let os_version = sysinfo::System::os_version().unwrap_or_else(|| "Unknown".to_owned());
    let kernel_version = sysinfo::System::kernel_version().unwrap_or_else(|| "Unknown".to_owned());
    let hostname = sysinfo::System::host_name().unwrap_or_else(|| "Unknown".to_owned());

    let total_memory_mb = sys.total_memory() / 1_048_576;
    let used_memory_mb = sys.used_memory() / 1_048_576;
    let cpu_count = sys.cpus().len();

    let doc_count = state.csaf_storage().count_documents().unwrap_or(0);
    let storage_ok = state.csaf_storage().check_storage_up().unwrap_or(false);

    let config = state.config();
    let settings = state.settings();

    let body = format!(
        r#"<h1>System Information</h1>
<div class="card">
  <h2>Host</h2>
  <table>
    <tr><th>Hostname</th><td>{hostname}</td></tr>
    <tr><th>OS</th><td>{os_name} {os_version}</td></tr>
    <tr><th>Kernel</th><td>{kernel_version}</td></tr>
    <tr><th>CPUs</th><td>{cpu_count}</td></tr>
    <tr><th>Memory</th><td>{used_memory_mb} MB / {total_memory_mb} MB</td></tr>
  </table>
</div>
<div class="card">
  <h2>Application</h2>
  <table>
    <tr><th>Version</th><td>{app_version}</td></tr>
    <tr><th>Rust Version</th><td>{rust_version}</td></tr>
    <tr><th>Listen Address</th><td>{listen}</td></tr>
    <tr><th>Data Directory</th><td>{data_dir}</td></tr>
    <tr><th>Storage Status</th><td>{storage_status}</td></tr>
    <tr><th>Documents Stored</th><td>{doc_count}</td></tr>
    <tr><th>CSAF Mode</th><td>{csaf_mode}</td></tr>
  </table>
</div>"#,
        hostname = html_escape(&hostname),
        os_name = html_escape(&os_name),
        os_version = html_escape(&os_version),
        kernel_version = html_escape(&kernel_version),
        app_version = env!("CARGO_PKG_VERSION"),
        rust_version = html_escape(env!("CARGO_PKG_RUST_VERSION", "unknown")),
        listen = html_escape(&config.listen_address()),
        data_dir = html_escape(&config.data_dir.display().to_string()),
        storage_status = if storage_ok { "Healthy" } else { "Degraded" },
        csaf_mode = html_escape(&settings.csaf_mode),
    );

    render_info_page("System Info", &settings.theme, &body)
}

/// `GET /info/privacy` -- Privacy page.
pub async fn privacy(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let body = r#"<h1>Privacy</h1>
<div class="card">
  <h2>Data Processing</h2>
  <p>CSAF is designed as a self-hosted application. All data is stored
  locally on the server where the application is deployed.</p>

  <h2>Data Collected</h2>
  <ul>
    <li><strong>CSAF documents</strong> — stored in the embedded redb database</li>
    <li><strong>User accounts</strong> — login credentials (hashed) stored in SQLite</li>
    <li><strong>Audit logs</strong> — timestamps and actions for document operations</li>
    <li><strong>Session data</strong> — temporary session tokens for authentication</li>
  </ul>

  <h2>No External Communication</h2>
  <p>The application does not send data to external services, analytics
  platforms, or third-party APIs. All processing occurs locally.</p>

  <h2>Data Retention</h2>
  <p>Data is retained until explicitly deleted by the user or administrator.
  Audit logs are stored indefinitely for compliance purposes.</p>

  <h2>GDPR Compliance</h2>
  <p>As a self-hosted tool, GDPR compliance responsibility lies with the
  deploying organisation. The application supports data export and deletion
  to facilitate compliance.</p>
</div>"#;

    render_info_page("Privacy", &state.settings().theme, body)
}

/// `GET /info/security` -- Security page.
pub async fn security(
    state: AppState,
    _parts: http::request::Parts,
    _params: Vec<(String, String)>,
) -> Response<Body> {
    let body = r#"<h1>Security</h1>
<div class="card">
  <h2>Transport Security</h2>
  <ul>
    <li>TLS 1.3 enforced via Rustls (no OpenSSL dependency)</li>
    <li>RSA ciphers excluded; only elliptic-curve cryptography (ECC) used</li>
    <li>Automatic self-signed certificate generation (45-day validity)</li>
    <li>HTTP/1.1 + HTTP/2 auto-negotiated via ALPN over TCP</li>
    <li>HTTP/3 served over QUIC/UDP</li>
  </ul>

  <h2>Authentication</h2>
  <ul>
    <li>Password hashing using Argon2id (RFC 9106)</li>
    <li>API key authentication for programmatic access</li>
    <li>Session-based authentication for the web UI</li>
  </ul>

  <h2>Data Integrity</h2>
  <ul>
    <li>SHA-256 and SHA3-512 sidecar hash files for exported documents</li>
    <li>CSAF document validation on every create, update, import, and export</li>
    <li>Audit trail for all document lifecycle operations</li>
  </ul>

  <h2>Input Validation</h2>
  <ul>
    <li>All CSAF documents validated against structural and semantic rules</li>
    <li>CVSS score range validation (0.0 to 10.0)</li>
    <li>Product ID cross-reference validation</li>
    <li>HTML output is escaped to prevent XSS</li>
    <li>RFC 9457 Problem Details for all API errors</li>
  </ul>

  <h2>Reporting Vulnerabilities</h2>
  <p>If you discover a security vulnerability in CSAF, please report it
  responsibly by contacting the development team at ndaal in Cologne.</p>
</div>"#;

    render_info_page("Security", &state.settings().theme, body)
}