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] 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

+ +
+
+ ⚠️ 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