Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apiserver: Add /report endpoint for CIS reporting #3219

Merged
merged 1 commit into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sources/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions sources/api/apiserver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ serde_json = "1"
simplelog = "0.12"
snafu = "0.7"
thar-be-updates = { path = "../thar-be-updates", version = "0.1" }
tokio = { version = "~1.25", default-features = false, features = ["process"] }
walkdir = "2"

[build-dependencies]
Expand Down
16 changes: 16 additions & 0 deletions sources/api/apiserver/src/server/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,22 @@ pub enum Error {
#[snafu(display("Failed to reboot, exit code: {}, stderr: {}", exit_code, stderr))]
Reboot { exit_code: i32, stderr: String },

#[snafu(display("Unable to generate report: {}", source))]
ReportExec { source: io::Error },

#[snafu(display(
"Failed to generate report, exit code: {}, stderr: {}",
exit_code,
stderr
))]
ReportResult { exit_code: i32, stderr: String },

#[snafu(display("Report type must be specified"))]
ReportTypeMissing {},

#[snafu(display("Report type '{}' is not supported", report_type))]
ReportNotSupported { report_type: String },

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

// Update related errors
Expand Down
55 changes: 54 additions & 1 deletion sources/api/apiserver/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use error::Result;
use fs2::FileExt;
use http::StatusCode;
use log::info;
use model::{ConfigurationFiles, Model, Services, Settings};
use model::{ConfigurationFiles, Model, Report, Services, Settings};
use nix::unistd::{chown, Gid};
use snafu::{ensure, OptionExt, ResultExt};
use std::collections::{HashMap, HashSet};
Expand All @@ -27,6 +27,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync;
use thar_be_updates::status::{UpdateStatus, UPDATE_LOCKFILE};
use tokio::process::Command as AsyncCommand;

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

Expand Down Expand Up @@ -121,6 +122,11 @@ where
)
.service(web::scope("/updates").route("/status", web::get().to(get_update_status)))
.service(web::resource("/exec").route(web::get().to(exec::ws_exec)))
.service(
web::scope("/report")
.route("", web::get().to(list_reports))
.route("/cis", web::get().to(get_cis_report)),
)
})
.workers(threads)
.bind_uds(socket_path.as_ref())
Expand Down Expand Up @@ -543,6 +549,46 @@ async fn reboot() -> Result<HttpResponse> {
Ok(HttpResponse::NoContent().finish())
}

/// Gets the set of report types supported by this host.
async fn list_reports() -> Result<ReportListResponse> {
// Add each report to list response when adding a new handler
let data = vec![Report {
name: "cis".to_string(),
description: "CIS Bottlerocket Benchmark".to_string(),
}];
Ok(ReportListResponse(data))
}

/// Gets the Bottlerocket CIS benchmark report.
async fn get_cis_report(query: web::Query<HashMap<String, String>>) -> Result<HttpResponse> {
let mut cmd = AsyncCommand::new("/usr/bin/bloodhound");

// Check for requested level, default is 1
if let Some(level) = query.get("level") {
cmd.arg("-l").arg(level);
}

// Check for requested format, default is text
stmcginnis marked this conversation as resolved.
Show resolved Hide resolved
if let Some(format) = query.get("format") {
cmd.arg("-f").arg(format);
}

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 Expand Up @@ -572,6 +618,7 @@ impl ResponseError for error::Error {
MissingInput { .. } => StatusCode::BAD_REQUEST,
EmptyInput { .. } => StatusCode::BAD_REQUEST,
NewKey { .. } => StatusCode::BAD_REQUEST,
ReportTypeMissing { .. } => StatusCode::BAD_REQUEST,

// 404 Not Found
MissingData { .. } => StatusCode::NOT_FOUND,
Expand All @@ -582,6 +629,7 @@ impl ResponseError for error::Error {

// 422 Unprocessable Entity
CommitWithNoPending => StatusCode::UNPROCESSABLE_ENTITY,
ReportNotSupported { .. } => StatusCode::UNPROCESSABLE_ENTITY,

// 423 Locked
UpdateShareLock { .. } => StatusCode::LOCKED,
Expand Down Expand Up @@ -618,6 +666,8 @@ impl ResponseError for error::Error {
UpdateStatusParse { .. } => StatusCode::INTERNAL_SERVER_ERROR,
UpdateInfoParse { .. } => StatusCode::INTERNAL_SERVER_ERROR,
UpdateLockOpen { .. } => StatusCode::INTERNAL_SERVER_ERROR,
ReportExec { .. } => StatusCode::INTERNAL_SERVER_ERROR,
ReportResult { .. } => StatusCode::INTERNAL_SERVER_ERROR,
};

HttpResponse::build(status_code).body(self.to_string())
Expand Down Expand Up @@ -702,3 +752,6 @@ impl_responder_for!(ChangedKeysResponse, self, self.0);

struct TransactionListResponse(HashSet<String>);
impl_responder_for!(TransactionListResponse, self, self.0);

struct ReportListResponse(Vec<Report>);
impl_responder_for!(ReportListResponse, self, self.0);
48 changes: 48 additions & 0 deletions sources/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,51 @@ paths:
description: "Connection upgraded to WebSocket"
500:
description: "Server error"

/report:
get:
summary: "Get available report types"
operationId: "get_reports"
responses:
200:
description: "Successful request"
content:
application/json:
schema:
type: array
$ref: "Report"
500:
description: "Server error"

/report/cis:
get:
summary: "Get CIS Bottlerocket benchmark report"
operationId: "cis-report"
parameters:
- in: query
name: level
description: "The CIS compliance level to test (1 or 2). Default level is 1."
schema:
type: integer
minimum: 1
maximum: 2
required: false
- in: query
name: format
description: "The CIS compliance report format (text or json). Default format is text."
schema:
type: string
required: false
responses:
200:
description: "Successful request"
content:
application/json:
schema:
$ref: "String"
400:
description: "Bad request input"
422:
description: "Unprocessable request"
500:
description: "Server error"
6 changes: 6 additions & 0 deletions sources/models/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,9 @@ struct OciDefaultsResourceLimit {
hard_limit: u32,
soft_limit: u32,
}

#[model(add_option = false)]
struct Report {
name: String,
description: String,
}