Skip to content

Commit

Permalink
api: add FIPS report to client and server
Browse files Browse the repository at this point in the history
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 <bcressey@amazon.com>
  • Loading branch information
bcressey committed Apr 12, 2024
1 parent 760f232 commit cb1df84
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 1 deletion.
13 changes: 13 additions & 0 deletions sources/api/apiclient/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions sources/api/apiclient/README.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
49 changes: 49 additions & 0 deletions sources/api/apiclient/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ enum UpdateSubcommand {
enum ReportSubcommand {
Cis(CisReportArgs),
CisK8s(CisReportArgs),
Fips(FipsReportArgs),
}

/// Stores common user-supplied arguments for the cis report subcommand.
Expand All @@ -113,6 +114,12 @@ struct CisReportArgs {
format: Option<String>,
}

/// Stores common user-supplied arguments for the fips report subcommand.
#[derive(Debug)]
struct FipsReportArgs {
format: Option<String>,
}

/// Stores user-supplied arguments for the 'update check' subcommand.
#[derive(Debug)]
struct UpdateCheckArgs {}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -586,6 +594,7 @@ fn parse_report_args(args: Vec<String>) -> 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),
Expand All @@ -595,6 +604,7 @@ fn parse_report_args(args: Vec<String>) -> 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'"),
};

Expand Down Expand Up @@ -642,6 +652,31 @@ fn parse_cis_arguments(args: Vec<String>) -> CisReportArgs {
CisReportArgs { level, format }
}

/// Parses arguments for the 'report' fips subcommand.
fn parse_report_fips_args(args: Vec<String>) -> ReportSubcommand {
ReportSubcommand::Fips(parse_fips_arguments(args))
}

fn parse_fips_arguments(args: Vec<String>) -> 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

Expand Down Expand Up @@ -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);
}
}

},
}

Expand Down
24 changes: 24 additions & 0 deletions sources/api/apiclient/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ where
Ok(body)
}

/// Handles requesting a FIPS report.
pub async fn get_fips_report<P>(
socket_path: P,
format: Option<String>,
) -> Result<String>
where
P: AsRef<Path>,
{
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;

Expand Down
31 changes: 30 additions & 1 deletion sources/api/apiserver/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -598,6 +600,33 @@ async fn get_cis_report(query: web::Query<HashMap<String, String>>) -> Result<Ht
.body(String::from_utf8_lossy(&output.stdout).to_string()))
}

/// Gets the FIPS Security Policy report.
async fn get_fips_report(query: web::Query<HashMap<String, String>>) -> Result<HttpResponse> {
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
Expand Down
25 changes: 25 additions & 0 deletions sources/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

0 comments on commit cb1df84

Please sign in to comment.