Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a3596aab6 | ||
|
|
1f68fc9825 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -226,7 +226,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ectf-tools"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ectf-tools"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
123
README.md
123
README.md
@@ -38,6 +38,129 @@ ectf-tools tools /dev/tty.usbmodemXXX listen
|
||||
|
||||
# Receive a file from another HSM
|
||||
ectf-tools tools /dev/tty.usbmodemXXX receive 1a2b3c 0 1
|
||||
|
||||
# Run the hardware test suite
|
||||
ectf-tools tools /dev/tty.usbmodemXXX test 1a2b3c 0x4321 /dev/tty.usbmodemYYY
|
||||
|
||||
# Run tests without a second HSM
|
||||
ectf-tools tools /dev/tty.usbmodemXXX test 1a2b3c 0x4321 --no-transfer
|
||||
|
||||
# Output test results as JSON (for CI)
|
||||
ectf-tools tools /dev/tty.usbmodemXXX test 1a2b3c 0x4321 --no-transfer --json
|
||||
```
|
||||
|
||||
### API Commands
|
||||
|
||||
```bash
|
||||
# Configure your API credentials
|
||||
ectf-tools config --token <TOKEN> --git-url <GIT_URL>
|
||||
|
||||
# Submit a design for testing
|
||||
ectf-tools api test submit <COMMIT_HASH>
|
||||
|
||||
# List recent test flows
|
||||
ectf-tools api test ls
|
||||
|
||||
# Get details on a specific flow
|
||||
ectf-tools api test info <FLOW_ID>
|
||||
|
||||
# Download job output
|
||||
ectf-tools api test get <JOB_ID> output.tar.gz
|
||||
|
||||
# Submit to handoff
|
||||
ectf-tools api submit <COMMIT_HASH>
|
||||
|
||||
# List and download attack packages
|
||||
ectf-tools api list
|
||||
ectf-tools api get <PACKAGE_NAME>
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
For CI environments, you can configure the tool entirely via environment variables instead of the config file:
|
||||
|
||||
| Variable | Description | Required |
|
||||
|---|---|---|
|
||||
| `ECTF_TOKEN` | API bearer token | Yes (with `ECTF_GIT_URL`) |
|
||||
| `ECTF_GIT_URL` | Git repository URL | Yes (with `ECTF_TOKEN`) |
|
||||
| `ECTF_API_URL` | API base URL (default: `https://api.ectf.mitre.org`) | No |
|
||||
|
||||
If both `ECTF_TOKEN` and `ECTF_GIT_URL` are set, no config file is needed. If a config file exists, env vars override individual fields.
|
||||
|
||||
### JSON Output (`--json`)
|
||||
|
||||
Most commands support `--json` for machine-readable output. The process exit code is still non-zero on failure.
|
||||
|
||||
#### `tools test --json`
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 15,
|
||||
"passed": 14,
|
||||
"failed": 1,
|
||||
"tests": [
|
||||
{
|
||||
"name": "list_empty",
|
||||
"passed": true,
|
||||
"duration_secs": 0.123
|
||||
},
|
||||
{
|
||||
"name": "bad_pin",
|
||||
"passed": false,
|
||||
"duration_secs": 1.234,
|
||||
"error": "Expected bad pin to fail, but it succeeded"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `api test submit --json` / `api clone submit --json` / `api submit --json`
|
||||
|
||||
```json
|
||||
{
|
||||
"flow": "test",
|
||||
"id": "abc12345-1234-1234-1234-123456789abc"
|
||||
}
|
||||
```
|
||||
|
||||
#### `api test ls --json` / `api clone ls --json` / `api remote ls --json`
|
||||
|
||||
```json
|
||||
{
|
||||
"flows": [
|
||||
{
|
||||
"id": "abc12345-1234-1234-1234-123456789abc",
|
||||
"submitted": "2026-02-11T22:05:47.000000Z",
|
||||
"status": "succeeded",
|
||||
"completed": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `api test info --json` / `api clone info --json` / `api remote info --json`
|
||||
|
||||
```json
|
||||
{
|
||||
"flow": "test",
|
||||
"id": "abc12345-1234-1234-1234-123456789abc",
|
||||
"submitted": "2026-02-11T22:05:47.000000Z",
|
||||
"status": "succeeded",
|
||||
"completed": true,
|
||||
"params": {
|
||||
"git_url": "https://github.com/example/repo.git",
|
||||
"commit_hash": "abc1234"
|
||||
},
|
||||
"jobs": [
|
||||
{
|
||||
"name": "build",
|
||||
"id": "def12345-1234-1234-1234-123456789abc",
|
||||
"status": "succeeded",
|
||||
"has_artifacts": true,
|
||||
"private": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Hardware Bootloader Tools (MSPM0L2228)
|
||||
|
||||
62
src/api.rs
62
src/api.rs
@@ -300,9 +300,26 @@ fn flow_overall_status(f: &Flow) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cmd_flow_list(flow: &str, count: usize) -> Result<()> {
|
||||
pub fn cmd_flow_list(flow: &str, count: usize, json: bool) -> Result<()> {
|
||||
let api = ApiClient::new()?;
|
||||
let flows = api.flow_list(flow, count)?;
|
||||
|
||||
if json {
|
||||
let items: Vec<serde_json::Value> = flows
|
||||
.iter()
|
||||
.map(|f| {
|
||||
serde_json::json!({
|
||||
"id": f.id,
|
||||
"submitted": f.submit_time,
|
||||
"status": flow_overall_status(f).to_lowercase(),
|
||||
"completed": f.completed,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
println!("{}", serde_json::to_string(&serde_json::json!({ "flows": items })).unwrap());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if flows.is_empty() {
|
||||
log::info("No flows found");
|
||||
return Ok(());
|
||||
@@ -348,9 +365,37 @@ pub fn cmd_flow_list(flow: &str, count: usize) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cmd_flow_info(flow: &str, id: &str) -> Result<()> {
|
||||
pub fn cmd_flow_info(flow: &str, id: &str, json: bool) -> Result<()> {
|
||||
let api = ApiClient::new()?;
|
||||
let f = api.flow_info(flow, id)?;
|
||||
|
||||
if json {
|
||||
let jobs: Vec<serde_json::Value> = f
|
||||
.jobs
|
||||
.iter()
|
||||
.map(|j| {
|
||||
serde_json::json!({
|
||||
"name": j.name,
|
||||
"id": j.id,
|
||||
"status": j.status,
|
||||
"has_artifacts": j.has_artifacts,
|
||||
"private": j.private,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let output = serde_json::json!({
|
||||
"flow": flow,
|
||||
"id": f.id,
|
||||
"submitted": f.submit_time,
|
||||
"status": flow_overall_status(&f).to_lowercase(),
|
||||
"completed": f.completed,
|
||||
"params": f.params,
|
||||
"jobs": jobs,
|
||||
});
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let status = flow_overall_status(&f);
|
||||
let sc = status_color(&status.to_lowercase());
|
||||
let when = relative_time(&f.submit_time);
|
||||
@@ -401,7 +446,7 @@ pub fn cmd_flow_info(flow: &str, id: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cmd_flow_submit(flow: &str, commit: &str, url: Option<&str>) -> Result<()> {
|
||||
pub fn cmd_flow_submit(flow: &str, commit: &str, url: Option<&str>, json: bool) -> Result<()> {
|
||||
let api = ApiClient::new()?;
|
||||
let git_url = url
|
||||
.map(|s| s.to_string())
|
||||
@@ -411,7 +456,12 @@ pub fn cmd_flow_submit(flow: &str, commit: &str, url: Option<&str>) -> Result<()
|
||||
"commit_hash": commit,
|
||||
});
|
||||
let id = api.flow_submit(flow, &body)?;
|
||||
log::success(&format!("Submitted {flow} flow: {id}"));
|
||||
if json {
|
||||
let output = serde_json::json!({ "flow": flow, "id": id.trim() });
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
} else {
|
||||
log::success(&format!("Submitted {flow} flow: {id}"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -430,8 +480,8 @@ pub fn cmd_flow_get(flow: &str, job_id: &str, out: &PathBuf) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cmd_submit(commit: &str) -> Result<()> {
|
||||
cmd_flow_submit("submit", commit, None)
|
||||
pub fn cmd_submit(commit: &str, json: bool) -> Result<()> {
|
||||
cmd_flow_submit("submit", commit, None, json)
|
||||
}
|
||||
|
||||
pub fn cmd_photo(file: &PathBuf) -> Result<()> {
|
||||
|
||||
32
src/main.rs
32
src/main.rs
@@ -106,6 +106,9 @@ enum ApiCmd {
|
||||
Submit {
|
||||
/// Git commit hash
|
||||
commit: String,
|
||||
/// Output result as JSON for CI
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Submit a PNG for the Team Photo flag
|
||||
Photo {
|
||||
@@ -164,11 +167,17 @@ enum FlowCmd {
|
||||
/// Number of flows to show (0 for all)
|
||||
#[arg(short, long, default_value = "5")]
|
||||
number: usize,
|
||||
/// Output result as JSON for CI
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Get flow details
|
||||
Info {
|
||||
/// Flow ID
|
||||
id: String,
|
||||
/// Output result as JSON for CI
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Submit a commit
|
||||
Submit {
|
||||
@@ -177,6 +186,9 @@ enum FlowCmd {
|
||||
/// Override git URL
|
||||
#[arg(short, long)]
|
||||
url: Option<String>,
|
||||
/// Output result as JSON for CI
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Cancel a flow
|
||||
Cancel {
|
||||
@@ -211,11 +223,17 @@ enum RemoteCmd {
|
||||
/// Number of flows to show (0 for all)
|
||||
#[arg(short, long, default_value = "5")]
|
||||
number: usize,
|
||||
/// Output result as JSON for CI
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Get remote flow details
|
||||
Info {
|
||||
/// Flow ID
|
||||
id: String,
|
||||
/// Output result as JSON for CI
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Cancel a remote flow
|
||||
Cancel {
|
||||
@@ -572,7 +590,7 @@ fn run_config(
|
||||
|
||||
fn run_api(cmd: ApiCmd) -> Result<()> {
|
||||
match cmd {
|
||||
ApiCmd::Submit { commit } => api::cmd_submit(&commit),
|
||||
ApiCmd::Submit { commit, json } => api::cmd_submit(&commit, json),
|
||||
ApiCmd::Photo { file } => api::cmd_photo(&file),
|
||||
ApiCmd::Design { file } => api::cmd_design(&file),
|
||||
ApiCmd::Steal { team, digest } => api::cmd_steal(&team, &digest),
|
||||
@@ -590,10 +608,10 @@ fn run_api(cmd: ApiCmd) -> Result<()> {
|
||||
|
||||
fn run_flow(flow: &str, cmd: FlowCmd) -> Result<()> {
|
||||
match cmd {
|
||||
FlowCmd::Ls { number } => api::cmd_flow_list(flow, number),
|
||||
FlowCmd::Info { id } => api::cmd_flow_info(flow, &id),
|
||||
FlowCmd::Submit { commit, url } => {
|
||||
api::cmd_flow_submit(flow, &commit, url.as_deref())
|
||||
FlowCmd::Ls { number, json } => api::cmd_flow_list(flow, number, json),
|
||||
FlowCmd::Info { id, json } => api::cmd_flow_info(flow, &id, json),
|
||||
FlowCmd::Submit { commit, url, json } => {
|
||||
api::cmd_flow_submit(flow, &commit, url.as_deref(), json)
|
||||
}
|
||||
FlowCmd::Cancel { id } => api::cmd_flow_cancel(flow, &id),
|
||||
FlowCmd::Get { job_id, out } => api::cmd_flow_get(flow, &job_id, &out),
|
||||
@@ -608,8 +626,8 @@ fn run_remote(cmd: RemoteCmd) -> Result<()> {
|
||||
team,
|
||||
timeout,
|
||||
} => api::cmd_remote_connect(&management_port, &transfer_port, &team, timeout),
|
||||
RemoteCmd::Ls { number } => api::cmd_flow_list("remote", number),
|
||||
RemoteCmd::Info { id } => api::cmd_flow_info("remote", &id),
|
||||
RemoteCmd::Ls { number, json } => api::cmd_flow_list("remote", number, json),
|
||||
RemoteCmd::Info { id, json } => api::cmd_flow_info("remote", &id, json),
|
||||
RemoteCmd::Cancel { id } => api::cmd_flow_cancel("remote", &id),
|
||||
RemoteCmd::Get { job_id, out } => api::cmd_flow_get("remote", &job_id, &out),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user