diff --git a/README.md b/README.md index 735a564..fafe74d 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

๐Ÿง  NeuroSploit v3.6.1

+

๐Ÿง  NeuroSploit v3.6.2

JoasASantos%2FNeuroSploit | Trendshift @@ -12,7 +12,7 @@

- + @@ -28,14 +28,15 @@ > > ๐Ÿ“– **New here? Read the [full Tutorial & User Guide โ†’](TUTORIAL.md)** โ€” every mode, flag, config and example explained. -> ๐Ÿ†• **New in v3.6.1 โ€” Cloud testing + REPL navigation + deeper recon:** -> **AWS/GCP/Azure** agents (+17 โ†’ **375** total) with credentials wired through -> `creds.yaml`; a more navigable **REPL** โ€” **`/timeout`** idle guardrail, -> **multi-target** `/target a,b,c` (sequential), an interactive **`/results`** -> browser (target โ†’ vuln โ†’ detail, Esc to go back) and **`/report`** picker; and -> **deeper recon** that downloads & analyzes JavaScript (endpoints, secrets, -> source maps) and does request/response differential analysis. Interactive -> line-editing prompt bug fixed. +> ๐Ÿ†• **New in v3.6.2 โ€” live Codex streaming + full activity feed:** +> Codex runs now **stream tool-by-tool** (`codex exec --json`) exactly like +> Claude Code โ€” every command, file edit, MCP call and token count appears live +> instead of a silent black box, so a long intense recon never looks frozen. The +> **`/logs`** feed and **`/status`** sign-of-line now capture the *actual +> commands each agent runs* (subfinder, httpx, nmap, curlโ€ฆ), not just pipeline +> phases. *(v3.6.1 added GPT-5.6 sol/terra/luna, the Codex exit-1 fix, `/logs` +> and richer `/status`; v3.6.0 added AI-agent/LLM/MCP/Skills/n8n testing + +> onboarding wizard + Cloud scope.)* > *(v3.5.4 added robust attack chaining + false-positive reduction; v3.5.3 > GitHub/GitLab/Jira **[integrations](TUTORIAL-INTEGRATION.md)**; v3.5.2 the DEPTH > doctrine + report-hygiene โ€” see [RELEASE.md](RELEASE.md).)* diff --git a/RELEASE.md b/RELEASE.md index 970fd73..635aff8 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,7 +1,7 @@ -# NeuroSploit v3.6.1 โ€” Release Notes +# NeuroSploit v3.6.2 โ€” Release Notes **Release Date:** July 2026 -**Codename:** GPT-5.6 models +**Codename:** Live Codex **License:** MIT **Credits:** Joas A Santos & Red Team Leaders @@ -9,6 +9,24 @@ ## Highlights +- **Codex now streams live, tool-by-tool.** `codex exec` is driven with `--json` + and its JSONL event stream is parsed into the same categorized activity feed + as Claude Code: every shell command it runs (`exec:`), file edit (`edit:`), + MCP tool call (`tool:`), web search (`net:`) and token count appears the moment + it happens. A long, intense recon (subfinder โ†’ httpx โ†’ katana โ†’ nmap โ€ฆ) is no + longer a silent black box โ€” you watch each tool execute. +- **`/logs` and `/status` now capture what each agent actually runs.** The + activity feed previously dropped the per-agent tool events; it now keeps the + actionable ones (commands, network, files, findings) and only filters long + model reasoning and token telemetry. `/logs` shows the real command trail; + `/status` `last:` shows a true sign-of-life. +- Failed internal commands surface as `exec: (exit N) ` instead of + silently vanishing, and Codex auth/rate errors are still detected from stderr. + +--- + +## Previously in v3.6.1 + - **Added the GPT-5.6 model line** (OpenAI / ChatGPT): `openai:gpt-5.6-sol` (frontier / default), `openai:gpt-5.6-terra` (balanced), and `openai:gpt-5.6-luna` (fast & affordable) โ€” alongside the existing GPT-5.x, diff --git a/TUTORIAL.md b/TUTORIAL.md index 9a19881..537b482 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,4 +1,4 @@ -# NeuroSploit โ€” Tutorial & User Guide (v3.6.1) +# NeuroSploit โ€” Tutorial & User Guide (v3.6.2) A complete, hands-on guide to installing, configuring and running NeuroSploit โ€” the autonomous, multi-model penetration-testing harness. @@ -98,7 +98,7 @@ Agents **degrade gracefully**: if `rustscan` is absent they use `nmap`; if neith ### Verify ```bash -neurosploit --version # neurosploit 3.6.1 +neurosploit --version # neurosploit 3.6.2 neurosploit agents # {"vulns":196,...,"chains":12,"total":417} neurosploit models # all providers & models ``` diff --git a/neurosploit-rs/Cargo.lock b/neurosploit-rs/Cargo.lock index 660360a..a5b13b5 100644 --- a/neurosploit-rs/Cargo.lock +++ b/neurosploit-rs/Cargo.lock @@ -871,7 +871,7 @@ dependencies = [ [[package]] name = "neurosploit" -version = "3.6.1" +version = "3.6.2" dependencies = [ "anyhow", "clap", @@ -888,7 +888,7 @@ dependencies = [ [[package]] name = "neurosploit-harness" -version = "3.6.1" +version = "3.6.2" dependencies = [ "anyhow", "futures", diff --git a/neurosploit-rs/Cargo.toml b/neurosploit-rs/Cargo.toml index df0ff94..f583193 100644 --- a/neurosploit-rs/Cargo.toml +++ b/neurosploit-rs/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/harness", "app"] resolver = "2" [workspace.package] -version = "3.6.1" +version = "3.6.2" edition = "2021" license = "MIT" repository = "https://github.com/JoasASantos/NeuroSploit" diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs index b362c76..40fc0bc 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.1 โ€” interactive harness + CLI (`run` / `whitebox` / `agents` / `models`). +//! NeuroSploit v3.6.2 โ€” interactive harness + CLI (`run` / `whitebox` / `agents` / `models`). mod repl; mod tui; @@ -11,8 +11,8 @@ use std::path::{Path, PathBuf}; #[command( name = "neurosploit", version, - about = "NeuroSploit v3.6.1 โ€” multi-model autonomous pentest harness", - long_about = "NeuroSploit v3.6.1 โ€” a Rust multi-model harness that drives a pool of LLMs \ + about = "NeuroSploit v3.6.2 โ€” multi-model autonomous pentest harness", + long_about = "NeuroSploit v3.6.2 โ€” a Rust multi-model harness that drives a pool of LLMs \ (API key or local subscription: Claude/Codex/Gemini/Grok) to autonomously test a target. \ After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \ them in parallel, then validates every finding by cross-model voting before reporting.\n\n\ @@ -721,7 +721,7 @@ pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode: println!(" โ”‚ ua : {ua}"); write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target)); - println!(" โ”Œโ”€ NeuroSploit v3.6.1 ยท by Joas A Santos & Red Team Leaders"); + println!(" โ”Œโ”€ NeuroSploit v3.6.2 ยท by Joas A Santos & Red Team Leaders"); println!(" โ”‚ run id : {run_id}"); println!(" โ”‚ target : {}", cfg.target); println!(" โ”‚ models : {}", cfg.models.join(", ")); diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs index 8a356b7..6787279 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.1 โ€” interactive session (Claude-Code / Codex / Cursor-CLI style). +//! NeuroSploit v3.6.2 โ€” interactive session (Claude-Code / Codex / Cursor-CLI style). //! //! Launched when `neurosploit` runs with no subcommand. A persistent REPL with //! real line editing (arrow-key history recall, Ctrl-A/E/K, paste), model @@ -52,9 +52,16 @@ impl RunLive { fn ingest(&mut self, line: &str) { let low = line.to_lowercase(); self.lines += 1; - // Keep a compact activity trail for /logs and the /status sign-of-life - // (skip the machine-only finding_json/ai/tokens noise). - if !low.starts_with("finding_json:") && !low.starts_with("@") && !low.contains("ai:") && !low.starts_with("tokens:") { + // Keep a compact activity trail for /logs and the /status sign-of-life. + // Streamed agent events are tagged "@label ": keep the actionable + // ones (commands, net, tools, file edits, phases) so the operator sees + // exactly what each agent is running โ€” drop only long model reasoning + // (ai:), token telemetry (tokens:), and machine JSON (finding_json:). + let payload = line.strip_prefix('@') + .and_then(|r| r.split_once(' ').map(|(_, rest)| rest)) + .unwrap_or(line); + let plow = payload.to_lowercase(); + if !low.starts_with("finding_json:") && !plow.starts_with("ai:") && !plow.starts_with("tokens:") { let clean: String = line.chars().take(160).collect(); self.last = clean.clone(); self.feed.push(clean); @@ -346,7 +353,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> { let backends = harness::installed_cli_backends(); println!("\x1b[1m"); println!(" โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—"); - println!(" โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•— NeuroSploit v3.6.1"); + println!(" โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•— NeuroSploit v3.6.2"); println!(" โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ interactive harness"); println!(" โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ by Joas A Santos"); println!(" โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• & Red Team Leaders"); diff --git a/neurosploit-rs/app/src/tui.rs b/neurosploit-rs/app/src/tui.rs index 2673e65..2086288 100644 --- a/neurosploit-rs/app/src/tui.rs +++ b/neurosploit-rs/app/src/tui.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.1 โ€” TUI "Mission Control" mode. +//! NeuroSploit v3.6.2 โ€” TUI "Mission Control" mode. //! //! Concurrent panels that update live while the engagement runs in the //! background, with a composer input that stays active during execution: diff --git a/neurosploit-rs/crates/harness/src/belief.rs b/neurosploit-rs/crates/harness/src/belief.rs index 21ff853..38a8c06 100644 --- a/neurosploit-rs/crates/harness/src/belief.rs +++ b/neurosploit-rs/crates/harness/src/belief.rs @@ -1,4 +1,4 @@ -//! POMDP belief-state world model (v3.6.1). +//! POMDP belief-state world model (v3.6.2). //! //! The target is only partially observable, so we don't track booleans โ€” we //! track a **belief**: a property graph whose nodes (host / service / vuln / diff --git a/neurosploit-rs/crates/harness/src/grounding.rs b/neurosploit-rs/crates/harness/src/grounding.rs index fd2d891..c1febba 100644 --- a/neurosploit-rs/crates/harness/src/grounding.rs +++ b/neurosploit-rs/crates/harness/src/grounding.rs @@ -1,4 +1,4 @@ -//! Verification / grounding engine (v3.6.1). +//! Verification / grounding engine (v3.6.2). //! //! Hard rule: **no claim enters the world model without a tool receipt** โ€” raw //! tool output, not the LLM's paraphrase. This is the empirical anti-hallucination diff --git a/neurosploit-rs/crates/harness/src/lib.rs b/neurosploit-rs/crates/harness/src/lib.rs index fc00b14..e64fbe1 100644 --- a/neurosploit-rs/crates/harness/src/lib.rs +++ b/neurosploit-rs/crates/harness/src/lib.rs @@ -1,4 +1,4 @@ -//! NeuroSploit v3.6.1 harness โ€” a robust multi-model runtime for the +//! NeuroSploit v3.6.2 harness โ€” a robust multi-model runtime for the //! markdown-driven autonomous pentest engine. //! //! The harness loads the `agents_md/` library, drives a *pool* of LLM models diff --git a/neurosploit-rs/crates/harness/src/models.rs b/neurosploit-rs/crates/harness/src/models.rs index 87e5da7..36df2d9 100644 --- a/neurosploit-rs/crates/harness/src/models.rs +++ b/neurosploit-rs/crates/harness/src/models.rs @@ -194,6 +194,12 @@ impl ChatClient { if bin == "claude" { return self.chat_claude_stream(label, model, &prompt, mcp_config, progress).await; } + // Codex exec streams JSONL events (`--json`): commands it runs, agent + // messages, file changes, token usage. Surface them live so recon and + // exploitation are visible tool-by-tool instead of a silent black box. + if bin == "codex" { + return self.chat_codex_stream(label, model, &prompt, mcp_config, progress).await; + } let mut cmd = Command::new(bin); match bin { @@ -366,6 +372,136 @@ impl ChatClient { } Ok(result) } + + /// Drive `codex exec --json` and surface its JSONL event stream as a live, + /// categorized activity feed (commands, agent messages, file changes, MCP + /// tool calls, token usage). The final agent message is returned as the + /// result. Mirrors `chat_claude_stream` so Codex runs are just as visible. + async fn chat_codex_stream( + &self, + label: &str, + model: &str, + prompt: &str, + mcp_config: Option<&str>, + progress: Option>, + ) -> Result { + let mut cmd = Command::new("codex"); + cmd.arg("exec").arg("--json").arg("--model").arg(model) + .arg("--dangerously-bypass-approvals-and-sandbox"); + if let Some(mcp) = mcp_config { + for (name, cmdline, args) in mcp_servers_from(mcp) { + cmd.arg("-c").arg(format!("mcp_servers.{name}.command={cmdline}")); + cmd.arg("-c").arg(format!("mcp_servers.{name}.args={args}")); + } + } + cmd.arg("-"); + cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true); + let mut child = cmd.spawn().map_err(|e| anyhow!("spawn codex failed: {e}"))?; + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(prompt.as_bytes()).await?; + // Drop closes stdin so Codex processes the prompt and exits. + } + let stdout = child.stdout.take().ok_or_else(|| anyhow!("no stdout"))?; + let stderr = child.stderr.take().ok_or_else(|| anyhow!("no stderr"))?; + let mut lines = BufReader::new(stdout).lines(); + let lbl = if label.is_empty() { String::new() } else { format!("@{label} ") }; + let emit = |s: String| { + if let Some(tx) = &progress { + let _ = tx.try_send(format!("{lbl}{s}")); + } + }; + + // Last agent_message is the model's final answer; keep every one so a + // run that ends on a tool call still returns the most recent reasoning. + let mut result = String::new(); + let read = async { + while let Ok(Some(line)) = lines.next_line().await { + let Ok(v) = serde_json::from_str::(&line) else { continue }; + let ty = v.get("type").and_then(|t| t.as_str()).unwrap_or(""); + match ty { + "item.started" | "item.completed" => { + let Some(item) = v.get("item") else { continue }; + let itype = item.get("type").and_then(|t| t.as_str()).unwrap_or(""); + match itype { + "command_execution" => { + // Only announce on start (avoid double lines); note failures on completion. + let c = item.get("command").and_then(|x| x.as_str()).unwrap_or(""); + // Strip the `/bin/sh -lc '...'` wrapper Codex adds. + let c = c.strip_prefix("/bin/sh -lc ").map(|s| s.trim_matches('\'')).unwrap_or(c); + if ty == "item.started" { + let danger = c.contains("rm -rf") || c.contains("mkfs") + || c.contains(":(){") || c.contains("dd if=") || c.contains("> /dev/"); + emit(format!("{}: {}", if danger { "danger" } else { "exec" }, truncate(c, 200))); + } else if let Some(code) = item.get("exit_code").and_then(|x| x.as_i64()) { + if code != 0 { + emit(format!("exec: (exit {code}) {}", truncate(c, 120))); + } + } + } + "agent_message" => { + if let Some(t) = item.get("text").and_then(|x| x.as_str()) { + let t = t.trim(); + if !t.is_empty() { + if ty == "item.completed" { result = t.to_string(); } + emit(format!("ai: {}", truncate(t, 240))); + } + } + } + "file_change" | "patch" => { + let p = item.get("path").and_then(|x| x.as_str()) + .or_else(|| item.get("file").and_then(|x| x.as_str())).unwrap_or("file"); + emit(format!("edit: {p}")); + } + "mcp_tool_call" => { + let name = item.get("tool").and_then(|x| x.as_str()) + .or_else(|| item.get("name").and_then(|x| x.as_str())).unwrap_or("mcp"); + emit(format!("tool: {name}")); + } + "web_search" => { + let q = item.get("query").and_then(|x| x.as_str()).unwrap_or(""); + emit(format!("net: search {}", truncate(q, 100))); + } + _ => {} + } + } + "turn.completed" => { + let ti = v.pointer("/usage/input_tokens").and_then(|x| x.as_u64()); + let to = v.pointer("/usage/output_tokens").and_then(|x| x.as_u64()); + if ti.is_some() || to.is_some() { + emit(format!("tokens: in={} out={}", ti.unwrap_or(0), to.unwrap_or(0))); + } + } + _ => {} + } + } + }; + // Bound the whole streamed turn (matches the buffered path's cap). + if tokio::time::timeout(Duration::from_secs(900), read).await.is_err() { + return Err(anyhow!("codex stream timed out after 900s")); + } + let status = child.wait().await.ok(); + // Drain stderr for auth/rate diagnostics if we got nothing usable. + if result.is_empty() { + let mut errbuf = String::new(); + let mut el = BufReader::new(stderr).lines(); + while let Ok(Some(l)) = el.next_line().await { + if !errbuf.is_empty() { errbuf.push('\n'); } + errbuf.push_str(&l); + if errbuf.len() > 2000 { break; } + } + let low = errbuf.to_lowercase(); + let hard = ["not logged in", "please log in", "please login", "run /login", + "unauthorized", "not authenticated", "invalid api key", "no api key", + "rate limit", "429", "quota", "credit balance", "usage limit"] + .iter().any(|k| low.contains(k)); + let code = status.and_then(|s| s.code()).map(|c| c.to_string()).unwrap_or_else(|| "signal".into()); + if hard || !errbuf.trim().is_empty() { + return Err(anyhow!("codex subscription CLI exit {}: {}", code, truncate(errbuf.trim(), 240))); + } + return Err(anyhow!("codex stream produced no result")); + } + Ok(result) + } } /// Categorise a Claude tool_use block into a tagged activity-feed event. diff --git a/neurosploit-rs/crates/harness/src/pomdp.rs b/neurosploit-rs/crates/harness/src/pomdp.rs index e4891ec..a69f2e4 100644 --- a/neurosploit-rs/crates/harness/src/pomdp.rs +++ b/neurosploit-rs/crates/harness/src/pomdp.rs @@ -1,4 +1,4 @@ -//! POMDP decision layer (v3.6.1): value-of-information planning + the +//! POMDP decision layer (v3.6.2): value-of-information planning + the //! anti-hallucination gate. //! //! The choice "scan more vs exploit now" is **not** a heuristic here โ€” it falls diff --git a/neurosploit-rs/crates/harness/src/probe.rs b/neurosploit-rs/crates/harness/src/probe.rs index c9f6698..2a2d119 100644 --- a/neurosploit-rs/crates/harness/src/probe.rs +++ b/neurosploit-rs/crates/harness/src/probe.rs @@ -1,4 +1,4 @@ -//! Deterministic HTTP request/response analysis (v3.6.1). +//! Deterministic HTTP request/response analysis (v3.6.2). //! //! Before the LLM recon runs, the harness performs a **real** probe of the //! target and captures observed facts โ€” status, headers, security headers, diff --git a/neurosploit-rs/crates/harness/src/report.rs b/neurosploit-rs/crates/harness/src/report.rs index 4e33c3e..6186a49 100644 --- a/neurosploit-rs/crates/harness/src/report.rs +++ b/neurosploit-rs/crates/harness/src/report.rs @@ -97,9 +97,9 @@ pub fn html(target: &str, findings: &[Finding]) -> String { h4{{margin:12px 0 3px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#8b5cf6}}\ .b{{color:#8b5cf6;font-weight:800}}\

NeuroSploit Penetration Test Report

\ -
Target: {t} ยท v3.6.1 Rust harness ยท multi-model validated
\ +
Target: {t} ยท v3.6.2 Rust harness ยท multi-model validated
\
{chips}
{graph_block}

Findings ({n})

{body}\ -

Authorized testing only. Findings confirmed by multi-model adversarial voting.
NeuroSploit v3.6.1 ยท by Joas A Santos & Red Team Leaders

", +

Authorized testing only. Findings confirmed by multi-model adversarial voting.
NeuroSploit v3.6.2 ยท by Joas A Santos & Red Team Leaders

", t = esc(target), chips = chips, n = sorted.len(), body = body, graph_block = graph_block, ) } @@ -135,7 +135,7 @@ pub fn typst_report(target: &str, findings: &[Finding], dir: &Path) -> std::io:: let mut data = String::new(); data.push_str(&format!( "#let meta = (target: {}, run_id: {}, generated: {}, model: {})\n", - tq(target), tq(&run_id), tq("NeuroSploit v3.6.1"), tq("multi-model") + tq(target), tq(&run_id), tq("NeuroSploit v3.6.2"), tq("multi-model") )); data.push_str("#let findings = (\n"); for f in sorted_findings(findings) {