diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4afeceebb3..0b79cba665 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -253,8 +253,10 @@ jobs: cp artifacts/azcopy/azcopy.exe artifacts/azcopy/ThirdPartyNotice.txt src/deployment/tools/win64 cp artifacts/agent/onefuzz-supervisor.exe src/deployment/tools/win64/ cp artifacts/agent/onefuzz-agent.exe src/deployment/tools/win64/ + cp artifacts/agent/onefuzz-downloader.exe src/deployment/tools/win64/ cp artifacts/agent/onefuzz-supervisor src/deployment/tools/linux/ cp artifacts/agent/onefuzz-agent src/deployment/tools/linux/ + cp artifacts/agent/onefuzz-downloader src/deployment/tools/linux/ cp artifacts/proxy/onefuzz-proxy-manager src/deployment/tools/linux/ cp artifacts/service/api-service.zip src/deployment cp -r artifacts/third-party src/deployment diff --git a/.gitignore b/.gitignore index 4681a90788..a93e85ab8e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,6 @@ .idea **/.direnv **/.envrc - +artifacts # vim *.swp diff --git a/src/agent/Cargo.lock b/src/agent/Cargo.lock index 204a10eef1..a0f15d8fcf 100644 --- a/src/agent/Cargo.lock +++ b/src/agent/Cargo.lock @@ -1274,6 +1274,29 @@ dependencies = [ "uuid", ] +[[package]] +name = "onefuzz-downloader" +version = "0.1.0" +dependencies = [ + "anyhow", + "appinsights", + "async-trait", + "clap", + "downcast-rs", + "env_logger", + "futures", + "log", + "onefuzz", + "reqwest", + "serde", + "serde_json", + "storage-queue", + "structopt", + "tokio", + "url", + "uuid", +] + [[package]] name = "onefuzz-supervisor" version = "0.1.0" diff --git a/src/agent/Cargo.toml b/src/agent/Cargo.toml index a486217126..cfaf322a80 100644 --- a/src/agent/Cargo.toml +++ b/src/agent/Cargo.toml @@ -5,6 +5,7 @@ members = [ "input-tester", "onefuzz", "onefuzz-agent", + "onefuzz-downloader", "onefuzz-supervisor", "storage-queue", "win-util", diff --git a/src/agent/onefuzz-downloader/.gitignore b/src/agent/onefuzz-downloader/.gitignore new file mode 100644 index 0000000000..04eeebc316 --- /dev/null +++ b/src/agent/onefuzz-downloader/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +data/licenses.json \ No newline at end of file diff --git a/src/agent/onefuzz-downloader/Cargo.toml b/src/agent/onefuzz-downloader/Cargo.toml new file mode 100644 index 0000000000..ab28f40aa8 --- /dev/null +++ b/src/agent/onefuzz-downloader/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "onefuzz-downloader" +version = "0.1.0" +authors = ["fuzzing@microsoft.com"] +edition = "2018" +publish = false + + +[dependencies] +anyhow = "1.0.31" +appinsights = "0.1" +async-trait = "0.1.36" +downcast-rs = "1.2.0" +env_logger = "0.7" +futures = "0.3.5" +log = "0.4" +onefuzz = { path = "../onefuzz" } +reqwest = { version = "0.10", features = ["json", "stream"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +storage-queue = { path = "../storage-queue" } +structopt = "0.3" +tokio = { version = "0.2.13", features = ["full"] } +url = { version = "2.1.1", features = ["serde"] } +uuid = { version = "0.8.1", features = ["serde", "v4"] } +clap = "2.33" diff --git a/src/agent/onefuzz-downloader/build.rs b/src/agent/onefuzz-downloader/build.rs new file mode 100644 index 0000000000..bec63b58b2 --- /dev/null +++ b/src/agent/onefuzz-downloader/build.rs @@ -0,0 +1,56 @@ +use std::env; +use std::error::Error; +use std::fs::File; +use std::io::prelude::*; +use std::process::Command; + +fn run_cmd(args: &[&str]) -> Result> { + let cmd = Command::new(args[0]).args(&args[1..]).output()?; + if cmd.status.success() { + Ok(String::from_utf8_lossy(&cmd.stdout).trim().to_string()) + } else { + Err(From::from("failed")) + } +} + +fn read_file(filename: &str) -> Result> { + let mut file = File::open(filename)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + contents = contents.trim().to_string(); + + Ok(contents) +} + +fn print_version(include_sha: bool, include_local: bool) -> Result<(), Box> { + let mut version = read_file("../../../CURRENT_VERSION")?; + let sha = run_cmd(&["git", "rev-parse", "HEAD"])?; + + if include_sha { + version.push('-'); + version.push_str(&sha); + + // if we're a non-release build, check to see if git has + // unstaged changes + if include_local && run_cmd(&["git", "diff", "--quiet"]).is_err() { + version.push('.'); + version.push_str("localchanges"); + } + } + + println!("cargo:rustc-env=GIT_VERSION={}", sha); + println!("cargo:rustc-env=ONEFUZZ_VERSION={}", version); + + Ok(()) +} + +fn main() -> Result<(), Box> { + // If we're built off of a tag, we accept CURRENT_VERSION as is. Otherwise + // modify it to indicate local build + let (include_sha, include_local_changes) = if let Ok(val) = env::var("GITHUB_REF") { + (!val.starts_with("refs/tags/"), false) + } else { + (true, true) + }; + print_version(include_sha, include_local_changes) +} diff --git a/src/agent/onefuzz-downloader/src/config.rs b/src/agent/onefuzz-downloader/src/config.rs new file mode 100644 index 0000000000..09449c062a --- /dev/null +++ b/src/agent/onefuzz-downloader/src/config.rs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use anyhow::Result; +use onefuzz::auth::{ClientCredentials, Credentials, ManagedIdentityCredentials}; +use std::path::Path; +use url::Url; +use uuid::Uuid; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub struct StaticConfig { + pub credentials: Credentials, + pub pool_name: String, + pub onefuzz_url: Url, + pub instrumentation_key: Option, + pub telemetry_key: Option, +} + +// Temporary shim type to bridge the current service-provided config. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +struct RawStaticConfig { + pub credentials: Option, + pub pool_name: String, + pub onefuzz_url: Url, + pub instrumentation_key: Option, + pub telemetry_key: Option, +} + +impl StaticConfig { + pub fn new(data: &[u8]) -> Result { + let config: RawStaticConfig = serde_json::from_slice(data)?; + + let credentials = match config.credentials { + Some(client) => client.into(), + None => { + // Remove trailing `/`, which is treated as a distinct resource. + let resource = config + .onefuzz_url + .to_string() + .trim_end_matches('/') + .to_owned(); + let managed = ManagedIdentityCredentials::new(resource); + managed.into() + } + }; + let config = StaticConfig { + credentials, + pool_name: config.pool_name, + onefuzz_url: config.onefuzz_url, + instrumentation_key: config.instrumentation_key, + telemetry_key: config.telemetry_key, + }; + + Ok(config) + } + + pub fn from_env() -> Result { + let client_id = Uuid::parse_str(&std::env::var("ONEFUZZ_CLIENT_ID")?)?; + let client_secret = std::env::var("ONEFUZZ_CLIENT_SECRET")?.into(); + let tenant = std::env::var("ONEFUZZ_TENANT")?; + let onefuzz_url = Url::parse(&std::env::var("ONEFUZZ_URL")?)?; + let pool_name = std::env::var("ONEFUZZ_POOL")?; + + let instrumentation_key = if let Ok(key) = std::env::var("ONEFUZZ_INSTRUMENTATION_KEY") { + Some(Uuid::parse_str(&key)?) + } else { + None + }; + + let telemetry_key = if let Ok(key) = std::env::var("ONEFUZZ_TELEMETRY_KEY") { + Some(Uuid::parse_str(&key)?) + } else { + None + }; + + let credentials = ClientCredentials::new( + client_id, + client_secret, + onefuzz_url.clone().to_string(), + tenant, + ) + .into(); + + Ok(Self { + credentials, + pool_name, + onefuzz_url, + instrumentation_key, + telemetry_key, + }) + } + + pub fn from_file(config_path: impl AsRef) -> Result { + verbose!("loading config from: {}", config_path.as_ref().display()); + let data = std::fs::read(config_path)?; + Self::new(&data) + } + + pub fn download_url(&self) -> Url { + let mut url = self.onefuzz_url.clone(); + url.set_path("/api/agents/download"); + url + } +} diff --git a/src/agent/onefuzz-downloader/src/main.rs b/src/agent/onefuzz-downloader/src/main.rs new file mode 100644 index 0000000000..1b91b5c645 --- /dev/null +++ b/src/agent/onefuzz-downloader/src/main.rs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[macro_use] +extern crate anyhow; +#[macro_use] +extern crate onefuzz; +#[macro_use] +extern crate serde; +#[macro_use] +extern crate clap; + +use std::path::PathBuf; + +use anyhow::Result; +use onefuzz::{ + machine_id::get_machine_id, + telemetry::{self, EventData}, +}; +use structopt::StructOpt; + +pub mod config; +pub mod setup; + +use config::StaticConfig; + +#[derive(StructOpt, Debug)] +enum Opt { + Run(RunOpt), + Licenses, + Version, +} + +#[derive(StructOpt, Debug)] +struct RunOpt { + #[structopt(short, long = "--config", parse(from_os_str))] + config_path: Option, + + #[structopt(short, long = "--onefuzz_path", parse(from_os_str))] + onefuzz_path: Option, + + #[structopt(short, long)] + start_supervisor: bool, +} + +fn main() -> Result<()> { + env_logger::init(); + + let opt = Opt::from_args(); + + match opt { + Opt::Run(opt) => run(opt)?, + Opt::Licenses => licenses()?, + Opt::Version => versions()?, + }; + + Ok(()) +} + +fn versions() -> Result<()> { + println!( + "{} onefuzz:{} git:{}", + crate_version!(), + env!("ONEFUZZ_VERSION"), + env!("GIT_VERSION") + ); + Ok(()) +} + +fn licenses() -> Result<()> { + use std::io::{self, Write}; + io::stdout().write_all(include_bytes!("../../data/licenses.json"))?; + Ok(()) +} + +fn run(opt: RunOpt) -> Result<()> { + // We can't send telemetry if this fails. + let config = load_config(&opt); + + if let Err(err) = &config { + error!("error loading supervisor agent config: {:?}", err); + } + + let config = config?; + init_telemetry(&config); + + let mut rt = tokio::runtime::Runtime::new()?; + let result = rt.block_on(run_downloader(config, &opt)); + + if let Err(err) = &result { + error!("error running downloader: {}", err); + } + + telemetry::try_flush_and_close(); + + result +} + +fn load_config(opt: &RunOpt) -> Result { + info!("loading downloader config"); + let config = match &opt.config_path { + Some(config_path) => StaticConfig::from_file(config_path)?, + None => StaticConfig::from_env()?, + }; + + Ok(config) +} + +async fn run_downloader(config: StaticConfig, opt: &RunOpt) -> Result<()> { + telemetry::set_property(EventData::MachineId(get_machine_id().await?)); + telemetry::set_property(EventData::Version(env!("ONEFUZZ_VERSION").to_string())); + + let setup_inst = setup::Setup { config }; + setup_inst.run(&opt.onefuzz_path).await?; + + if opt.start_supervisor { + setup_inst.launch_supervisor(&opt.config_path).await?; + } + + Ok(()) +} + +fn init_telemetry(config: &StaticConfig) { + let inst_key = config.instrumentation_key; + let tele_key = config.telemetry_key; + telemetry::set_appinsights_clients(inst_key, tele_key); +} diff --git a/src/agent/onefuzz-downloader/src/setup.rs b/src/agent/onefuzz-downloader/src/setup.rs new file mode 100644 index 0000000000..148d51cdf2 --- /dev/null +++ b/src/agent/onefuzz-downloader/src/setup.rs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::config::StaticConfig; +use anyhow::Result; +use onefuzz::az_copy; +use onefuzz::process::Output; +use std::{env, path::PathBuf, process::Stdio}; +use tokio::fs; +use tokio::process::Command; +use url::Url; + +pub struct Setup { + pub config: StaticConfig, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct DownloadConfig { + pub tools: Url, +} + +impl Setup { + pub async fn run(&self, onefuzz_path: &Option) -> Result<()> { + let download_config = self.get_download_config().await?; + + let onefuzz_path = match onefuzz_path { + Some(x) => x.to_owned(), + None => onefuzz::fs::onefuzz_root()?, + }; + + let tools_dir = onefuzz_path.join("tools"); + self.add_tools_path(&tools_dir)?; + + fs::create_dir_all(&tools_dir).await?; + env::set_current_dir(&onefuzz_path)?; + + az_copy::sync(download_config.tools.to_string(), &tools_dir).await?; + let output: Output = self + .setup_command(&onefuzz_path, tools_dir) + .output() + .await? + .into(); + + if output.exit_status.success { + verbose!( + "setup script succeeded. stdout:{:?}, stderr:{:?}", + output.stdout, + output.stderr + ); + } else { + bail!( + "setup script failed. stdout:{:?}, stderr:{:?}", + output.stdout, + output.stderr + ); + } + + Ok(()) + } + + fn add_tools_path(&self, path: &PathBuf) -> Result<()> { + let path_env = env::var("PATH")?; + let os_path = match env::consts::OS { + "linux" => format!("{}:{}", path_env, path.join("linux").to_string_lossy()), + "windows" => format!("{};{}", path_env, path.join("win64").to_string_lossy()), + _ => unimplemented!("unsupported OS"), + }; + env::set_var("PATH", os_path); + Ok(()) + } + + pub async fn launch_supervisor(&self, path: &Option) -> Result<()> { + let mut cmd = Command::new("onefuzz-supervisor"); + cmd.arg("run"); + if let Some(path) = path { + cmd.arg("--config"); + cmd.arg(path); + } + + let output = cmd.status().await?; + if !output.success() { + bail!("supervisor failed: {:?}", output.code()); + } + Ok(()) + } + + #[cfg(target_os = "windows")] + fn setup_command(&self, onefuzz_path: &PathBuf, mut path: PathBuf) -> Command { + path.push("win64"); + path.push("setup-download.ps1"); + + let mut cmd = Command::new("powershell.exe"); + cmd.arg("-ExecutionPolicy"); + cmd.arg("Unrestricted"); + cmd.arg("-File"); + cmd.arg(path); + cmd.env("ONEFUZZ_ROOT", onefuzz_path); + cmd.stderr(Stdio::piped()); + cmd.stdout(Stdio::piped()); + + cmd + } + + #[cfg(target_os = "linux")] + fn setup_command(&self, onefuzz_path: &PathBuf, mut path: PathBuf) -> Command { + path.push("linux"); + path.push("setup-download.sh"); + let mut cmd = Command::new("bash"); + cmd.arg(path); + cmd.env("ONEFUZZ_ROOT", onefuzz_path); + cmd.stderr(Stdio::piped()); + cmd.stdout(Stdio::piped()); + + cmd + } + + async fn get_download_config(&self) -> Result { + let token = self.config.credentials.access_token().await?; + let machine_id = onefuzz::machine_id::get_machine_id().await?; + let mut url = self.config.download_url(); + url.query_pairs_mut() + .append_pair("machine_id", &machine_id.to_string()) + .append_pair("pool_name", &self.config.pool_name) + .append_pair("version", env!("ONEFUZZ_VERSION")); + + let response = reqwest::Client::new() + .get(url) + .bearer_auth(token.secret().expose_ref()) + .send() + .await? + .error_for_status()?; + + let to_download: DownloadConfig = response.json().await?; + Ok(to_download) + } +} diff --git a/src/agent/onefuzz-supervisor/.gitignore b/src/agent/onefuzz-supervisor/.gitignore index 96ef6c0b94..04eeebc316 100644 --- a/src/agent/onefuzz-supervisor/.gitignore +++ b/src/agent/onefuzz-supervisor/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +data/licenses.json \ No newline at end of file diff --git a/src/agent/onefuzz-supervisor/src/config.rs b/src/agent/onefuzz-supervisor/src/config.rs index 00f11ad017..fd1173462d 100644 --- a/src/agent/onefuzz-supervisor/src/config.rs +++ b/src/agent/onefuzz-supervisor/src/config.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use anyhow::Result; +use onefuzz::auth::{ClientCredentials, Credentials, ManagedIdentityCredentials}; use onefuzz::http::ResponseExt; use reqwest::StatusCode; use std::{ @@ -12,8 +13,6 @@ use tokio::fs; use url::Url; use uuid::Uuid; -use crate::auth::{ClientCredentials, Credentials, ManagedIdentityCredentials}; - #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] pub struct StaticConfig { pub credentials: Credentials, @@ -74,8 +73,52 @@ impl StaticConfig { Ok(config) } - pub async fn load(config_path: impl AsRef) -> Result { - let data = tokio::fs::read(config_path).await?; + pub fn from_env() -> Result { + let client_id = Uuid::parse_str(&std::env::var("ONEFUZZ_CLIENT_ID")?)?; + let client_secret = std::env::var("ONEFUZZ_CLIENT_SECRET")?.into(); + let tenant = std::env::var("ONEFUZZ_TENANT")?; + let onefuzz_url = Url::parse(&std::env::var("ONEFUZZ_URL")?)?; + let pool_name = std::env::var("ONEFUZZ_POOL")?; + + let heartbeat_queue = if let Ok(key) = std::env::var("ONEFUZZ_HEARTBEAT") { + Some(Url::parse(&key)?) + } else { + None + }; + + let instrumentation_key = if let Ok(key) = std::env::var("ONEFUZZ_INSTRUMENTATION_KEY") { + Some(Uuid::parse_str(&key)?) + } else { + None + }; + + let telemetry_key = if let Ok(key) = std::env::var("ONEFUZZ_TELEMETRY_KEY") { + Some(Uuid::parse_str(&key)?) + } else { + None + }; + + let credentials = ClientCredentials::new( + client_id, + client_secret, + onefuzz_url.clone().to_string(), + tenant, + ) + .into(); + + Ok(Self { + credentials, + pool_name, + onefuzz_url, + instrumentation_key, + telemetry_key, + heartbeat_queue, + }) + } + + pub fn from_file(config_path: impl AsRef) -> Result { + verbose!("loading config from: {}", config_path.as_ref().display()); + let data = std::fs::read(config_path)?; Self::new(&data) } diff --git a/src/agent/onefuzz-supervisor/src/coordinator.rs b/src/agent/onefuzz-supervisor/src/coordinator.rs index be35a13e74..790055cac4 100644 --- a/src/agent/onefuzz-supervisor/src/coordinator.rs +++ b/src/agent/onefuzz-supervisor/src/coordinator.rs @@ -3,12 +3,11 @@ use anyhow::Result; use downcast_rs::Downcast; -use onefuzz::{http::ResponseExt, process::Output}; +use onefuzz::{auth::AccessToken, http::ResponseExt, process::Output}; use reqwest::{Client, Request, Response, StatusCode}; use serde::Serialize; use uuid::Uuid; -use crate::auth::AccessToken; use crate::config::Registration; use crate::work::{TaskId, WorkSet}; use crate::worker::WorkerEvent; diff --git a/src/agent/onefuzz-supervisor/src/main.rs b/src/agent/onefuzz-supervisor/src/main.rs index 91949c91b3..09ec9992c9 100644 --- a/src/agent/onefuzz-supervisor/src/main.rs +++ b/src/agent/onefuzz-supervisor/src/main.rs @@ -23,7 +23,6 @@ use onefuzz::{ use structopt::StructOpt; pub mod agent; -pub mod auth; pub mod config; pub mod coordinator; pub mod debug; @@ -42,12 +41,13 @@ enum Opt { Run(RunOpt), Debug(debug::DebugOpt), Licenses, + Version, } #[derive(StructOpt, Debug)] struct RunOpt { #[structopt(short, long = "--config", parse(from_os_str))] - config_path: PathBuf, + config_path: Option, } fn main() -> Result<()> { @@ -59,11 +59,22 @@ fn main() -> Result<()> { Opt::Run(opt) => run(opt)?, Opt::Debug(opt) => debug::debug(opt)?, Opt::Licenses => licenses()?, + Opt::Version => versions()?, }; Ok(()) } +fn versions() -> Result<()> { + println!( + "{} onefuzz:{} git:{}", + crate_version!(), + env!("ONEFUZZ_VERSION"), + env!("GIT_VERSION") + ); + Ok(()) +} + fn licenses() -> Result<()> { use std::io::{self, Write}; io::stdout().write_all(include_bytes!("../../data/licenses.json"))?; @@ -71,13 +82,6 @@ fn licenses() -> Result<()> { } fn run(opt: RunOpt) -> Result<()> { - info!( - "{} onefuzz:{} git:{}", - crate_version!(), - env!("ONEFUZZ_VERSION"), - env!("GIT_VERSION") - ); - if done::is_agent_done()? { verbose!( "agent is done, remove lock ({}) to continue", @@ -110,9 +114,10 @@ fn run(opt: RunOpt) -> Result<()> { fn load_config(opt: RunOpt) -> Result { info!("loading supervisor agent config"); - let data = std::fs::read(&opt.config_path)?; - let config = StaticConfig::new(&data)?; - verbose!("loaded static config from: {}", opt.config_path.display()); + let config = match &opt.config_path { + Some(config_path) => StaticConfig::from_file(config_path)?, + None => StaticConfig::from_env()?, + }; init_telemetry(&config); @@ -122,13 +127,20 @@ fn load_config(opt: RunOpt) -> Result { async fn run_agent(config: StaticConfig) -> Result<()> { telemetry::set_property(EventData::MachineId(get_machine_id().await?)); telemetry::set_property(EventData::Version(env!("ONEFUZZ_VERSION").to_string())); - if let Ok(scaleset) = get_scaleset_name().await { - telemetry::set_property(EventData::ScalesetId(scaleset)); + let scaleset = get_scaleset_name().await; + if let Ok(scaleset) = &scaleset { + telemetry::set_property(EventData::ScalesetId(scaleset.clone())); } let registration = match config::Registration::load_existing(config.clone()).await { Ok(registration) => registration, - Err(_) => config::Registration::create_managed(config.clone()).await?, + Err(_) => { + if scaleset.is_ok() { + config::Registration::create_managed(config.clone()).await? + } else { + config::Registration::create_unmanaged(config.clone()).await? + } + } }; verbose!("current registration: {:?}", registration); diff --git a/src/agent/onefuzz-supervisor/src/work.rs b/src/agent/onefuzz-supervisor/src/work.rs index da5aaf8801..b384af08c6 100644 --- a/src/agent/onefuzz-supervisor/src/work.rs +++ b/src/agent/onefuzz-supervisor/src/work.rs @@ -5,11 +5,10 @@ use std::path::PathBuf; use anyhow::Result; use downcast_rs::Downcast; -use onefuzz::blob::BlobContainerUrl; +use onefuzz::{auth::Secret, blob::BlobContainerUrl}; use storage_queue::QueueClient; use uuid::Uuid; -use crate::auth::Secret; use crate::config::Registration; pub type JobId = Uuid; diff --git a/src/agent/onefuzz-supervisor/src/auth.rs b/src/agent/onefuzz/src/auth.rs similarity index 87% rename from src/agent/onefuzz-supervisor/src/auth.rs rename to src/agent/onefuzz/src/auth.rs index 022f619959..fab450e5a9 100644 --- a/src/agent/onefuzz-supervisor/src/auth.rs +++ b/src/agent/onefuzz/src/auth.rs @@ -4,10 +4,11 @@ use std::fmt; use anyhow::Result; -use onefuzz::http::ResponseExt; use url::Url; use uuid::Uuid; +use crate::http::ResponseExt; + #[derive(Clone, Deserialize, Eq, PartialEq, Serialize)] pub struct Secret(T); @@ -105,13 +106,17 @@ impl ClientCredentials { let mut url = Url::parse("https://login.microsoftonline.com")?; url.path_segments_mut() .expect("Authority URL is cannot-be-a-base") - .push(&self.tenant) - .push("oauth2/v2.0/token"); + .extend(&[&self.tenant, "oauth2", "v2.0", "token"]); let response = reqwest::Client::new() .post(url) - .header("Content-Length", "0") - .form(&self.form_data()) + .form(&[ + ("client_id", self.client_id.to_hyphenated().to_string()), + ("client_secret", self.client_secret.expose_ref().to_string()), + ("grant_type", "client_credentials".into()), + ("tenant", self.tenant.clone()), + ("scope", format!("{}.default", self.resource)), + ]) .send() .await? .error_for_status_with_body() @@ -121,27 +126,6 @@ impl ClientCredentials { Ok(body.into()) } - - fn form_data(&self) -> FormData { - let scope = format!("{}/.default", self.resource); - - FormData { - client_id: self.client_id, - client_secret: self.client_secret.clone(), - grant_type: "client_credentials".into(), - scope, - tenant: self.tenant.clone(), - } - } -} - -#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)] -struct FormData { - client_id: Uuid, - client_secret: Secret, - grant_type: String, - scope: String, - tenant: String, } // See: https://docs.microsoft.com/en-us/azure/active-directory/develop diff --git a/src/agent/onefuzz/src/lib.rs b/src/agent/onefuzz/src/lib.rs index cb43004f1f..04933041a1 100644 --- a/src/agent/onefuzz/src/lib.rs +++ b/src/agent/onefuzz/src/lib.rs @@ -14,6 +14,7 @@ extern crate serde; pub mod telemetry; pub mod asan; +pub mod auth; pub mod az_copy; pub mod blob; pub mod expand; diff --git a/src/api-service/__app__/agent_download/__init__.py b/src/api-service/__app__/agent_download/__init__.py new file mode 100644 index 0000000000..a6b5bc3c21 --- /dev/null +++ b/src/api-service/__app__/agent_download/__init__.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import azure.functions as func +from onefuzztypes.models import Error +from onefuzztypes.requests import DownloadConfigRequest +from onefuzztypes.responses import DownloadConfig + +from ..onefuzzlib.agent_authorization import verify_token +from ..onefuzzlib.azure.containers import get_container_sas_url +from ..onefuzzlib.azure.creds import get_func_storage +from ..onefuzzlib.request import not_ok, ok, parse_uri + + +def get(req: func.HttpRequest) -> func.HttpResponse: + request = parse_uri(DownloadConfigRequest, req) + if isinstance(request, Error): + return not_ok(request, context="DownloadConfigRequest") + + tools_sas = get_container_sas_url( + "tools", read=True, list=True, account_id=get_func_storage() + ) + + return ok(DownloadConfig(tools=tools_sas)) + + +def main(req: func.HttpRequest) -> func.HttpResponse: + if req.method == "GET": + m = get + else: + raise Exception("invalid method") + + return verify_token(req, m) diff --git a/src/api-service/__app__/agent_download/function.json b/src/api-service/__app__/agent_download/function.json new file mode 100644 index 0000000000..4b87ffb43d --- /dev/null +++ b/src/api-service/__app__/agent_download/function.json @@ -0,0 +1,20 @@ +{ + "scriptFile": "__init__.py", + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get" + ], + "route": "agents/download" + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} \ No newline at end of file diff --git a/src/api-service/__app__/onefuzzlib/agent_authorization.py b/src/api-service/__app__/onefuzzlib/agent_authorization.py index 7eaf113293..6b49579bca 100644 --- a/src/api-service/__app__/onefuzzlib/agent_authorization.py +++ b/src/api-service/__app__/onefuzzlib/agent_authorization.py @@ -4,7 +4,7 @@ # Licensed under the MIT License. import logging -from typing import Callable, Union +from typing import Callable, Optional, Union from uuid import UUID import azure.functions as func @@ -14,13 +14,13 @@ from onefuzztypes.models import Error from pydantic import BaseModel -from .pools import Scaleset +from .pools import Pool, Scaleset from .request import not_ok class TokenData(BaseModel): application_id: UUID - object_id: UUID + object_id: Optional[UUID] def try_get_token_auth_header(request: func.HttpRequest) -> Union[Error, TokenData]: @@ -49,13 +49,20 @@ def try_get_token_auth_header(request: func.HttpRequest) -> Union[Error, TokenDa # This token has already been verified by the azure authentication layer token = jwt.decode(parts[1], verify=False) - return TokenData(application_id=UUID(token["appid"]), object_id=UUID(token["oid"])) + + application_id = UUID(token["appid"]) + object_id = UUID(token["oid"]) if "oid" in token else None + return TokenData(application_id=application_id, object_id=object_id) @cached(ttl=60) def is_authorized(token_data: TokenData) -> bool: - scalesets = Scaleset.get_by_object_id(token_data.object_id) - return len(scalesets) > 0 + if token_data.object_id: + scalesets = Scaleset.get_by_object_id(token_data.object_id) + return len(scalesets) > 0 + + pools = Pool.search(query={"client_id": [token_data.application_id]}) + return len(pools) > 0 def verify_token( diff --git a/src/ci/agent.sh b/src/ci/agent.sh index 642294a01d..0f14197d63 100755 --- a/src/ci/agent.sh +++ b/src/ci/agent.sh @@ -26,3 +26,4 @@ cargo test --release --manifest-path ./onefuzz/Cargo.toml cp target/release/onefuzz-agent* ../../artifacts/agent cp target/release/onefuzz-supervisor* ../../artifacts/agent +cp target/release/onefuzz-downloader* ../../artifacts/agent diff --git a/src/ci/azcopy.sh b/src/ci/azcopy.sh index 370f6f1b7b..7dafb2980a 100755 --- a/src/ci/azcopy.sh +++ b/src/ci/azcopy.sh @@ -5,12 +5,15 @@ set -ex -mkdir -p artifacts/azcopy - -wget -O azcopy.zip https://aka.ms/downloadazcopy-v10-windows -unzip azcopy.zip -mv azcopy_windows*/* artifacts/azcopy/ - -wget -O azcopy.tgz https://aka.ms/downloadazcopy-v10-linux -tar zxvf azcopy.tgz -mv azcopy_linux_amd64*/* artifacts/azcopy/ +if [ ! -f artifacts/azcopy/azcopy ]; then + mkdir -p artifacts/azcopy + + wget -O azcopy.zip https://aka.ms/downloadazcopy-v10-windows + unzip azcopy.zip + mv azcopy_windows*/* artifacts/azcopy/ + + wget -O azcopy.tgz https://aka.ms/downloadazcopy-v10-linux + tar zxvf azcopy.tgz + mv azcopy_linux_amd64*/* artifacts/azcopy/ + rm -r azcopy* +fi diff --git a/src/docker/linux/.gitignore b/src/docker/linux/.gitignore new file mode 100644 index 0000000000..5db856397e --- /dev/null +++ b/src/docker/linux/.gitignore @@ -0,0 +1,2 @@ +tools +env-config diff --git a/src/docker/linux/Dockerfile b/src/docker/linux/Dockerfile new file mode 100644 index 0000000000..dbfc8047a4 --- /dev/null +++ b/src/docker/linux/Dockerfile @@ -0,0 +1,21 @@ +FROM ubuntu:18.04 +LABEL maintainer="fuzzing@microsoft.com" +LABEL ABOUT="OneFuzz fuzzing container" + +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y gdb gdbserver llvm-10 wget unzip libssl1.0.0 libunwind-dev sudo \ + && apt-get clean + +RUN mkdir -p /onefuzz/bin /onefuzz/etc +ENV ASAN_SYMBOLIZER_PATH=/onefuzz/bin/llvm-symbolizer +RUN ln -f -s $(which llvm-symbolizer-10) $ASAN_SYMBOLIZER_PATH + +ENV RUST_BACKTRACE=full +ENV RUST_LOG=info +ENV PATH=/onefuzz/bin:/onefuzz/tools/linux:$PATH +COPY tools /onefuzz/tools + +WORKDIR /onefuzz +ENTRYPOINT ["onefuzz-downloader", "run", "--start-supervisor"] \ No newline at end of file diff --git a/src/docker/linux/build.sh b/src/docker/linux/build.sh new file mode 100755 index 0000000000..215db1c74f --- /dev/null +++ b/src/docker/linux/build.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -ex + +BUILD_DIR=$(dirname $(realpath $0)) +cd ${BUILD_DIR} + +rm -rf tools +mkdir -p tools/linux + +if [ ! -d ../../../artifacts/azcopy ]; then + (cd ../../../; ./src/ci/agent.sh) +fi +(cd ../../../; cp artifacts/azcopy/azcopy ${BUILD_DIR}/tools/linux) + +if [ -d ../../../artifacts/agent ]; then + (cd ../../../; cp artifacts/agent/onefuzz-downloader ${BUILD_DIR}/tools/linux) +else + (cd ../../agent; cargo build --release; cp target/release/onefuzz-downloader ${BUILD_DIR}/tools/linux) +fi + +docker build -t onefuzz:latest . \ No newline at end of file diff --git a/src/pytypes/onefuzztypes/requests.py b/src/pytypes/onefuzztypes/requests.py index e0fb44f0b4..f70144e34b 100644 --- a/src/pytypes/onefuzztypes/requests.py +++ b/src/pytypes/onefuzztypes/requests.py @@ -204,3 +204,9 @@ class ProxyReset(BaseRequest): class CanScheduleRequest(BaseRequest): machine_id: UUID task_id: UUID + + +class DownloadConfigRequest(BaseRequest): + machine_id: UUID + pool_name: str + version: str diff --git a/src/pytypes/onefuzztypes/responses.py b/src/pytypes/onefuzztypes/responses.py index 19acd55723..384d4902ff 100644 --- a/src/pytypes/onefuzztypes/responses.py +++ b/src/pytypes/onefuzztypes/responses.py @@ -63,3 +63,7 @@ class PendingNodeCommand(BaseResponse): class CanSchedule(BaseResponse): allowed: bool work_stopped: bool + + +class DownloadConfig(BaseResponse): + tools: str diff --git a/src/runtime-tools/linux/run.sh b/src/runtime-tools/linux/run.sh index 486136c3b0..4204788073 100755 --- a/src/runtime-tools/linux/run.sh +++ b/src/runtime-tools/linux/run.sh @@ -7,22 +7,15 @@ set -ex export PATH=$PATH:/onefuzz/bin:/onefuzz/tools/linux:/onefuzz/tools/linux/afl:/onefuzz/tools/linux/radamsa export ONEFUZZ_TOOLS=/onefuzz/tools export ONEFUZZ_ROOT=/onefuzz +export ASAN_SYMBOLIZER_PATH=/onefuzz/bin/llvm-symbolizer logger "onefuzz: starting up onefuzz" -# disable ASLR -echo 0 | sudo tee /proc/sys/kernel/randomize_va_space - # use core files, not external crash handler echo core | sudo tee /proc/sys/kernel/core_pattern echo 0 | sudo tee /proc/sys/kernel/randomize_va_space echo 1 | sudo tee /proc/sys/fs/suid_dumpable -if type apt > /dev/null 2> /dev/null; then - sudo apt update - sudo apt install -y gdb -fi - cd /onefuzz MODE=$(cat /onefuzz/etc/mode) case ${MODE} in @@ -36,24 +29,11 @@ case ${MODE} in "fuzz") logger "onefuzz: starting fuzzing" echo fuzzing - if type apt > /dev/null 2> /dev/null; then - export ASAN_SYMBOLIZER_PATH=/onefuzz/bin/llvm-symbolizer - if ! [ -f ${ASAN_SYMBOLIZER_PATH} ]; then - sudo apt install -y llvm-10 - - # If specifying symbolizer, exe name must be a "known symbolizer". - # Using `llvm-symbolizer` works for clang 8 .. 10. - sudo ln -f -s $(which llvm-symbolizer-10) $ASAN_SYMBOLIZER_PATH - fi - fi export RUST_BACKTRACE=full onefuzz-supervisor run --config /onefuzz/config.json ;; "repro") logger "onefuzz: starting repro" - if type apt > /dev/null 2> /dev/null; then - sudo apt install -y gdb gdbserver - fi export ASAN_OPTIONS=abort_on_error=1 repro.sh ;; diff --git a/src/runtime-tools/linux/setup-download.sh b/src/runtime-tools/linux/setup-download.sh new file mode 100755 index 0000000000..18c05c875b --- /dev/null +++ b/src/runtime-tools/linux/setup-download.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +set -x + +export ONEFUZZ_TOOLS=${ONEFUZZ_ROOT}/tools +export ASAN_SYMBOLIZER_PATH=${ONEFUZZ_ROOT}/bin/llvm-symbolizer + +mkdir -p ${ONEFUZZ_ROOT}/{bin,logs,tools,etc} +chmod -R a+rx ${ONEFUZZ_ROOT}/{bin,tools/linux} + +echo core | sudo tee /proc/sys/kernel/core_pattern || echo unable to set core pattern +echo 0 | sudo tee /proc/sys/kernel/randomize_va_space || echo unable to disable ASLR +echo 1 | sudo tee /proc/sys/fs/suid_dumpable || echo unable to set suid_dumpable \ No newline at end of file diff --git a/src/runtime-tools/linux/setup.sh b/src/runtime-tools/linux/setup.sh index d30fc4409c..65398ae84f 100755 --- a/src/runtime-tools/linux/setup.sh +++ b/src/runtime-tools/linux/setup.sh @@ -84,6 +84,19 @@ fi chmod -R a+rx /onefuzz/tools/linux +if type apt > /dev/null 2> /dev/null; then + sudo apt update + sudo apt install -y gdb gdbserver + + export ASAN_SYMBOLIZER_PATH=/onefuzz/bin/llvm-symbolizer + if ! [ -f ${ASAN_SYMBOLIZER_PATH} ]; then + sudo apt install -y llvm-10 + + # If specifying symbolizer, exe name must be a "known symbolizer". + # Using `llvm-symbolizer` works for clang 8 .. 10. + sudo ln -f -s $(which llvm-symbolizer-10) $ASAN_SYMBOLIZER_PATH + fi +fi if [ -d /etc/systemd/system ]; then logger "onefuzz: setting up systemd"