From 667021b275752572eb97d376bda175e34ec22267 Mon Sep 17 00:00:00 2001 From: mblanke Date: Tue, 2 Dec 2025 16:29:41 -0500 Subject: [PATCH 01/14] Add files via upload --- files.zip | Bin 0 -> 28908 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 files.zip diff --git a/files.zip b/files.zip new file mode 100644 index 0000000000000000000000000000000000000000..d796398a021cdbd2395a7a768ecef4b41c75f7ff GIT binary patch literal 28908 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 literal 0 HcmV?d00001 From 08d604ba38bc88f66c5ba9110ec3b0ccc9bc09e0 Mon Sep 17 00:00:00 2001 From: mblanke Date: Tue, 2 Dec 2025 16:52:39 -0500 Subject: [PATCH 02/14] Update unpack-zip workflow to create PR on changes --- .github/workflows/unpack-zip.yml | 114 ++++++++++++++++++++++++------- 1 file changed, 88 insertions(+), 26 deletions(-) diff --git a/.github/workflows/unpack-zip.yml b/.github/workflows/unpack-zip.yml index 85166ae..2a876c8 100644 --- a/.github/workflows/unpack-zip.yml +++ b/.github/workflows/unpack-zip.yml @@ -1,4 +1,4 @@ -name: Unpack files.zip +name: Unpack files.zip (create branch + PR) on: workflow_dispatch: @@ -6,52 +6,114 @@ on: branch: description: 'Branch containing files.zip' required: true - default: 'c2-integration' + default: 'C2-integration' + +permissions: + contents: write + pull-requests: write jobs: - unpack: + unpack-and-pr: runs-on: ubuntu-latest + steps: - - name: Checkout branch + # --------------------------------------------------------- + # 0. Checkout the target branch ONLY — prevents recursion + # --------------------------------------------------------- + - name: Checkout target branch uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch }} + fetch-depth: 0 persist-credentials: true - - name: Ensure unzip available - run: sudo apt-get update -y && sudo apt-get install -y unzip rsync - - - name: Verify files.zip exists + - name: Install tools run: | - if [ ! -f files.zip ]; then - echo "ERROR: files.zip not found in repo root on branch ${{ github.event.inputs.branch }}" + sudo apt-get update -y + sudo apt-get install -y unzip rsync jq + + # --------------------------------------------------------- + # 1. Verify files.zip exists in branch root + # --------------------------------------------------------- + - name: Check for files.zip + run: | + if [ ! -f "files.zip" ]; then + echo "::error ::files.zip not found in root of branch ${{ github.event.inputs.branch }}" exit 1 fi - echo "files.zip found:" && ls -lh files.zip + echo "Found files.zip:" + ls -lh files.zip - - name: Unpack files.zip + # --------------------------------------------------------- + # 2. Unzip files into extracted/ + # --------------------------------------------------------- + - name: Extract zip run: | rm -rf extracted - mkdir -p extracted + mkdir extracted unzip -o files.zip -d extracted - echo "Sample extracted files:" - find extracted -maxdepth 3 -type f | sed -n '1,200p' + echo "Extracted files sample:" + find extracted -type f | sed -n '1,50p' - - name: Copy extracted files into repository + # --------------------------------------------------------- + # 3. Copy extracted files into root of repo + # --------------------------------------------------------- + - name: Copy extracted contents run: | - rsync -a --exclude='.git' extracted/ . + rsync -a extracted/ . --exclude='.git' - - name: Commit and push changes (if any) - env: - BRANCH: ${{ github.event.inputs.branch }} + # --------------------------------------------------------- + # 4. Detect changes and create commit branch + # --------------------------------------------------------- + - name: Commit changes if any + id: gitops run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - if git diff --cached --quiet; then - echo "No changes to commit." + + if git status --porcelain | grep -q . ; then + BRANCH="unpacked-${{ github.event.inputs.branch }}-$(date +%s)" + git checkout -b "$BRANCH" + git add -A + git commit -m "Unpacked files.zip automatically" + echo "branch=$BRANCH" >> $GITHUB_OUTPUT else - git commit -m "Unpack files.zip into branch ${BRANCH} via workflow" - git push origin "HEAD:${BRANCH}" - echo "Changes pushed." + echo "nochanges=true" >> $GITHUB_OUTPUT + fi + + # --------------------------------------------------------- + # 5. Push branch only if changes exist + # --------------------------------------------------------- + - name: Push branch + if: steps.gitops.outputs.nochanges != 'true' + run: | + git push --set-upstream origin "${{ steps.gitops.outputs.branch }}" + + # --------------------------------------------------------- + # 6. Open PR only if changes exist + # --------------------------------------------------------- + - name: Open Pull Request + if: steps.gitops.outputs.nochanges != 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: "Automated unpack of files.zip into ${{ github.event.inputs.branch }}" + body: | + This PR was automatically generated. + + **Action:** Unpacked `files.zip` from branch `${{ github.event.inputs.branch }}`. + **Branch:** `${{ steps.gitops.outputs.branch }}` + base: ${{ github.event.inputs.branch }} + head: ${{ steps.gitops.outputs.branch }} + draft: false + + # --------------------------------------------------------- + # 7. Final log + # --------------------------------------------------------- + - name: Done + run: | + if [ "${{ steps.gitops.outputs.nochanges }}" = "true" ]; then + echo "No changes detected. Nothing to commit." + else + echo "PR created successfully." fi From 193cb42aefe58e3c4167f06ad774deb03ff88ca8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:53:35 +0000 Subject: [PATCH 03/14] Unpacked files.zip automatically --- create_and_zip.sh | 522 +++++++++++++++++++++++++++++++++ extracted/create_and_zip.sh | 522 +++++++++++++++++++++++++++++++++ extracted/ish_setup_and_run.sh | 49 ++++ extracted/upload_repo.py | 134 +++++++++ extracted/upload_repo_diag.py | 26 ++ ish_setup_and_run.sh | 49 ++++ upload_repo.py | 134 +++++++++ upload_repo_diag.py | 26 ++ 8 files changed, 1462 insertions(+) create mode 100644 create_and_zip.sh create mode 100644 extracted/create_and_zip.sh create mode 100644 extracted/ish_setup_and_run.sh create mode 100644 extracted/upload_repo.py create mode 100644 extracted/upload_repo_diag.py create mode 100644 ish_setup_and_run.sh create mode 100644 upload_repo.py create mode 100644 upload_repo_diag.py diff --git a/create_and_zip.sh b/create_and_zip.sh new file mode 100644 index 0000000..125efcb --- /dev/null +++ b/create_and_zip.sh @@ -0,0 +1,522 @@ +#!/usr/bin/env bash +# create_and_zip.sh +# Creates the directory tree and files for the "new files from today" +# and packages them into goose_c2_files.zip +# Usage in iSH: +# paste this file via heredoc, then: +# chmod +x create_and_zip.sh +# ./create_and_zip.sh +set -euo pipefail + +# Create directories (idempotent) +mkdir -p backend/workers frontend/src/components + +# Write backend/models.py +cat > backend/models.py <<'PYEOF' +# -- C2 Models Extension for GooseStrike -- +from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey, Table +from sqlalchemy.orm import relationship +from sqlalchemy.types import JSON as JSONType +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +c2_agent_asset = Table( + 'c2_agent_asset', Base.metadata, + Column('agent_id', Integer, ForeignKey('c2_agents.id')), + Column('asset_id', Integer, ForeignKey('assets.id')), +) + +class C2Instance(Base): + __tablename__ = "c2_instances" + id = Column(Integer, primary_key=True) + provider = Column(String) + status = Column(String) + last_poll = Column(DateTime) + error = Column(Text) + +class C2Operation(Base): + __tablename__ = 'c2_operations' + id = Column(Integer, primary_key=True) + operation_id = Column(String, unique=True, index=True) + name = Column(String) + provider = Column(String) + campaign_id = Column(Integer, ForeignKey("campaigns.id"), nullable=True) + description = Column(Text) + start_time = Column(DateTime) + end_time = Column(DateTime) + alerts = relationship("C2Event", backref="operation") + +class C2Agent(Base): + __tablename__ = 'c2_agents' + id = Column(Integer, primary_key=True) + agent_id = Column(String, unique=True, index=True) + provider = Column(String) + name = Column(String) + operation_id = Column(Integer, ForeignKey("c2_operations.id"), nullable=True) + first_seen = Column(DateTime) + last_seen = Column(DateTime) + ip_address = Column(String) + hostname = Column(String) + platform = Column(String) + user = Column(String) + pid = Column(Integer) + state = Column(String) + mitre_techniques = Column(JSONType) + assets = relationship("Asset", secondary=c2_agent_asset, backref="c2_agents") + +class C2Event(Base): + __tablename__ = 'c2_events' + id = Column(Integer, primary_key=True) + event_id = Column(String, unique=True, index=True) + type = Column(String) + description = Column(Text) + agent_id = Column(Integer, ForeignKey('c2_agents.id')) + operation_id = Column(Integer, ForeignKey('c2_operations.id')) + timestamp = Column(DateTime) + mitre_tag = Column(String) + details = Column(JSONType, default=dict) + +class C2Payload(Base): + __tablename__ = "c2_payloads" + id = Column(Integer, primary_key=True) + payload_id = Column(String, unique=True) + provider = Column(String) + agent_id = Column(String) + operation_id = Column(String) + type = Column(String) + created_at = Column(DateTime) + filename = Column(String) + path = Column(String) + content = Column(Text) + +class C2Listener(Base): + __tablename__ = "c2_listeners" + id = Column(Integer, primary_key=True) + listener_id = Column(String, unique=True) + provider = Column(String) + operation_id = Column(String) + port = Column(Integer) + transport = Column(String) + status = Column(String) + created_at = Column(DateTime) + +class C2Task(Base): + __tablename__ = "c2_tasks" + id = Column(Integer, primary_key=True) + task_id = Column(String, unique=True, index=True) + agent_id = Column(String) + operation_id = Column(String) + command = Column(Text) + status = Column(String) + result = Column(Text) + created_at = Column(DateTime) + executed_at = Column(DateTime) + error = Column(Text) + mitre_technique = Column(String) +PYEOF + +# Write backend/workers/c2_integration.py +cat > backend/workers/c2_integration.py <<'PYEOF' +#!/usr/bin/env python3 +# Simplified C2 poller adapters (Mythic/Caldera) — adjust imports for your repo +import os, time, requests, logging +from datetime import datetime +# Import models and Session from your project; this is a placeholder import +try: + from models import Session, C2Instance, C2Agent, C2Operation, C2Event, C2Payload, C2Listener, C2Task, Asset +except Exception: + # If using package layout, adapt the import path + try: + from backend.models import Session, C2Instance, C2Agent, C2Operation, C2Event, C2Payload, C2Listener, C2Task, Asset + except Exception: + # Minimal placeholders to avoid immediate runtime errors during demo + Session = None + C2Instance = C2Agent = C2Operation = C2Event = C2Payload = C2Listener = C2Task = Asset = object + +from urllib.parse import urljoin + +class BaseC2Adapter: + def __init__(self, base_url, api_token): + self.base_url = base_url + self.api_token = api_token + + def api(self, path, method="get", **kwargs): + url = urljoin(self.base_url, path) + headers = kwargs.pop("headers", {}) + if self.api_token: + headers["Authorization"] = f"Bearer {self.api_token}" + try: + r = getattr(requests, method)(url, headers=headers, timeout=15, **kwargs) + r.raise_for_status() + return r.json() + except Exception as e: + logging.error(f"C2 API error {url}: {e}") + return None + + def get_status(self): raise NotImplementedError + def get_agents(self): raise NotImplementedError + def get_operations(self): raise NotImplementedError + def get_events(self, since=None): raise NotImplementedError + def create_payload(self, op_id, typ, params): raise NotImplementedError + def launch_command(self, agent_id, cmd): raise NotImplementedError + def create_listener(self, op_id, port, transport): raise NotImplementedError + +class MythicAdapter(BaseC2Adapter): + def get_status(self): return self.api("/api/v1/status") + def get_agents(self): return (self.api("/api/v1/agents") or {}).get("agents", []) + def get_operations(self): return (self.api("/api/v1/operations") or {}).get("operations", []) + def get_events(self, since=None): return (self.api("/api/v1/events") or {}).get("events", []) + def create_payload(self, op_id, typ, params): + return self.api("/api/v1/payloads", "post", json={"operation_id": op_id, "type": typ, "params": params}) + def launch_command(self, agent_id, cmd): + return self.api(f"/api/v1/agents/{agent_id}/tasks", "post", json={"command": cmd}) + def create_listener(self, op_id, port, transport): + return self.api("/api/v1/listeners", "post", json={"operation_id": op_id, "port": port, "transport": transport}) + +class CalderaAdapter(BaseC2Adapter): + def _caldera_headers(self): + headers = {"Content-Type": "application/json"} + if self.api_token: + headers["Authorization"] = f"Bearer {self.api_token}" + return headers + + def get_status(self): + try: + r = requests.get(f"{self.base_url}/api/health", headers=self._caldera_headers(), timeout=10) + return {"provider": "caldera", "status": r.json().get("status", "healthy")} + except Exception: + return {"provider": "caldera", "status": "unreachable"} + + def get_agents(self): + r = requests.get(f"{self.base_url}/api/agents/all", headers=self._caldera_headers(), timeout=15) + agents = r.json() if r.status_code == 200 else [] + for agent in agents: + mitre_tids = [] + for ab in agent.get("abilities", []): + tid = ab.get("attack", {}).get("technique_id") + if tid: + mitre_tids.append(tid) + agent["mitre"] = mitre_tids + return [{"id": agent.get("paw"), "name": agent.get("host"), "ip": agent.get("host"), "hostname": agent.get("host"), "platform": agent.get("platform"), "pid": agent.get("pid"), "status": "online" if agent.get("trusted", False) else "offline", "mitre": agent.get("mitre"), "operation": agent.get("operation")} for agent in agents] + + def get_operations(self): + r = requests.get(f"{self.base_url}/api/operations", headers=self._caldera_headers(), timeout=10) + ops = r.json() if r.status_code == 200 else [] + return [{"id": op.get("id"), "name": op.get("name"), "start_time": op.get("start"), "description": op.get("description", "")} for op in ops] + + def get_events(self, since_timestamp=None): + events = [] + ops = self.get_operations() + for op in ops: + url = f"{self.base_url}/api/operations/{op['id']}/reports" + r = requests.get(url, headers=self._caldera_headers(), timeout=15) + reports = r.json() if r.status_code == 200 else [] + for event in reports: + evt_time = event.get("timestamp") + if since_timestamp and evt_time < since_timestamp: + continue + events.append({"id": event.get("id", ""), "type": event.get("event_type", ""), "description": event.get("message", ""), "agent": event.get("paw", None), "operation": op["id"], "time": evt_time, "mitre": event.get("ability_id", None), "details": event}) + return events + + def create_payload(self, operation_id, payload_type, params): + ability_id = params.get("ability_id") + if not ability_id: + return {"error": "ability_id required"} + r = requests.post(f"{self.base_url}/api/abilities/{ability_id}/create_payload", headers=self._caldera_headers(), json={"operation_id": operation_id}) + j = r.json() if r.status_code == 200 else {} + return {"id": j.get("id", ""), "filename": j.get("filename", ""), "path": j.get("path", ""), "content": j.get("content", "")} + + def launch_command(self, agent_id, command): + ability_id = command.get("ability_id") + cmd_blob = command.get("cmd_blob") + data = {"ability_id": ability_id} + if cmd_blob: + data["cmd"] = cmd_blob + r = requests.post(f"{self.base_url}/api/agents/{agent_id}/task", headers=self._caldera_headers(), json=data) + return r.json() if r.status_code in (200,201) else {"error": "failed"} + + def create_listener(self, operation_id, port, transport): + try: + r = requests.post(f"{self.base_url}/api/listeners", headers=self._caldera_headers(), json={"operation_id": operation_id, "port": port, "transport": transport}) + return r.json() + except Exception as e: + return {"error": str(e)} + +def get_c2_adapter(): + provider = os.getenv("C2_PROVIDER", "none") + url = os.getenv("C2_BASE_URL", "http://c2:7443") + token = os.getenv("C2_API_TOKEN", "") + if provider == "mythic": + return MythicAdapter(url, token) + if provider == "caldera": + return CalderaAdapter(url, token) + return None + +class C2Poller: + def __init__(self, poll_interval=60): + self.adapter = get_c2_adapter() + self.poll_interval = int(os.getenv("C2_POLL_INTERVAL", poll_interval or 60)) + self.last_event_poll = None + + def _store(self, instance_raw, agents_raw, operations_raw, events_raw): + # This function expects a working SQLAlchemy Session and models + if Session is None: + return + db = Session() + now = datetime.utcnow() + inst = db.query(C2Instance).first() + if not inst: + inst = C2Instance(provider=instance_raw.get("provider"), status=instance_raw.get("status"), last_poll=now) + else: + inst.status = instance_raw.get("status") + inst.last_poll = now + db.add(inst) + + opmap = {} + for op_data in operations_raw or []: + op = db.query(C2Operation).filter_by(operation_id=op_data["id"]).first() + if not op: + op = C2Operation(operation_id=op_data["id"], name=op_data.get("name"), provider=inst.provider, start_time=op_data.get("start_time")) + db.merge(op) + db.flush() + opmap[op.operation_id] = op.id + + for agent_data in agents_raw or []: + agent = db.query(C2Agent).filter_by(agent_id=agent_data["id"]).first() + if not agent: + agent = C2Agent(agent_id=agent_data["id"], provider=inst.provider, name=agent_data.get("name"), first_seen=now) + agent.last_seen = now + agent.operation_id = opmap.get(agent_data.get("operation")) + agent.ip_address = agent_data.get("ip") + agent.state = agent_data.get("status", "unknown") + agent.mitre_techniques = agent_data.get("mitre", []) + db.merge(agent) + db.flush() + + for evt in events_raw or []: + event = db.query(C2Event).filter_by(event_id=evt.get("id","")).first() + if not event: + event = C2Event(event_id=evt.get("id",""), type=evt.get("type",""), description=evt.get("description",""), agent_id=evt.get("agent"), operation_id=evt.get("operation"), timestamp=evt.get("time", now), mitre_tag=evt.get("mitre"), details=evt) + db.merge(event) + db.commit() + db.close() + + def run(self): + while True: + try: + if not self.adapter: + time.sleep(self.poll_interval) + continue + instance = self.adapter.get_status() + agents = self.adapter.get_agents() + operations = self.adapter.get_operations() + events = self.adapter.get_events(since=self.last_event_poll) + self.last_event_poll = datetime.utcnow().isoformat() + self._store(instance, agents, operations, events) + except Exception as e: + print("C2 poll error", e) + time.sleep(self.poll_interval) + +if __name__ == "__main__": + C2Poller().run() +PYEOF + +# Write backend/routes/c2.py +cat > backend/routes_c2_placeholder.py <<'PYEOF' +# Placeholder router. In your FastAPI app, create a router that imports your adapter and DB models. +# This file is a simple reference; integrate into your backend/routes/c2.py as needed. +from fastapi import APIRouter, Request +from datetime import datetime +router = APIRouter() + +@router.get("/status") +def c2_status(): + return {"provider": None, "status": "not-configured", "last_poll": None} +PYEOF +mv backend/routes_c2_placeholder.py backend/routes_c2.py + +# Create the frontend component file +cat > frontend/src/components/C2Operations.jsx <<'JSEOF' +import React, {useEffect, useState} from "react"; +export default function C2Operations() { + const [status, setStatus] = useState({}); + const [agents, setAgents] = useState([]); + const [ops, setOps] = useState([]); + const [events, setEvents] = useState([]); + const [abilityList, setAbilityList] = useState([]); + const [showTaskDialog, setShowTaskDialog] = useState(false); + const [taskAgentId, setTaskAgentId] = useState(null); + const [activeOp, setActiveOp] = useState(null); + + useEffect(() => { + fetch("/c2/status").then(r=>r.json()).then(setStatus).catch(()=>{}); + fetch("/c2/operations").then(r=>r.json()).then(ops=>{ + setOps(ops); setActiveOp(ops.length ? ops[0].id : null); + }).catch(()=>{}); + fetch("/c2/abilities").then(r=>r.json()).then(setAbilityList).catch(()=>{}); + }, []); + + useEffect(() => { + if (activeOp) { + fetch(`/c2/agents?operation=${activeOp}`).then(r=>r.json()).then(setAgents).catch(()=>{}); + fetch(`/c2/events?op=${activeOp}`).then(r=>r.json()).then(setEvents).catch(()=>{}); + } + }, [activeOp]); + + const genPayload = async () => { + const typ = prompt("Payload type? (beacon/http etc)"); + if (!typ) return; + const res = await fetch("/c2/payload", { + method:"POST",headers:{"Content-Type":"application/json"}, + body:JSON.stringify({operation_id:activeOp,type:typ,params:{}}) + }); + alert("Payload: " + (await res.text())); + }; + const createListener = async () => { + const port = prompt("Port to listen on?"); + const transport = prompt("Transport? (http/smb/etc)"); + if (!port || !transport) return; + await fetch("/c2/listener",{method:"POST",headers:{"Content-Type":"application/json"}, + body:JSON.stringify({operation_id:activeOp,port:Number(port),transport}) + }); + alert("Listener created!"); + }; + const openTaskDialog = (agentId) => { + setTaskAgentId(agentId); + setShowTaskDialog(true); + }; + const handleTaskSend = async () => { + const abilityId = document.getElementById("caldera_ability_select").value; + const cmd_blob = document.getElementById("caldera_cmd_input").value; + await fetch(`/c2/agents/${taskAgentId}/command`, { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({command:{ability_id:abilityId, cmd_blob}}) + }); + setShowTaskDialog(false); + alert("Task sent to agent!"); + }; + + const renderMitre = tidList => tidList ? tidList.map(tid=> + {tid} + ) : null; + + return ( +
+

C2 Operations ({status.provider || 'Unconfigured'})

+
+ + + + +
+
+

Agents

+ + + + {agents.map(a=> + + + + + + + + + )} +
AgentIPHostnameStatusMITRETask
{a.name||a.id}{a.ip}{a.hostname}{a.state}{renderMitre(a.mitre_techniques)}
+
+
+

Recent Events

+
    + {events.map(e=> +
  • [{e.type}] {e.desc} [Agent:{e.agent} Op:{e.op}] {e.mitre && {e.mitre}} @ {e.time}
  • + )} +
+
+
+ ⚠️ LAB ONLY: All actions are for simulation/training inside this closed cyber range! +
+ {showTaskDialog && +
+

Task Agent {taskAgentId} (Caldera)

+ + +
+ + +
+ + +
+ } +
+ ); +} +JSEOF + +# Minimal supporting files +cat > docker-compose.kali.yml <<'YAML' +services: + api: + build: ./backend + ui: + build: ./frontend +YAML + +cat > COMPREHENSIVE_GUIDE.md <<'GUIDE' +# Comprehensive Guide (placeholder) +This is the comprehensive guide placeholder. Replace with full content as needed. +GUIDE + +cat > C2-integration-session.md <<'SESSION' +C2 integration session transcript placeholder. +SESSION + +cat > README.md <<'RME' +# GooseStrike Cyber Range - placeholder README +RME + +# Create a simple package.json to ensure directory present +mkdir -p frontend +cat > frontend/package.json <<'PKG' +{ "name": "goosestrike-frontend", "version": "0.1.0" } +PKG + +# Create the zip +ZIPNAME="goose_c2_files.zip" +if command -v zip >/dev/null 2>&1; then + zip -r "${ZIPNAME}" backend frontend docker-compose.kali.yml COMPREHENSIVE_GUIDE.md C2-integration-session.md README.md >/dev/null +else + python3 - < backend/models.py <<'PYEOF' +# -- C2 Models Extension for GooseStrike -- +from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey, Table +from sqlalchemy.orm import relationship +from sqlalchemy.types import JSON as JSONType +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +c2_agent_asset = Table( + 'c2_agent_asset', Base.metadata, + Column('agent_id', Integer, ForeignKey('c2_agents.id')), + Column('asset_id', Integer, ForeignKey('assets.id')), +) + +class C2Instance(Base): + __tablename__ = "c2_instances" + id = Column(Integer, primary_key=True) + provider = Column(String) + status = Column(String) + last_poll = Column(DateTime) + error = Column(Text) + +class C2Operation(Base): + __tablename__ = 'c2_operations' + id = Column(Integer, primary_key=True) + operation_id = Column(String, unique=True, index=True) + name = Column(String) + provider = Column(String) + campaign_id = Column(Integer, ForeignKey("campaigns.id"), nullable=True) + description = Column(Text) + start_time = Column(DateTime) + end_time = Column(DateTime) + alerts = relationship("C2Event", backref="operation") + +class C2Agent(Base): + __tablename__ = 'c2_agents' + id = Column(Integer, primary_key=True) + agent_id = Column(String, unique=True, index=True) + provider = Column(String) + name = Column(String) + operation_id = Column(Integer, ForeignKey("c2_operations.id"), nullable=True) + first_seen = Column(DateTime) + last_seen = Column(DateTime) + ip_address = Column(String) + hostname = Column(String) + platform = Column(String) + user = Column(String) + pid = Column(Integer) + state = Column(String) + mitre_techniques = Column(JSONType) + assets = relationship("Asset", secondary=c2_agent_asset, backref="c2_agents") + +class C2Event(Base): + __tablename__ = 'c2_events' + id = Column(Integer, primary_key=True) + event_id = Column(String, unique=True, index=True) + type = Column(String) + description = Column(Text) + agent_id = Column(Integer, ForeignKey('c2_agents.id')) + operation_id = Column(Integer, ForeignKey('c2_operations.id')) + timestamp = Column(DateTime) + mitre_tag = Column(String) + details = Column(JSONType, default=dict) + +class C2Payload(Base): + __tablename__ = "c2_payloads" + id = Column(Integer, primary_key=True) + payload_id = Column(String, unique=True) + provider = Column(String) + agent_id = Column(String) + operation_id = Column(String) + type = Column(String) + created_at = Column(DateTime) + filename = Column(String) + path = Column(String) + content = Column(Text) + +class C2Listener(Base): + __tablename__ = "c2_listeners" + id = Column(Integer, primary_key=True) + listener_id = Column(String, unique=True) + provider = Column(String) + operation_id = Column(String) + port = Column(Integer) + transport = Column(String) + status = Column(String) + created_at = Column(DateTime) + +class C2Task(Base): + __tablename__ = "c2_tasks" + id = Column(Integer, primary_key=True) + task_id = Column(String, unique=True, index=True) + agent_id = Column(String) + operation_id = Column(String) + command = Column(Text) + status = Column(String) + result = Column(Text) + created_at = Column(DateTime) + executed_at = Column(DateTime) + error = Column(Text) + mitre_technique = Column(String) +PYEOF + +# Write backend/workers/c2_integration.py +cat > backend/workers/c2_integration.py <<'PYEOF' +#!/usr/bin/env python3 +# Simplified C2 poller adapters (Mythic/Caldera) — adjust imports for your repo +import os, time, requests, logging +from datetime import datetime +# Import models and Session from your project; this is a placeholder import +try: + from models import Session, C2Instance, C2Agent, C2Operation, C2Event, C2Payload, C2Listener, C2Task, Asset +except Exception: + # If using package layout, adapt the import path + try: + from backend.models import Session, C2Instance, C2Agent, C2Operation, C2Event, C2Payload, C2Listener, C2Task, Asset + except Exception: + # Minimal placeholders to avoid immediate runtime errors during demo + Session = None + C2Instance = C2Agent = C2Operation = C2Event = C2Payload = C2Listener = C2Task = Asset = object + +from urllib.parse import urljoin + +class BaseC2Adapter: + def __init__(self, base_url, api_token): + self.base_url = base_url + self.api_token = api_token + + def api(self, path, method="get", **kwargs): + url = urljoin(self.base_url, path) + headers = kwargs.pop("headers", {}) + if self.api_token: + headers["Authorization"] = f"Bearer {self.api_token}" + try: + r = getattr(requests, method)(url, headers=headers, timeout=15, **kwargs) + r.raise_for_status() + return r.json() + except Exception as e: + logging.error(f"C2 API error {url}: {e}") + return None + + def get_status(self): raise NotImplementedError + def get_agents(self): raise NotImplementedError + def get_operations(self): raise NotImplementedError + def get_events(self, since=None): raise NotImplementedError + def create_payload(self, op_id, typ, params): raise NotImplementedError + def launch_command(self, agent_id, cmd): raise NotImplementedError + def create_listener(self, op_id, port, transport): raise NotImplementedError + +class MythicAdapter(BaseC2Adapter): + def get_status(self): return self.api("/api/v1/status") + def get_agents(self): return (self.api("/api/v1/agents") or {}).get("agents", []) + def get_operations(self): return (self.api("/api/v1/operations") or {}).get("operations", []) + def get_events(self, since=None): return (self.api("/api/v1/events") or {}).get("events", []) + def create_payload(self, op_id, typ, params): + return self.api("/api/v1/payloads", "post", json={"operation_id": op_id, "type": typ, "params": params}) + def launch_command(self, agent_id, cmd): + return self.api(f"/api/v1/agents/{agent_id}/tasks", "post", json={"command": cmd}) + def create_listener(self, op_id, port, transport): + return self.api("/api/v1/listeners", "post", json={"operation_id": op_id, "port": port, "transport": transport}) + +class CalderaAdapter(BaseC2Adapter): + def _caldera_headers(self): + headers = {"Content-Type": "application/json"} + if self.api_token: + headers["Authorization"] = f"Bearer {self.api_token}" + return headers + + def get_status(self): + try: + r = requests.get(f"{self.base_url}/api/health", headers=self._caldera_headers(), timeout=10) + return {"provider": "caldera", "status": r.json().get("status", "healthy")} + except Exception: + return {"provider": "caldera", "status": "unreachable"} + + def get_agents(self): + r = requests.get(f"{self.base_url}/api/agents/all", headers=self._caldera_headers(), timeout=15) + agents = r.json() if r.status_code == 200 else [] + for agent in agents: + mitre_tids = [] + for ab in agent.get("abilities", []): + tid = ab.get("attack", {}).get("technique_id") + if tid: + mitre_tids.append(tid) + agent["mitre"] = mitre_tids + return [{"id": agent.get("paw"), "name": agent.get("host"), "ip": agent.get("host"), "hostname": agent.get("host"), "platform": agent.get("platform"), "pid": agent.get("pid"), "status": "online" if agent.get("trusted", False) else "offline", "mitre": agent.get("mitre"), "operation": agent.get("operation")} for agent in agents] + + def get_operations(self): + r = requests.get(f"{self.base_url}/api/operations", headers=self._caldera_headers(), timeout=10) + ops = r.json() if r.status_code == 200 else [] + return [{"id": op.get("id"), "name": op.get("name"), "start_time": op.get("start"), "description": op.get("description", "")} for op in ops] + + def get_events(self, since_timestamp=None): + events = [] + ops = self.get_operations() + for op in ops: + url = f"{self.base_url}/api/operations/{op['id']}/reports" + r = requests.get(url, headers=self._caldera_headers(), timeout=15) + reports = r.json() if r.status_code == 200 else [] + for event in reports: + evt_time = event.get("timestamp") + if since_timestamp and evt_time < since_timestamp: + continue + events.append({"id": event.get("id", ""), "type": event.get("event_type", ""), "description": event.get("message", ""), "agent": event.get("paw", None), "operation": op["id"], "time": evt_time, "mitre": event.get("ability_id", None), "details": event}) + return events + + def create_payload(self, operation_id, payload_type, params): + ability_id = params.get("ability_id") + if not ability_id: + return {"error": "ability_id required"} + r = requests.post(f"{self.base_url}/api/abilities/{ability_id}/create_payload", headers=self._caldera_headers(), json={"operation_id": operation_id}) + j = r.json() if r.status_code == 200 else {} + return {"id": j.get("id", ""), "filename": j.get("filename", ""), "path": j.get("path", ""), "content": j.get("content", "")} + + def launch_command(self, agent_id, command): + ability_id = command.get("ability_id") + cmd_blob = command.get("cmd_blob") + data = {"ability_id": ability_id} + if cmd_blob: + data["cmd"] = cmd_blob + r = requests.post(f"{self.base_url}/api/agents/{agent_id}/task", headers=self._caldera_headers(), json=data) + return r.json() if r.status_code in (200,201) else {"error": "failed"} + + def create_listener(self, operation_id, port, transport): + try: + r = requests.post(f"{self.base_url}/api/listeners", headers=self._caldera_headers(), json={"operation_id": operation_id, "port": port, "transport": transport}) + return r.json() + except Exception as e: + return {"error": str(e)} + +def get_c2_adapter(): + provider = os.getenv("C2_PROVIDER", "none") + url = os.getenv("C2_BASE_URL", "http://c2:7443") + token = os.getenv("C2_API_TOKEN", "") + if provider == "mythic": + return MythicAdapter(url, token) + if provider == "caldera": + return CalderaAdapter(url, token) + return None + +class C2Poller: + def __init__(self, poll_interval=60): + self.adapter = get_c2_adapter() + self.poll_interval = int(os.getenv("C2_POLL_INTERVAL", poll_interval or 60)) + self.last_event_poll = None + + def _store(self, instance_raw, agents_raw, operations_raw, events_raw): + # This function expects a working SQLAlchemy Session and models + if Session is None: + return + db = Session() + now = datetime.utcnow() + inst = db.query(C2Instance).first() + if not inst: + inst = C2Instance(provider=instance_raw.get("provider"), status=instance_raw.get("status"), last_poll=now) + else: + inst.status = instance_raw.get("status") + inst.last_poll = now + db.add(inst) + + opmap = {} + for op_data in operations_raw or []: + op = db.query(C2Operation).filter_by(operation_id=op_data["id"]).first() + if not op: + op = C2Operation(operation_id=op_data["id"], name=op_data.get("name"), provider=inst.provider, start_time=op_data.get("start_time")) + db.merge(op) + db.flush() + opmap[op.operation_id] = op.id + + for agent_data in agents_raw or []: + agent = db.query(C2Agent).filter_by(agent_id=agent_data["id"]).first() + if not agent: + agent = C2Agent(agent_id=agent_data["id"], provider=inst.provider, name=agent_data.get("name"), first_seen=now) + agent.last_seen = now + agent.operation_id = opmap.get(agent_data.get("operation")) + agent.ip_address = agent_data.get("ip") + agent.state = agent_data.get("status", "unknown") + agent.mitre_techniques = agent_data.get("mitre", []) + db.merge(agent) + db.flush() + + for evt in events_raw or []: + event = db.query(C2Event).filter_by(event_id=evt.get("id","")).first() + if not event: + event = C2Event(event_id=evt.get("id",""), type=evt.get("type",""), description=evt.get("description",""), agent_id=evt.get("agent"), operation_id=evt.get("operation"), timestamp=evt.get("time", now), mitre_tag=evt.get("mitre"), details=evt) + db.merge(event) + db.commit() + db.close() + + def run(self): + while True: + try: + if not self.adapter: + time.sleep(self.poll_interval) + continue + instance = self.adapter.get_status() + agents = self.adapter.get_agents() + operations = self.adapter.get_operations() + events = self.adapter.get_events(since=self.last_event_poll) + self.last_event_poll = datetime.utcnow().isoformat() + self._store(instance, agents, operations, events) + except Exception as e: + print("C2 poll error", e) + time.sleep(self.poll_interval) + +if __name__ == "__main__": + C2Poller().run() +PYEOF + +# Write backend/routes/c2.py +cat > backend/routes_c2_placeholder.py <<'PYEOF' +# Placeholder router. In your FastAPI app, create a router that imports your adapter and DB models. +# This file is a simple reference; integrate into your backend/routes/c2.py as needed. +from fastapi import APIRouter, Request +from datetime import datetime +router = APIRouter() + +@router.get("/status") +def c2_status(): + return {"provider": None, "status": "not-configured", "last_poll": None} +PYEOF +mv backend/routes_c2_placeholder.py backend/routes_c2.py + +# Create the frontend component file +cat > frontend/src/components/C2Operations.jsx <<'JSEOF' +import React, {useEffect, useState} from "react"; +export default function C2Operations() { + const [status, setStatus] = useState({}); + const [agents, setAgents] = useState([]); + const [ops, setOps] = useState([]); + const [events, setEvents] = useState([]); + const [abilityList, setAbilityList] = useState([]); + const [showTaskDialog, setShowTaskDialog] = useState(false); + const [taskAgentId, setTaskAgentId] = useState(null); + const [activeOp, setActiveOp] = useState(null); + + useEffect(() => { + fetch("/c2/status").then(r=>r.json()).then(setStatus).catch(()=>{}); + fetch("/c2/operations").then(r=>r.json()).then(ops=>{ + setOps(ops); setActiveOp(ops.length ? ops[0].id : null); + }).catch(()=>{}); + fetch("/c2/abilities").then(r=>r.json()).then(setAbilityList).catch(()=>{}); + }, []); + + useEffect(() => { + if (activeOp) { + fetch(`/c2/agents?operation=${activeOp}`).then(r=>r.json()).then(setAgents).catch(()=>{}); + fetch(`/c2/events?op=${activeOp}`).then(r=>r.json()).then(setEvents).catch(()=>{}); + } + }, [activeOp]); + + const genPayload = async () => { + const typ = prompt("Payload type? (beacon/http etc)"); + if (!typ) return; + const res = await fetch("/c2/payload", { + method:"POST",headers:{"Content-Type":"application/json"}, + body:JSON.stringify({operation_id:activeOp,type:typ,params:{}}) + }); + alert("Payload: " + (await res.text())); + }; + const createListener = async () => { + const port = prompt("Port to listen on?"); + const transport = prompt("Transport? (http/smb/etc)"); + if (!port || !transport) return; + await fetch("/c2/listener",{method:"POST",headers:{"Content-Type":"application/json"}, + body:JSON.stringify({operation_id:activeOp,port:Number(port),transport}) + }); + alert("Listener created!"); + }; + const openTaskDialog = (agentId) => { + setTaskAgentId(agentId); + setShowTaskDialog(true); + }; + const handleTaskSend = async () => { + const abilityId = document.getElementById("caldera_ability_select").value; + const cmd_blob = document.getElementById("caldera_cmd_input").value; + await fetch(`/c2/agents/${taskAgentId}/command`, { + method: "POST", + headers: {"Content-Type":"application/json"}, + body: JSON.stringify({command:{ability_id:abilityId, cmd_blob}}) + }); + setShowTaskDialog(false); + alert("Task sent to agent!"); + }; + + const renderMitre = tidList => tidList ? tidList.map(tid=> + {tid} + ) : null; + + return ( +
+

C2 Operations ({status.provider || 'Unconfigured'})

+
+ + + + +
+
+

Agents

+ + + + {agents.map(a=> + + + + + + + + + )} +
AgentIPHostnameStatusMITRETask
{a.name||a.id}{a.ip}{a.hostname}{a.state}{renderMitre(a.mitre_techniques)}
+
+
+

Recent Events

+
    + {events.map(e=> +
  • [{e.type}] {e.desc} [Agent:{e.agent} Op:{e.op}] {e.mitre && {e.mitre}} @ {e.time}
  • + )} +
+
+
+ ⚠️ LAB ONLY: All actions are for simulation/training inside this closed cyber range! +
+ {showTaskDialog && +
+

Task Agent {taskAgentId} (Caldera)

+ + +
+ + +
+ + +
+ } +
+ ); +} +JSEOF + +# Minimal supporting files +cat > docker-compose.kali.yml <<'YAML' +services: + api: + build: ./backend + ui: + build: ./frontend +YAML + +cat > COMPREHENSIVE_GUIDE.md <<'GUIDE' +# Comprehensive Guide (placeholder) +This is the comprehensive guide placeholder. Replace with full content as needed. +GUIDE + +cat > C2-integration-session.md <<'SESSION' +C2 integration session transcript placeholder. +SESSION + +cat > README.md <<'RME' +# GooseStrike Cyber Range - placeholder README +RME + +# Create a simple package.json to ensure directory present +mkdir -p frontend +cat > frontend/package.json <<'PKG' +{ "name": "goosestrike-frontend", "version": "0.1.0" } +PKG + +# Create the zip +ZIPNAME="goose_c2_files.zip" +if command -v zip >/dev/null 2>&1; then + zip -r "${ZIPNAME}" backend frontend docker-compose.kali.yml COMPREHENSIVE_GUIDE.md C2-integration-session.md README.md >/dev/null +else + python3 - < create_and_zip.sh <<'EOF'" +echo " (paste content)" +echo " EOF" +echo " then chmod +x create_and_zip.sh" +echo " 2) Or, use nano/vi if you installed an editor: apk add nano; nano create_and_zip.sh" +echo +echo "If you already have create_and_zip.sh, run:" +echo " chmod +x create_and_zip.sh" +echo " ./create_and_zip.sh" +echo +echo "After the zip is created (goose_c2_files.zip), you can either:" +echo " - Upload from iSH to GitHub directly with upload_repo.py (preferred):" +echo " export GITHUB_TOKEN=''" +echo " export REPO='owner/repo' # e.g. mblanke/StrikePackageGPT-Lab" +echo " export BRANCH='c2-integration' # optional" +echo " export ZIP_FILENAME='goose_c2_files.zip'" +echo " python3 upload_repo.py" +echo +echo " - Or download the zip to your iPad using a simple HTTP server:" +echo " python3 -m http.server 8000 &" +echo " Then open Safari and go to http://127.0.0.1:8000 to tap and download goose_c2_files.zip" +echo +echo "Note: iSH storage is in-app. If you want the zip in Files app, use the HTTP server method and save from Safari, or upload to Replit/GitHub directly from iSH." +echo +echo "Done. If you want I can paste create_and_zip.sh and upload_repo.py here for you to paste into iSH." \ No newline at end of file diff --git a/extracted/upload_repo.py b/extracted/upload_repo.py new file mode 100644 index 0000000..855071a --- /dev/null +++ b/extracted/upload_repo.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +""" +upload_repo.py + +Uploads files from a zip into a GitHub repo branch using the Contents API. + +Environment variables: + GITHUB_TOKEN - personal access token (repo scope) + REPO - owner/repo (e.g. mblanke/StrikePackageGPT-Lab) + BRANCH - target branch name (default: c2-integration) + ZIP_FILENAME - name of zip file present in the current directory + +Usage: + export GITHUB_TOKEN='ghp_xxx' + export REPO='owner/repo' + export BRANCH='c2-integration' + export ZIP_FILENAME='goose_c2_files.zip' + python3 upload_repo.py +""" +import os, sys, base64, zipfile, requests, time +from pathlib import Path +from urllib.parse import quote_plus + +API_BASE = "https://api.github.com" + +def die(msg): + print("ERROR:", msg); sys.exit(1) + +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") +REPO = os.environ.get("REPO") +BRANCH = os.environ.get("BRANCH", "c2-integration") +ZIP_FILENAME = os.environ.get("ZIP_FILENAME") + +def api_headers(): + if not GITHUB_TOKEN: + die("GITHUB_TOKEN not set") + return {"Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json"} + +def get_default_branch(): + url = f"{API_BASE}/repos/{REPO}" + r = requests.get(url, headers=api_headers()) + if r.status_code != 200: + die(f"Failed to get repo info: {r.status_code} {r.text}") + return r.json().get("default_branch") + +def get_ref_sha(branch): + url = f"{API_BASE}/repos/{REPO}/git/refs/heads/{branch}" + r = requests.get(url, headers=api_headers()) + if r.status_code == 200: + return r.json()["object"]["sha"] + return None + +def create_branch(new_branch, from_sha): + url = f"{API_BASE}/repos/{REPO}/git/refs" + payload = {"ref": f"refs/heads/{new_branch}", "sha": from_sha} + r = requests.post(url, json=payload, headers=api_headers()) + if r.status_code in (201, 422): + print(f"Branch {new_branch} created or already exists.") + return True + else: + die(f"Failed to create branch: {r.status_code} {r.text}") + +def get_file_sha(path, branch): + url = f"{API_BASE}/repos/{REPO}/contents/{quote_plus(path)}?ref={branch}" + r = requests.get(url, headers=api_headers()) + if r.status_code == 200: + return r.json().get("sha") + return None + +def put_file(path, content_b64, message, branch, sha=None): + url = f"{API_BASE}/repos/{REPO}/contents/{quote_plus(path)}" + payload = {"message": message, "content": content_b64, "branch": branch} + if sha: + payload["sha"] = sha + r = requests.put(url, json=payload, headers=api_headers()) + return (r.status_code in (200,201)), r.text + +def extract_zip(zip_path, target_dir): + with zipfile.ZipFile(zip_path, 'r') as z: + z.extractall(target_dir) + +def gather_files(root_dir): + files = [] + for dirpath, dirnames, filenames in os.walk(root_dir): + if ".git" in dirpath.split(os.sep): + continue + for fn in filenames: + files.append(os.path.join(dirpath, fn)) + return files + +def main(): + if not GITHUB_TOKEN or not REPO or not ZIP_FILENAME: + print("Set env vars: GITHUB_TOKEN, REPO, ZIP_FILENAME. Optionally BRANCH.") + sys.exit(1) + if not os.path.exists(ZIP_FILENAME): + die(f"Zip file not found: {ZIP_FILENAME}") + default_branch = get_default_branch() + print("Default branch:", default_branch) + base_sha = get_ref_sha(default_branch) + if not base_sha: + die(f"Could not find ref for default branch {default_branch}") + create_branch(BRANCH, base_sha) + tmp_dir = Path("tmp_upload") + if tmp_dir.exists(): + for p in tmp_dir.rglob("*"): + try: + if p.is_file(): p.unlink() + except: pass + tmp_dir.mkdir(exist_ok=True) + print("Extracting zip...") + extract_zip(ZIP_FILENAME, str(tmp_dir)) + files = gather_files(str(tmp_dir)) + print(f"Found {len(files)} files to upload") + uploaded = 0 + for fpath in files: + rel = os.path.relpath(fpath, str(tmp_dir)) + rel_posix = Path(rel).as_posix() + with open(fpath, "rb") as fh: + data = fh.read() + content_b64 = base64.b64encode(data).decode("utf-8") + sha = get_file_sha(rel_posix, BRANCH) + msg = f"Add/update {rel_posix} via uploader" + ok, resp = put_file(rel_posix, content_b64, msg, BRANCH, sha=sha) + if ok: + uploaded += 1 + print(f"[{uploaded}/{len(files)}] Uploaded: {rel_posix}") + else: + print(f"[!] Failed: {rel_posix} - {resp}") + time.sleep(0.25) + print(f"Completed. Uploaded {uploaded} files to branch {BRANCH}.") + print(f"Open PR: https://github.com/{REPO}/compare/{BRANCH}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/extracted/upload_repo_diag.py b/extracted/upload_repo_diag.py new file mode 100644 index 0000000..f729e82 --- /dev/null +++ b/extracted/upload_repo_diag.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import os, sys +from pathlib import Path + +def env(k): + v = os.environ.get(k) + return "" if v else "" + +print("Python:", sys.version.splitlines()[0]) +print("PWD:", os.getcwd()) +print("Workspace files:") +for p in Path(".").iterdir(): + print(" -", p) + +print("\nImportant env vars:") +for k in ("GITHUB_TOKEN","REPO","BRANCH","ZIP_FILENAME"): + print(f" {k}: {env(k)}") + +print("\nAttempting to read ZIP_FILENAME if set...") +zipf = os.environ.get("ZIP_FILENAME") +if zipf: + p = Path(zipf) + print("ZIP path:", p.resolve()) + print("Exists:", p.exists(), "Size:", p.stat().st_size if p.exists() else "N/A") +else: + print("ZIP_FILENAME not set; cannot check file.") \ No newline at end of file diff --git a/ish_setup_and_run.sh b/ish_setup_and_run.sh new file mode 100644 index 0000000..51e4fe9 --- /dev/null +++ b/ish_setup_and_run.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env sh +# iSH helper: install deps, run create_and_zip.sh, optionally run upload_repo.py +# Save this as ish_setup_and_run.sh, then: +# chmod +x ish_setup_and_run.sh +# ./ish_setup_and_run.sh + +set -e + +echo "Updating apk index..." +apk update + +echo "Installing packages: python3, py3-pip, zip, unzip, curl, git, bash" +apk add --no-cache python3 py3-pip zip unzip curl git bash + +# Ensure pip and requests are available +python3 -m ensurepip || true +pip3 install --no-cache-dir requests + +echo "All dependencies installed." + +echo +echo "FILES: place create_and_zip.sh and upload_repo.py in the current directory." +echo "Two ways to create files in iSH:" +echo " 1) On iPad: open this chat in Safari side-by-side with iSH, copy the script text, then run:" +echo " cat > create_and_zip.sh <<'EOF'" +echo " (paste content)" +echo " EOF" +echo " then chmod +x create_and_zip.sh" +echo " 2) Or, use nano/vi if you installed an editor: apk add nano; nano create_and_zip.sh" +echo +echo "If you already have create_and_zip.sh, run:" +echo " chmod +x create_and_zip.sh" +echo " ./create_and_zip.sh" +echo +echo "After the zip is created (goose_c2_files.zip), you can either:" +echo " - Upload from iSH to GitHub directly with upload_repo.py (preferred):" +echo " export GITHUB_TOKEN=''" +echo " export REPO='owner/repo' # e.g. mblanke/StrikePackageGPT-Lab" +echo " export BRANCH='c2-integration' # optional" +echo " export ZIP_FILENAME='goose_c2_files.zip'" +echo " python3 upload_repo.py" +echo +echo " - Or download the zip to your iPad using a simple HTTP server:" +echo " python3 -m http.server 8000 &" +echo " Then open Safari and go to http://127.0.0.1:8000 to tap and download goose_c2_files.zip" +echo +echo "Note: iSH storage is in-app. If you want the zip in Files app, use the HTTP server method and save from Safari, or upload to Replit/GitHub directly from iSH." +echo +echo "Done. If you want I can paste create_and_zip.sh and upload_repo.py here for you to paste into iSH." \ No newline at end of file diff --git a/upload_repo.py b/upload_repo.py new file mode 100644 index 0000000..855071a --- /dev/null +++ b/upload_repo.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +""" +upload_repo.py + +Uploads files from a zip into a GitHub repo branch using the Contents API. + +Environment variables: + GITHUB_TOKEN - personal access token (repo scope) + REPO - owner/repo (e.g. mblanke/StrikePackageGPT-Lab) + BRANCH - target branch name (default: c2-integration) + ZIP_FILENAME - name of zip file present in the current directory + +Usage: + export GITHUB_TOKEN='ghp_xxx' + export REPO='owner/repo' + export BRANCH='c2-integration' + export ZIP_FILENAME='goose_c2_files.zip' + python3 upload_repo.py +""" +import os, sys, base64, zipfile, requests, time +from pathlib import Path +from urllib.parse import quote_plus + +API_BASE = "https://api.github.com" + +def die(msg): + print("ERROR:", msg); sys.exit(1) + +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") +REPO = os.environ.get("REPO") +BRANCH = os.environ.get("BRANCH", "c2-integration") +ZIP_FILENAME = os.environ.get("ZIP_FILENAME") + +def api_headers(): + if not GITHUB_TOKEN: + die("GITHUB_TOKEN not set") + return {"Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json"} + +def get_default_branch(): + url = f"{API_BASE}/repos/{REPO}" + r = requests.get(url, headers=api_headers()) + if r.status_code != 200: + die(f"Failed to get repo info: {r.status_code} {r.text}") + return r.json().get("default_branch") + +def get_ref_sha(branch): + url = f"{API_BASE}/repos/{REPO}/git/refs/heads/{branch}" + r = requests.get(url, headers=api_headers()) + if r.status_code == 200: + return r.json()["object"]["sha"] + return None + +def create_branch(new_branch, from_sha): + url = f"{API_BASE}/repos/{REPO}/git/refs" + payload = {"ref": f"refs/heads/{new_branch}", "sha": from_sha} + r = requests.post(url, json=payload, headers=api_headers()) + if r.status_code in (201, 422): + print(f"Branch {new_branch} created or already exists.") + return True + else: + die(f"Failed to create branch: {r.status_code} {r.text}") + +def get_file_sha(path, branch): + url = f"{API_BASE}/repos/{REPO}/contents/{quote_plus(path)}?ref={branch}" + r = requests.get(url, headers=api_headers()) + if r.status_code == 200: + return r.json().get("sha") + return None + +def put_file(path, content_b64, message, branch, sha=None): + url = f"{API_BASE}/repos/{REPO}/contents/{quote_plus(path)}" + payload = {"message": message, "content": content_b64, "branch": branch} + if sha: + payload["sha"] = sha + r = requests.put(url, json=payload, headers=api_headers()) + return (r.status_code in (200,201)), r.text + +def extract_zip(zip_path, target_dir): + with zipfile.ZipFile(zip_path, 'r') as z: + z.extractall(target_dir) + +def gather_files(root_dir): + files = [] + for dirpath, dirnames, filenames in os.walk(root_dir): + if ".git" in dirpath.split(os.sep): + continue + for fn in filenames: + files.append(os.path.join(dirpath, fn)) + return files + +def main(): + if not GITHUB_TOKEN or not REPO or not ZIP_FILENAME: + print("Set env vars: GITHUB_TOKEN, REPO, ZIP_FILENAME. Optionally BRANCH.") + sys.exit(1) + if not os.path.exists(ZIP_FILENAME): + die(f"Zip file not found: {ZIP_FILENAME}") + default_branch = get_default_branch() + print("Default branch:", default_branch) + base_sha = get_ref_sha(default_branch) + if not base_sha: + die(f"Could not find ref for default branch {default_branch}") + create_branch(BRANCH, base_sha) + tmp_dir = Path("tmp_upload") + if tmp_dir.exists(): + for p in tmp_dir.rglob("*"): + try: + if p.is_file(): p.unlink() + except: pass + tmp_dir.mkdir(exist_ok=True) + print("Extracting zip...") + extract_zip(ZIP_FILENAME, str(tmp_dir)) + files = gather_files(str(tmp_dir)) + print(f"Found {len(files)} files to upload") + uploaded = 0 + for fpath in files: + rel = os.path.relpath(fpath, str(tmp_dir)) + rel_posix = Path(rel).as_posix() + with open(fpath, "rb") as fh: + data = fh.read() + content_b64 = base64.b64encode(data).decode("utf-8") + sha = get_file_sha(rel_posix, BRANCH) + msg = f"Add/update {rel_posix} via uploader" + ok, resp = put_file(rel_posix, content_b64, msg, BRANCH, sha=sha) + if ok: + uploaded += 1 + print(f"[{uploaded}/{len(files)}] Uploaded: {rel_posix}") + else: + print(f"[!] Failed: {rel_posix} - {resp}") + time.sleep(0.25) + print(f"Completed. Uploaded {uploaded} files to branch {BRANCH}.") + print(f"Open PR: https://github.com/{REPO}/compare/{BRANCH}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/upload_repo_diag.py b/upload_repo_diag.py new file mode 100644 index 0000000..f729e82 --- /dev/null +++ b/upload_repo_diag.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import os, sys +from pathlib import Path + +def env(k): + v = os.environ.get(k) + return "" if v else "" + +print("Python:", sys.version.splitlines()[0]) +print("PWD:", os.getcwd()) +print("Workspace files:") +for p in Path(".").iterdir(): + print(" -", p) + +print("\nImportant env vars:") +for k in ("GITHUB_TOKEN","REPO","BRANCH","ZIP_FILENAME"): + print(f" {k}: {env(k)}") + +print("\nAttempting to read ZIP_FILENAME if set...") +zipf = os.environ.get("ZIP_FILENAME") +if zipf: + p = Path(zipf) + print("ZIP path:", p.resolve()) + print("Exists:", p.exists(), "Size:", p.stat().st_size if p.exists() else "N/A") +else: + print("ZIP_FILENAME not set; cannot check file.") \ No newline at end of file From 7b75477450b7f88a2a201c492ade08b29a06d069 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:39:18 +0000 Subject: [PATCH 04/14] Initial plan From f49b63e7af8c598890340a0a659ce2e6db0f36c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:50:53 +0000 Subject: [PATCH 05/14] Add backend modules and frontend components for StrikePackageGPT expansion Co-authored-by: mblanke <9078342+mblanke@users.noreply.github.com> --- services/dashboard/ExplainButton.jsx | 345 ++++++++++++ services/dashboard/GuidedWizard.jsx | 487 +++++++++++++++++ services/dashboard/HelpChat.jsx | 424 ++++++++++++++ services/dashboard/NetworkMap.jsx | 315 +++++++++++ services/dashboard/VoiceControls.jsx | 354 ++++++++++++ services/dashboard/static/linux.svg | 9 + services/dashboard/static/mac.svg | 8 + services/dashboard/static/network.svg | 16 + services/dashboard/static/server.svg | 15 + services/dashboard/static/unknown.svg | 5 + services/dashboard/static/windows.svg | 7 + services/dashboard/static/workstation.svg | 9 + services/hackgpt-api/app/config_validator.py | 516 +++++++++++++++++ services/hackgpt-api/app/explain.py | 547 +++++++++++++++++++ services/hackgpt-api/app/llm_help.py | 435 +++++++++++++++ services/hackgpt-api/app/nmap_parser.py | 505 +++++++++++++++++ services/hackgpt-api/app/voice.py | 508 +++++++++++++++++ services/hackgpt-api/requirements.txt | 1 + 18 files changed, 4506 insertions(+) create mode 100644 services/dashboard/ExplainButton.jsx create mode 100644 services/dashboard/GuidedWizard.jsx create mode 100644 services/dashboard/HelpChat.jsx create mode 100644 services/dashboard/NetworkMap.jsx create mode 100644 services/dashboard/VoiceControls.jsx create mode 100644 services/dashboard/static/linux.svg create mode 100644 services/dashboard/static/mac.svg create mode 100644 services/dashboard/static/network.svg create mode 100644 services/dashboard/static/server.svg create mode 100644 services/dashboard/static/unknown.svg create mode 100644 services/dashboard/static/windows.svg create mode 100644 services/dashboard/static/workstation.svg create mode 100644 services/hackgpt-api/app/config_validator.py create mode 100644 services/hackgpt-api/app/explain.py create mode 100644 services/hackgpt-api/app/llm_help.py create mode 100644 services/hackgpt-api/app/nmap_parser.py create mode 100644 services/hackgpt-api/app/voice.py diff --git a/services/dashboard/ExplainButton.jsx b/services/dashboard/ExplainButton.jsx new file mode 100644 index 0000000..efdcccc --- /dev/null +++ b/services/dashboard/ExplainButton.jsx @@ -0,0 +1,345 @@ +/** + * ExplainButton Component + * Reusable inline "Explain" button for configs, logs, and errors + * Shows modal/popover with LLM-powered explanation + */ + +import React, { useState } from 'react'; + +const ExplainButton = ({ + type = 'config', // config, log, error, scan_result + content, + context = {}, + size = 'small', + style = {} +}) => { + const [isLoading, setIsLoading] = useState(false); + const [showModal, setShowModal] = useState(false); + const [explanation, setExplanation] = useState(null); + const [error, setError] = useState(null); + + const handleExplain = async () => { + setIsLoading(true); + setError(null); + setShowModal(true); + + try { + const response = await fetch('/api/explain', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type, + content, + context + }) + }); + + if (!response.ok) { + throw new Error('Failed to get explanation'); + } + + const data = await response.json(); + setExplanation(data); + } catch (err) { + console.error('Error getting explanation:', err); + setError('Failed to load explanation. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + const closeModal = () => { + setShowModal(false); + setExplanation(null); + setError(null); + }; + + const buttonSizes = { + small: { padding: '4px 8px', fontSize: '12px' }, + medium: { padding: '6px 12px', fontSize: '14px' }, + large: { padding: '8px 16px', fontSize: '16px' } + }; + + const buttonStyle = { + ...buttonSizes[size], + backgroundColor: '#3498DB', + color: 'white', + border: 'none', + borderRadius: '4px', + cursor: 'pointer', + display: 'inline-flex', + alignItems: 'center', + gap: '4px', + transition: 'background-color 0.2s', + ...style + }; + + const renderExplanation = () => { + if (error) { + return ( +
+ {error} +
+ ); + } + + if (isLoading) { + return ( +
+
+
Generating explanation...
+
+ ); + } + + if (!explanation) { + return null; + } + + // Render based on explanation type + switch (type) { + case 'config': + return ( +
+

+ {explanation.config_key || 'Configuration'} +

+ +
+ Current Value: + + {explanation.current_value} + +
+ +
+ What it does: +

{explanation.description}

+
+ + {explanation.example && ( +
+ Example: +

{explanation.example}

+
+ )} + + {explanation.value_analysis && ( +
+ Analysis: {explanation.value_analysis} +
+ )} + + {explanation.recommendations && explanation.recommendations.length > 0 && ( +
+ Recommendations: +
    + {explanation.recommendations.map((rec, i) => ( +
  • {rec}
  • + ))} +
+
+ )} + +
+ {explanation.requires_restart && ( +
⚠️ Changing this setting requires a restart
+ )} + {!explanation.safe_to_change && ( +
⚠️ Use caution when changing this setting
+ )} +
+
+ ); + + case 'error': + return ( +
+

+ Error Explanation +

+ +
+ Original Error: +
+ {explanation.original_error} +
+
+ +
+ What went wrong: +

{explanation.plain_english}

+
+ +
+ Likely causes: +
    + {explanation.likely_causes?.map((cause, i) => ( +
  • {cause}
  • + ))} +
+
+ +
+ 💡 How to fix it: +
    + {explanation.suggested_fixes?.map((fix, i) => ( +
  1. {fix}
  2. + ))} +
+
+ +
+ Severity: + {(explanation.severity || 'unknown').toUpperCase()} + +
+
+ ); + + case 'log': + return ( +
+

+ Log Entry Explanation +

+ +
+ {explanation.log_entry} +
+ +
+ Level: + + {explanation.log_level} + +
+ + {explanation.timestamp && ( +
+ Time: {explanation.timestamp} +
+ )} + +
+ What this means: +

{explanation.explanation}

+
+ + {explanation.action_needed && explanation.next_steps && explanation.next_steps.length > 0 && ( +
+ ⚠️ Action needed: +
    + {explanation.next_steps.map((step, i) => ( +
  • {step}
  • + ))} +
+
+ )} +
+ ); + + default: + return ( +
+
{explanation.explanation || 'No explanation available.'}
+
+ ); + } + }; + + return ( + <> + + + {showModal && ( +
+
e.stopPropagation()} + > +
+

Explanation

+ +
+ + {renderExplanation()} +
+
+ )} + + ); +}; + +export default ExplainButton; diff --git a/services/dashboard/GuidedWizard.jsx b/services/dashboard/GuidedWizard.jsx new file mode 100644 index 0000000..9f72fd9 --- /dev/null +++ b/services/dashboard/GuidedWizard.jsx @@ -0,0 +1,487 @@ +/** + * GuidedWizard Component + * Multi-step wizard for onboarding flows + * Types: create_operation, onboard_agent, run_scan, first_time_setup + */ + +import React, { useState, useEffect } from 'react'; + +const GuidedWizard = ({ + wizardType = 'first_time_setup', + onComplete, + onCancel, + initialData = {} +}) => { + const [currentStep, setCurrentStep] = useState(1); + const [formData, setFormData] = useState(initialData); + const [stepHelp, setStepHelp] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const wizardConfigs = { + create_operation: { + title: 'Create New Operation', + steps: [ + { + number: 1, + title: 'Operation Name and Type', + fields: [ + { name: 'operation_name', label: 'Operation Name', type: 'text', required: true, placeholder: 'Q4 Security Assessment' }, + { name: 'operation_type', label: 'Operation Type', type: 'select', required: true, options: [ + { value: 'external', label: 'External Penetration Test' }, + { value: 'internal', label: 'Internal Network Assessment' }, + { value: 'webapp', label: 'Web Application Test' }, + { value: 'wireless', label: 'Wireless Security Assessment' } + ]} + ] + }, + { + number: 2, + title: 'Define Target Scope', + fields: [ + { name: 'target_range', label: 'Target Network Range', type: 'text', required: true, placeholder: '192.168.1.0/24' }, + { name: 'excluded_hosts', label: 'Excluded Hosts (comma-separated)', type: 'text', placeholder: '192.168.1.1, 192.168.1.254' }, + { name: 'domains', label: 'Target Domains', type: 'textarea', placeholder: 'example.com\napp.example.com' } + ] + }, + { + number: 3, + title: 'Configure Assessment Tools', + fields: [ + { name: 'scan_intensity', label: 'Scan Intensity', type: 'select', required: true, options: [ + { value: '1', label: 'Stealth (Slowest, least detectable)' }, + { value: '3', label: 'Balanced (Recommended)' }, + { value: '5', label: 'Aggressive (Fastest, easily detected)' } + ]}, + { name: 'tools', label: 'Tools to Use', type: 'multiselect', options: [ + { value: 'nmap', label: 'Nmap (Network Scanning)' }, + { value: 'nikto', label: 'Nikto (Web Server Scanning)' }, + { value: 'gobuster', label: 'Gobuster (Directory Enumeration)' }, + { value: 'sqlmap', label: 'SQLMap (SQL Injection Testing)' } + ]} + ] + } + ] + }, + run_scan: { + title: 'Run Security Scan', + steps: [ + { + number: 1, + title: 'Select Scan Tool', + fields: [ + { name: 'tool', label: 'Security Tool', type: 'select', required: true, options: [ + { value: 'nmap', label: 'Nmap - Network Scanner' }, + { value: 'nikto', label: 'Nikto - Web Server Scanner' }, + { value: 'gobuster', label: 'Gobuster - Directory/File Discovery' }, + { value: 'sqlmap', label: 'SQLMap - SQL Injection' }, + { value: 'whatweb', label: 'WhatWeb - Technology Detection' } + ]} + ] + }, + { + number: 2, + title: 'Specify Target', + fields: [ + { name: 'target', label: 'Target', type: 'text', required: true, placeholder: '192.168.1.0/24 or example.com' }, + { name: 'ports', label: 'Ports (optional)', type: 'text', placeholder: '80,443,8080 or 1-1000' } + ] + }, + { + number: 3, + title: 'Scan Options', + fields: [ + { name: 'scan_type', label: 'Scan Type', type: 'select', required: true, options: [ + { value: 'quick', label: 'Quick Scan (Fast, common ports)' }, + { value: 'full', label: 'Full Scan (Comprehensive, slower)' }, + { value: 'stealth', label: 'Stealth Scan (Slow, harder to detect)' }, + { value: 'vuln', label: 'Vulnerability Scan (Checks for known vulns)' } + ]}, + { name: 'timeout', label: 'Timeout (seconds)', type: 'number', placeholder: '300' } + ] + } + ] + }, + first_time_setup: { + title: 'Welcome to StrikePackageGPT', + steps: [ + { + number: 1, + title: 'Welcome', + fields: [ + { name: 'user_name', label: 'Your Name', type: 'text', placeholder: 'John Doe' }, + { name: 'skill_level', label: 'Security Testing Experience', type: 'select', required: true, options: [ + { value: 'beginner', label: 'Beginner - Learning the basics' }, + { value: 'intermediate', label: 'Intermediate - Some experience' }, + { value: 'advanced', label: 'Advanced - Professional pentester' } + ]} + ] + }, + { + number: 2, + title: 'Configure LLM Provider', + fields: [ + { name: 'llm_provider', label: 'LLM Provider', type: 'select', required: true, options: [ + { value: 'ollama', label: 'Ollama (Local, Free)' }, + { value: 'openai', label: 'OpenAI (Cloud, Requires API Key)' }, + { value: 'anthropic', label: 'Anthropic Claude (Cloud, Requires API Key)' } + ]}, + { name: 'api_key', label: 'API Key (if using cloud provider)', type: 'password', placeholder: 'sk-...' } + ] + }, + { + number: 3, + title: 'Review and Finish', + fields: [] + } + ] + } + }; + + const config = wizardConfigs[wizardType] || wizardConfigs.first_time_setup; + const totalSteps = config.steps.length; + const currentStepConfig = config.steps[currentStep - 1]; + + useEffect(() => { + fetchStepHelp(); + }, [currentStep]); + + const fetchStepHelp = async () => { + try { + const response = await fetch(`/api/wizard/help?type=${wizardType}&step=${currentStep}`); + if (response.ok) { + const data = await response.json(); + setStepHelp(data); + } + } catch (err) { + console.error('Failed to fetch step help:', err); + } + }; + + const handleFieldChange = (fieldName, value) => { + setFormData(prev => ({ ...prev, [fieldName]: value })); + }; + + const validateCurrentStep = () => { + const requiredFields = currentStepConfig.fields.filter(f => f.required); + for (const field of requiredFields) { + if (!formData[field.name]) { + setError(`${field.label} is required`); + return false; + } + } + setError(null); + return true; + }; + + const handleNext = () => { + if (!validateCurrentStep()) return; + + if (currentStep < totalSteps) { + setCurrentStep(prev => prev + 1); + } else { + handleComplete(); + } + }; + + const handleBack = () => { + if (currentStep > 1) { + setCurrentStep(prev => prev - 1); + setError(null); + } + }; + + const handleComplete = async () => { + if (!validateCurrentStep()) return; + + setLoading(true); + try { + if (onComplete) { + await onComplete(formData); + } + } catch (err) { + setError('Failed to complete wizard: ' + err.message); + } finally { + setLoading(false); + } + }; + + const renderField = (field) => { + const commonStyle = { + width: '100%', + padding: '10px', + border: '1px solid #ddd', + borderRadius: '4px', + fontSize: '14px' + }; + + switch (field.type) { + case 'text': + case 'password': + case 'number': + return ( + handleFieldChange(field.name, e.target.value)} + placeholder={field.placeholder} + style={commonStyle} + /> + ); + + case 'textarea': + return ( +