From cb1df84511bebb7648cc3e210af029a79fdb4dcb Mon Sep 17 00:00:00 2001 From: Ben Cressey Date: Wed, 10 Apr 2024 23:19:24 +0000 Subject: [PATCH] api: add FIPS report to client and server Add a server endpoint for generating a FIPS report, and extend the client with a subcommand to call it. The goal is to enable a Crypto Officer without direct access to the underlying host to verify that the system is operating in the approved mode. Signed-off-by: Ben Cressey --- sources/api/apiclient/README.md | 13 +++++++ sources/api/apiclient/README.tpl | 13 +++++++ sources/api/apiclient/src/main.rs | 49 +++++++++++++++++++++++++ sources/api/apiclient/src/report.rs | 24 ++++++++++++ sources/api/apiserver/src/server/mod.rs | 31 +++++++++++++++- sources/api/openapi.yaml | 25 +++++++++++++ 6 files changed, 154 insertions(+), 1 deletion(-) diff --git a/sources/api/apiclient/README.md b/sources/api/apiclient/README.md index 15b87673c6c..dea3dfc89e6 100644 --- a/sources/api/apiclient/README.md +++ b/sources/api/apiclient/README.md @@ -270,6 +270,19 @@ Refer to the [Kubernetes CIS Benchmark] for detailed audit and remediation steps [Bottlerocket CIS Benchmark]: https://www.cisecurity.org/benchmark/bottlerocket [Kubernetes CIS Benchmark]: https://www.cisecurity.org/benchmark/kubernetes +#### FIPS Security Policy reports + +This command can be used to evaluate the current system state and settings for compliance with the requirements of the FIPS Security Policy. + +```shell +apiclient report fips +``` + +The results from each item in the report will be one of: + +- **PASS**: The system has been evaluated to be in compliance with the requirements of the FIPS Security Policy. +- **FAIL**: The system has been evaluated to not be in compliance with the requirements of the FIPS Security Policy. + ## apiclient library The apiclient library provides high-level methods to interact with the Bottlerocket API. See diff --git a/sources/api/apiclient/README.tpl b/sources/api/apiclient/README.tpl index d5784476778..9604d7f6869 100644 --- a/sources/api/apiclient/README.tpl +++ b/sources/api/apiclient/README.tpl @@ -270,6 +270,19 @@ Refer to the [Kubernetes CIS Benchmark] for detailed audit and remediation steps [Bottlerocket CIS Benchmark]: https://www.cisecurity.org/benchmark/bottlerocket [Kubernetes CIS Benchmark]: https://www.cisecurity.org/benchmark/kubernetes +#### FIPS Security Policy reports + +This command can be used to evaluate the current system state and settings for compliance with the requirements of the FIPS Security Policy. + +```shell +apiclient report fips +``` + +The results from each item in the report will be one of: + +- **PASS**: The system has been evaluated to be in compliance with the requirements of the FIPS Security Policy. +- **FAIL**: The system has been evaluated to not be in compliance with the requirements of the FIPS Security Policy. + ## apiclient library {{readme}} diff --git a/sources/api/apiclient/src/main.rs b/sources/api/apiclient/src/main.rs index c48c435ce61..ea992878991 100644 --- a/sources/api/apiclient/src/main.rs +++ b/sources/api/apiclient/src/main.rs @@ -104,6 +104,7 @@ enum UpdateSubcommand { enum ReportSubcommand { Cis(CisReportArgs), CisK8s(CisReportArgs), + Fips(FipsReportArgs), } /// Stores common user-supplied arguments for the cis report subcommand. @@ -113,6 +114,12 @@ struct CisReportArgs { format: Option, } +/// Stores common user-supplied arguments for the fips report subcommand. +#[derive(Debug)] +struct FipsReportArgs { + format: Option, +} + /// Stores user-supplied arguments for the 'update check' subcommand. #[derive(Debug)] struct UpdateCheckArgs {} @@ -153,6 +160,7 @@ fn usage() -> ! { exec Execute a command in a host container. report cis Retrieve a Bottlerocket CIS benchmark compliance report. report cis-k8s Retrieve a Kubernetes CIS benchmark compliance report. + report fips Retrieve a FIPS Security Policy compliance report. raw options: -u, --uri URI Required; URI to request from the server, e.g. /tx @@ -586,6 +594,7 @@ fn parse_report_args(args: Vec) -> Subcommand { // Subcommands "cis" if subcommand.is_none() && !arg.starts_with('-') => subcommand = Some(arg), "cis-k8s" if subcommand.is_none() && !arg.starts_with('-') => subcommand = Some(arg), + "fips" if subcommand.is_none() && !arg.starts_with('-') => subcommand = Some(arg), // Other arguments are passed to the subcommand parser _ => subcommand_args.push(arg), @@ -595,6 +604,7 @@ fn parse_report_args(args: Vec) -> Subcommand { let report_type = match subcommand.as_deref() { Some("cis") => parse_report_cis_args(subcommand_args), Some("cis-k8s") => parse_report_cis_k8s_args(subcommand_args), + Some("fips") => parse_report_fips_args(subcommand_args), _ => usage_msg("Missing or unknown subcommand for 'report'"), }; @@ -642,6 +652,31 @@ fn parse_cis_arguments(args: Vec) -> CisReportArgs { CisReportArgs { level, format } } +/// Parses arguments for the 'report' fips subcommand. +fn parse_report_fips_args(args: Vec) -> ReportSubcommand { + ReportSubcommand::Fips(parse_fips_arguments(args)) +} + +fn parse_fips_arguments(args: Vec) -> FipsReportArgs { + let mut format = None; + + let mut iter = args.into_iter(); + while let Some(arg) = iter.next() { + match arg.as_ref() { + "-f" | "--format" => { + format = Some( + iter.next() + .unwrap_or_else(|| usage_msg("Did not give argument to -f | --format")), + ) + } + + x => usage_msg(format!("Unknown argument '{}'", x)), + } + } + + FipsReportArgs { format } +} + // =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= // Helpers @@ -858,6 +893,20 @@ async fn run() -> Result<()> { print!("{}", body); } } + + ReportSubcommand::Fips(fips_args) => { + let body = report::get_fips_report( + &args.socket_path, + fips_args.format, + ) + .await + .context(error::ReportSnafu)?; + + if !body.is_empty() { + print!("{}", body); + } + } + }, } diff --git a/sources/api/apiclient/src/report.rs b/sources/api/apiclient/src/report.rs index ec7ac19a9ee..ab7879dc3f9 100644 --- a/sources/api/apiclient/src/report.rs +++ b/sources/api/apiclient/src/report.rs @@ -30,6 +30,30 @@ where Ok(body) } +/// Handles requesting a FIPS report. +pub async fn get_fips_report

( + socket_path: P, + format: Option, +) -> Result +where + P: AsRef, +{ + let method = "GET"; + + let mut query = Vec::new(); + if let Some(query_format) = format { + query.push(format!("format={}", query_format)); + } + + let uri = format!("/report/fips?{}", query.join("&")); + + let (_status, body) = crate::raw_request(&socket_path, &uri, method, None) + .await + .context(error::RequestSnafu { uri, method })?; + + Ok(body) +} + mod error { use snafu::Snafu; diff --git a/sources/api/apiserver/src/server/mod.rs b/sources/api/apiserver/src/server/mod.rs index f4224d0275c..e8480da8dc5 100644 --- a/sources/api/apiserver/src/server/mod.rs +++ b/sources/api/apiserver/src/server/mod.rs @@ -31,6 +31,7 @@ use tokio::process::Command as AsyncCommand; const BLOODHOUND_BIN: &str = "/usr/bin/bloodhound"; const BLOODHOUND_K8S_CHECKS: &str = "/usr/libexec/cis-checks/kubernetes"; +const BLOODHOUND_FIPS_CHECKS: &str = "/usr/libexec/fips-checks/bottlerocket"; // =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= @@ -128,7 +129,8 @@ where .service( web::scope("/report") .route("", web::get().to(list_reports)) - .route("/cis", web::get().to(get_cis_report)), + .route("/cis", web::get().to(get_cis_report)) + .route("/fips", web::get().to(get_fips_report)), ) }) .workers(threads) @@ -598,6 +600,33 @@ async fn get_cis_report(query: web::Query>) -> Result>) -> Result { + let mut cmd = AsyncCommand::new(BLOODHOUND_BIN); + + // Check for requested format, default is text + if let Some(format) = query.get("format") { + cmd.arg("-f").arg(format); + } + + cmd.arg("-c").arg(BLOODHOUND_FIPS_CHECKS); + + let output = cmd.output().await.context(error::ReportExecSnafu)?; + ensure!( + output.status.success(), + error::ReportResultSnafu { + exit_code: match output.status.code() { + Some(code) => code, + None => output.status.signal().unwrap_or(1), + }, + stderr: String::from_utf8_lossy(&output.stderr), + } + ); + Ok(HttpResponse::Ok() + .content_type("application/text") + .body(String::from_utf8_lossy(&output.stdout).to_string())) +} + // =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= // Helpers for handler methods called by the router diff --git a/sources/api/openapi.yaml b/sources/api/openapi.yaml index 19c456baf09..867faf9199c 100644 --- a/sources/api/openapi.yaml +++ b/sources/api/openapi.yaml @@ -682,3 +682,28 @@ paths: description: "Unprocessable request" 500: description: "Server error" + + /report/fips: + get: + summary: "Get FIPS Security Policy report" + operationId: "fips-report" + parameters: + - in: query + name: format + description: "The FIPS Security Policy report format (text or json). Default format is text." + schema: + type: string + required: false + responses: + 200: + description: "Successful request" + content: + application/json: + schema: + type: string + 400: + description: "Bad request input" + 422: + description: "Unprocessable request" + 500: + description: "Server error"