mod access;
mod checks;
mod cloud;
mod docker_vm;
mod leaks;
mod lifecycle;
mod risks;
mod terraform;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use crate::net::FakePorts;
use crate::process::fake::{Call, FakeProcesses, Reply};
use crate::secrets::{SecretValue, Secrets};
use crate::testing::{FLAG_PORTAL, LabFolder, modelled, processes, runner_config};
use crate::{Event, EventKind, Recorder, RunOptions, Runner};
const FLAG: &str = "flag{fake-0001}";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum Containers {
#[default]
None,
Up,
Stopped,
}
#[derive(Debug, Default)]
struct Engine {
containers: Containers,
check_time: Vec<(String, Duration)>,
}
struct Fixture {
folder: LabFolder,
manifest: PathBuf,
fake: Arc<FakeProcesses>,
ports: Arc<FakePorts>,
engine: Arc<Mutex<Engine>>,
runner: Runner,
}
fn fixture() -> Fixture {
fixture_with(FLAG_PORTAL)
}
fn fixture_with(manifest: &str) -> Fixture {
fixture_answering(manifest, answer)
}
fn fixture_answering(manifest: &str, answer: fn(&Mutex<Engine>, &Call) -> Reply) -> Fixture {
let folder = LabFolder::new("docker", manifest);
let (engine, fake) = modelled(answer);
let ports = Arc::new(FakePorts::new([41873, 41874, 41875]));
let host_ports: Arc<dyn crate::HostPorts> = Arc::<FakePorts>::clone(&ports);
let runner = Runner::new(runner_config(folder.home.path(), processes(&fake), host_ports, crate::this_host())).unwrap();
Fixture {
manifest: folder.manifest.clone(),
folder,
fake,
ports,
engine,
runner,
}
}
impl Fixture {
fn home(&self) -> &std::path::Path {
self.folder.home.path()
}
fn containers(&self) -> Containers {
self.engine.lock().unwrap_or_else(PoisonError::into_inner).containers
}
fn set(&self, containers: Containers) {
self.engine.lock().unwrap_or_else(PoisonError::into_inner).containers = containers;
}
fn check_takes(&self, service: &str, time: Duration) {
self.engine
.lock()
.unwrap_or_else(PoisonError::into_inner)
.check_time
.push((service.into(), time));
}
fn called(&self, text: &str) -> usize {
self.fake.lines().iter().filter(|l| l.contains(text)).count()
}
}
fn opts() -> RunOptions {
let mut secrets = Secrets::new();
secrets.insert("CTF_FLAG_MAIN", SecretValue::new(FLAG));
RunOptions {
secrets,
..RunOptions::default()
}
}
fn kinds(events: &Recorder) -> Vec<EventKind> {
events.events().into_iter().map(|e: Event| e.kind).collect()
}
fn answer(engine: &Mutex<Engine>, call: &Call) -> Reply {
let line = call.line();
let mut e = engine.lock().unwrap_or_else(PoisonError::into_inner);
let up = e.containers == Containers::Up;
if line.contains(" ps --all ") || line.contains(" ps --format json") {
return Reply::ok(&ps(e.containers, line.contains("--all"), pinned(call)));
}
if line.contains(" config --format json") {
let published = if call.env("ISOLOOM_PUBLISH_FIXED").is_some() {
Some(8080)
} else {
pinned(call)
};
return Reply::ok(&config(published));
}
if line.contains(" up --detach ") {
e.containers = Containers::Up;
return Reply::ok(&format!(" Container web Started\nseeding {}", call.env("CTF_FLAG_MAIN").unwrap_or("no flag")));
}
if line.contains("--profile check run") {
return check_runner(&e, &line);
}
let next = match () {
() if line.ends_with(" stop") && up => Containers::Stopped,
() if line.ends_with(" start") && e.containers == Containers::Stopped => Containers::Up,
() if line.contains(" down --volumes") => Containers::None,
() if line.starts_with("docker ps -aq") => {
return Reply::ok(if e.containers == Containers::None { "" } else { "c1\nc2" });
}
() => e.containers,
};
e.containers = next;
Reply::ok("")
}
fn pinned(call: &Call) -> Option<u16> {
let file = call.args.iter().find(|a| a.ends_with("ports.yml"))?;
let text = std::fs::read_to_string(file).ok()?;
let (_, after) = text.split_once("127.0.0.1:")?;
after.split(':').next()?.parse().ok()
}
fn ps(containers: Containers, all: bool, port: Option<u16>) -> String {
let (state, running) = match containers {
Containers::None => return String::new(),
Containers::Up => ("running", true),
Containers::Stopped => ("exited", false),
};
let publishers = port.map_or_else(String::new, |p| {
format!(r#"{{"URL":"127.0.0.1","PublishedPort":{p},"TargetPort":80,"Protocol":"tcp"}}"#)
});
let mut lines: Vec<String> = ["web", "db", "web-routes", "db-routes", "isoloom-probe-amd64"]
.iter()
.map(|s| {
format!(
r#"{{"Service":"{s}","State":"{state}","Health":"","Publishers":[{}]}}"#,
if *s == "web" { publishers.as_str() } else { "" }
)
})
.collect();
if all {
lines.push(r#"{"Service":"web-init-1","State":"exited","ExitCode":0}"#.into());
}
if !running && !all {
return String::new();
}
lines.join("\n")
}
fn config(published: Option<u16>) -> String {
let published = published.map_or_else(|| "\"\"".to_owned(), |p| format!("\"{p}\""));
format!(
r#"{{"services":{{"web":{{"ports":[{{"target":80,"host_ip":"127.0.0.1","published":{published},"protocol":"tcp"}}]}},"db":{{}}}},"networks":{{"lab":{{"name":"flag-portal_lab"}}}}}}"#
)
}
fn check_runner(e: &Engine, line: &str) -> Reply {
let service = line.rsplit(' ').next().unwrap_or_default();
let names: &[&str] = match service {
"isoloom-check" => &["http://web/", "db:3306", "build/check/check.sh"],
"isoloom-check-db" => &["web:80 from db", "no internet from db"],
_ => &["db:3306 from web", "no internet from web"],
};
let mut out: Vec<String> = names.iter().map(|n| format!("isoloom-check: PASS {n}")).collect();
out.push(format!("isoloom-check: END {} passed, 0 failed", names.len()));
let time = e.check_time.iter().find(|(s, _)| s == service).map_or(Duration::ZERO, |(_, t)| *t);
Reply::ok(&out.join("\n")).after(time)
}