├── schemas ├── ludus_ghosts_server.yaml ├── badsectorlabs.ludus_remnux.yaml ├── ludus_caldera_agent.yaml ├── 0xRedpoll.ludus_mythic_teamserver.yaml ├── aleemladha.ludus_wazuh_agent.yaml ├── ludus_juiceshop.yaml ├── badsectorlabs.ludus_flarevm.yaml ├── ludus_aurora_agent.yaml ├── badsectorlabs.ludus_commandovm.yaml ├── ludus_caldera_server.yaml ├── ludus_velociraptor_client.yaml ├── ludus_graylog_server.yaml ├── 0xRedpoll.ludus_cobaltstrike_teamserver.yaml ├── badsectorlabs.ludus_xz_backdoor.yaml ├── badsectorlabs.ludus_elastic_container.yaml ├── aleemladha.wazuh_server_install.yaml ├── aleemladha.ludus_exchange.yaml ├── bagelByt3s.ludus_adfs.yaml ├── ludus_velociraptor_server.yaml ├── badsectorlabs.ludus_vulhub.yaml ├── NocteDefensor.ludus_tailscale.yaml ├── ludus-local-users.yaml ├── curi0usjack.ludus_enable_mdi_gpo.yaml ├── badsectorlabs.ludus_bloodhound_ce.yaml ├── badsectorlabs.ludus_emux.yaml ├── curi0usjack.ludus_badblood.yaml ├── badsectorlabs.ludus_elastic_agent.yaml ├── ludus_child_domain_join.yaml ├── badsectorlabs.ludus_mssql.yaml ├── ludus-ad-content.yaml ├── Sample-schema.yaml ├── curi0usjack.ludus_enable_asr.yaml ├── ludus-gitlab-ce.yaml ├── mojeda101.ludus_veeam_vbr.yaml ├── mojeda101.ludus_adtimeline_syncthing.yaml ├── aleemladha.ludus_exchange2016.yaml ├── ludus_child_domain.yaml ├── ludus-ad-vulns.yaml ├── netpenguins.ludus_k3s.yaml ├── netpenguins.ludus_redirector.yaml ├── netpenguins.ludus_sliver.yaml ├── ludus_filigran_opencti.yaml ├── mojeda101.ludus_fake_configs.yaml ├── professor-moody.ludus_litterbox.yaml ├── badsectorlabs.ludus_adcs.yaml └── 5tuk0v.ludus_wsus.yaml ├── base-configs ├── adcs-lab.yml ├── adtimeline-splunk-lab.yml ├── Splunk-Attack-Range-Sample.yaml ├── malware-lab.yml ├── elastic-security.yml ├── veeam-backup-lab.yml ├── basic-ad-network.yml ├── k3s-cluster-lab.yml ├── parent-child-template-config.yml ├── ADFS-sample.yaml ├── fake-config-files-lab.yml ├── malware-analysis-lab.yml └── sccm-lab.yml ├── .gitignore ├── .npmignore ├── tsconfig.json ├── .eslintrc.js ├── package.json ├── CHANGELOG.md └── src ├── prompts ├── index.ts └── executeLudusCmd.ts ├── tools ├── getTags.ts ├── getRangeStatus.ts ├── rangeAbort.ts ├── ludusRolesSearch.ts ├── listAllUsers.ts ├── listUserRanges.ts ├── ludusNetworkingSearch.ts ├── destroyRange.ts ├── ludusCliExecute.ts ├── ludusHelp.ts ├── ludusPower.ts └── deployRange.ts ├── utils ├── logger.ts ├── keyring.ts ├── fileLogger.ts ├── downloadDocs.ts └── downloadBaseConfigs.ts └── ludusMCP └── config.ts /schemas/ludus_ghosts_server.yaml: -------------------------------------------------------------------------------- 1 | name: ludus_ghosts_server 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs GHOSTS server for activity simulation" 5 | repository: "https://github.com/frack113/ludus_ghosts_server" 6 | author: "frack113" 7 | installation_method: "ludus ansible role add ludus_ghosts_server" 8 | dependencies: [] 9 | 10 | variables: {} -------------------------------------------------------------------------------- /schemas/badsectorlabs.ludus_remnux.yaml: -------------------------------------------------------------------------------- 1 | name: badsectorlabs.ludus_remnux 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs REMnux on Linux systems" 5 | repository: "https://github.com/badsectorlabs/ludus_remnux" 6 | author: "Bad Sector Labs" 7 | installation_method: "ludus ansible role add badsectorlabs.ludus_remnux" 8 | dependencies: [] 9 | 10 | variables: {} -------------------------------------------------------------------------------- /schemas/ludus_caldera_agent.yaml: -------------------------------------------------------------------------------- 1 | name: ludus_caldera_agent 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs MITRE Caldera agent" 5 | repository: "https://github.com/frack113/ludus_caldera_agent" 6 | author: "frack113" 7 | installation_method: "ludus ansible role add ludus_caldera_agent" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_caldera_windows_agent_name: 12 | type: "string" 13 | default: "C:\\Users\\Public\\splunkd.exe" 14 | description: "Name of the Windows payload" -------------------------------------------------------------------------------- /schemas/0xRedpoll.ludus_mythic_teamserver.yaml: -------------------------------------------------------------------------------- 1 | name: 0xRedpoll.ludus_mythic_teamserver 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Mythic C2 team server" 5 | repository: "https://github.com/0xRedpoll/ludus_mythic_teamserver" 6 | author: "0xRedpoll" 7 | installation_method: "ludus ansible role add 0xRedpoll.ludus_mythic_teamserver" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_install_directory: 12 | type: "string" 13 | default: "/opt/ludus" 14 | description: "Ludus installation directory" -------------------------------------------------------------------------------- /schemas/aleemladha.ludus_wazuh_agent.yaml: -------------------------------------------------------------------------------- 1 | name: aleemladha.ludus_wazuh_agent 2 | type: role 3 | version: "1.0.0" 4 | description: "Deploys Wazuh Agents to Windows, Debian, and Ubuntu systems" 5 | repository: "https://github.com/aleemladha/ludus_wazuh_agent" 6 | author: "Aleem Ladha (@LadhaAleem)" 7 | installation_method: "ludus ansible role add aleemladha.ludus_exchange2016" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_wazuh_siem_server: 12 | type: "string" 13 | required: true 14 | default: "" 15 | description: "IP address of Wazuh SIEM server (use 'ludus range status' to get IP)" -------------------------------------------------------------------------------- /schemas/ludus_juiceshop.yaml: -------------------------------------------------------------------------------- 1 | name: ludus_juiceshop 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs OWASP Juice Shop vulnerable web application" 5 | repository: "https://github.com/xurger/ludus_juiceshop" 6 | author: "xurger" 7 | installation_method: "ludus ansible role add -d /path/to/directory" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_juiceshop_port: 12 | type: "integer" 13 | default: 80 14 | description: "Port for Juice Shop to listen on" 15 | 16 | ludus_juiceshop_user: 17 | type: "string" 18 | default: "root" 19 | description: "User to run Juice Shop as" -------------------------------------------------------------------------------- /base-configs/adcs-lab.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://docs.ludus.cloud/schemas/range-config.json 2 | # Active Directory Certificate Services Lab 3 | # DC with ADCS role for certificate authority attack paths (ESC1-15) 4 | 5 | ludus: 6 | - vm_name: "{{ range_id }}-ad-dc-win2022-server-x64-1" 7 | hostname: "{{ range_id }}-DC01-2022" 8 | template: win2022-server-x64-template 9 | vlan: 10 10 | ip_last_octet: 11 11 | ram_gb: 6 12 | cpus: 4 13 | windows: 14 | sysprep: true 15 | domain: 16 | fqdn: ludus.domain 17 | role: primary-dc 18 | roles: 19 | - badsectorlabs.ludus_adcs -------------------------------------------------------------------------------- /schemas/badsectorlabs.ludus_flarevm.yaml: -------------------------------------------------------------------------------- 1 | name: badsectorlabs.ludus_flarevm 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs FlareVM on Windows systems" 5 | repository: "https://github.com/badsectorlabs/ludus_flarevm" 6 | author: "Bad Sector Labs" 7 | installation_method: "ludus ansible role add badsectorlabs.ludus_flarevm" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_flarevm_use_flarevm_wallpaper: 12 | type: "boolean" 13 | default: true 14 | description: "Whether to use FlareVM wallpaper" 15 | 16 | ludus_flarevm_config_file: 17 | type: "string" 18 | default: "https://raw.githubusercontent.com/mandiant/flare-vm/refs/heads/main/config.xml" 19 | description: "URL to FlareVM configuration file" -------------------------------------------------------------------------------- /schemas/ludus_aurora_agent.yaml: -------------------------------------------------------------------------------- 1 | name: ludus_aurora_agent 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Aurora EDR agent" 5 | repository: "https://github.com/frack113/ludus_aurora_agent" 6 | author: "frack113" 7 | installation_method: "ludus ansible role add ludus_aurora_agent" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_aurora_dashboard: 12 | type: "boolean" 13 | default: false 14 | description: "Whether to install the dashboard" 15 | 16 | ludus_aurora_udp_target: 17 | type: "string" 18 | default: "" 19 | description: "UDP target for sending logs to SIEM" 20 | 21 | ludus_aurora_udp_format: 22 | type: "string" 23 | default: "" 24 | description: "UDP format for SIEM logs" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build outputs 8 | dist/ 9 | build/ 10 | *.tsbuildinfo 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage/ 20 | *.lcov 21 | 22 | # Logs 23 | logs 24 | *.log 25 | 26 | # Environment variables 27 | .env 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # IDE files 34 | .vscode/ 35 | .idea/ 36 | *.swp 37 | *.swo 38 | *~ 39 | 40 | # OS generated files 41 | .DS_Store 42 | .DS_Store? 43 | ._* 44 | .Spotlight-V100 45 | .Trashes 46 | ehthumbs.db 47 | Thumbs.db 48 | 49 | # Temporary files 50 | *.tmp 51 | *.temp 52 | *.bak 53 | -------------------------------------------------------------------------------- /schemas/badsectorlabs.ludus_commandovm.yaml: -------------------------------------------------------------------------------- 1 | name: badsectorlabs.ludus_commandovm 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs CommandoVM on Windows systems" 5 | repository: "https://github.com/badsectorlabs/ludus_commandovm" 6 | author: "Bad Sector Labs" 7 | installation_method: "ludus ansible role add badsectorlabs.ludus_commandovm" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_commandovm_use_commandovm_wallpaper: 12 | type: "boolean" 13 | default: true 14 | description: "Whether to use CommandoVM wallpaper" 15 | 16 | ludus_commandovm_nopassword: 17 | type: "boolean" 18 | default: true 19 | description: "Whether to disable password requirement" 20 | 21 | ludus_commandovm_password: 22 | type: "string" 23 | default: null 24 | description: "Optional password for CommandoVM user" -------------------------------------------------------------------------------- /schemas/ludus_caldera_server.yaml: -------------------------------------------------------------------------------- 1 | name: ludus_caldera_server 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs MITRE Caldera server" 5 | repository: "https://github.com/frack113/ludus_caldera_server" 6 | author: "frack113" 7 | installation_method: "ludus ansible role add ludus_caldera_server" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_caldera_git: 12 | type: "string" 13 | default: "b24f6e7a99cab19cbf417009bda9b9c6c81abc31" 14 | description: "SHA-1 hash of the commit" 15 | 16 | ludus_caldera_admin_pwd: 17 | type: "string" 18 | default: "admin" 19 | description: "Admin user password" 20 | 21 | ludus_caldera_red_pwd: 22 | type: "string" 23 | default: "red" 24 | description: "Red team user password" 25 | 26 | ludus_caldera_blue_pwd: 27 | type: "string" 28 | default: "blue" 29 | description: "Blue team user password" -------------------------------------------------------------------------------- /schemas/ludus_velociraptor_client.yaml: -------------------------------------------------------------------------------- 1 | name: ludus_velociraptor_client 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Velociraptor client agent" 5 | repository: "https://github.com/fmurer/ludus_velociraptor_client" 6 | author: "fmurer" 7 | installation_method: "ludus ansible role add -d /path/to/directory" 8 | dependencies: [] 9 | 10 | variables: 11 | velociraptor_version: 12 | type: "string" 13 | default: "v0.72" 14 | description: "Velociraptor version" 15 | 16 | velociraptor_version_patch: 17 | type: "string" 18 | default: "v0.72.0" 19 | description: "Velociraptor version with patch level" 20 | 21 | velociraptor_client_config: 22 | type: "string" 23 | default: "" 24 | description: "Client configuration content" 25 | 26 | velociraptor_install_path: 27 | type: "string" 28 | default: "/opt/velociraptor" 29 | description: "Install path on Velociraptor server - used to get client config" -------------------------------------------------------------------------------- /schemas/ludus_graylog_server.yaml: -------------------------------------------------------------------------------- 1 | name: ludus_graylog_server 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Graylog server for log management" 5 | repository: "https://github.com/frack113/ludus_graylog_server" 6 | author: "frack113" 7 | installation_method: "ludus ansible role add -d /path/to/directory" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_graylog_opensearch_admin_password: 12 | type: "string" 13 | default: "pdMDL4!5ELcBSWqu" 14 | description: "OpenSearch admin password" 15 | 16 | ludus_graylog_svr_password_secret: 17 | type: "string" 18 | default: "r9t-HW87CV3OJysF1BvVBmkZCvpFdGgLsXCkf8NUbUZSZnW2lvdwraeUdP1y4eP8wbOd2LizMjSvMiJPWsg4VcAufhij809c" 19 | description: "Graylog server password secret" 20 | 21 | ludus_graylog_svr_root_password_sha2: 22 | type: "string" 23 | default: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" 24 | description: "Graylog server root password SHA2 hash" -------------------------------------------------------------------------------- /schemas/0xRedpoll.ludus_cobaltstrike_teamserver.yaml: -------------------------------------------------------------------------------- 1 | name: 0xRedpoll.ludus_cobaltstrike_teamserver 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Cobalt Strike team server" 5 | repository: "https://github.com/0xRedpoll/ludus_cobaltstrike_teamserver" 6 | author: "0xRedpoll" 7 | installation_method: "ludus ansible role add 0xRedpoll.ludus_cobaltstrike_teamserver" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_install_directory: 12 | type: "string" 13 | default: "/opt/ludus" 14 | description: "Ludus installation directory" 15 | 16 | ludus_cobaltstrike_teamserver_password: 17 | type: "string" 18 | default: "0xRedpoll" 19 | description: "Team server password" 20 | 21 | ludus_cobaltstrike_c2_profile: 22 | type: "string" 23 | default: "" 24 | description: "C2 profile path" 25 | 26 | ludus_cobaltstrike_license: 27 | type: "string" 28 | default: "0000-0000-0000-0000" 29 | description: "Cobalt Strike license key" -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Development and build files 2 | node_modules/ 3 | .git/ 4 | .gitignore 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Dynamic content (downloaded from GitHub) 11 | schemas/ 12 | base-configs/ 13 | 14 | # Build output (will be generated by prepare script) 15 | dist/ 16 | 17 | # IDE and editor files 18 | .vscode/ 19 | .idea/ 20 | *.swp 21 | *.swo 22 | *~ 23 | .DS_Store 24 | Thumbs.db 25 | 26 | # Testing files 27 | test/ 28 | tests/ 29 | coverage/ 30 | *.test.js 31 | *.test.ts 32 | *.spec.js 33 | *.spec.ts 34 | jest.config.js 35 | 36 | # Development configuration 37 | .eslintrc.js 38 | .prettierrc* 39 | 40 | # Temporary files 41 | .tmp/ 42 | temp/ 43 | *.tsbuildinfo 44 | 45 | # Documentation (keep README.md and LICENSE) 46 | docs/ 47 | wiki/ 48 | 49 | # Keep these files for source-only package: 50 | # - src/ (TypeScript source) 51 | # - package.json 52 | # - package-lock.json 53 | # - tsconfig.json (needed for compilation) 54 | # - README.md 55 | # - LICENSE -------------------------------------------------------------------------------- /schemas/badsectorlabs.ludus_xz_backdoor.yaml: -------------------------------------------------------------------------------- 1 | name: badsectorlabs.ludus_xz_backdoor 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs the xz backdoor (CVE-2024-3094) on a Debian host and optionally installs the xzbot tool" 5 | repository: "https://github.com/badsectorlabs/ludus_xz_backdoor" 6 | author: "Bad Sector Labs" 7 | installation_method: "ludus ansible role add badsectorlabs.ludus_xz_backdoor" 8 | dependencies: [] 9 | 10 | warning: "This role deploys malware on purpose! Be careful." 11 | 12 | variables: 13 | ludus_xz_backdoor_install_xzbot: 14 | type: "boolean" 15 | default: true 16 | description: "Install the xzbot cli tool used to send commands to the backdoor" 17 | 18 | ludus_xz_backdoor_install_backdoor: 19 | type: "boolean" 20 | default: true 21 | description: "Install the xz backdoor library and reboot" 22 | 23 | ludus_xz_backdoor_uninstall_backdoor: 24 | type: "boolean" 25 | default: false 26 | description: "Remove the backdoor by replacing the symlink and rebooting" -------------------------------------------------------------------------------- /schemas/badsectorlabs.ludus_elastic_container.yaml: -------------------------------------------------------------------------------- 1 | name: badsectorlabs.ludus_elastic_container 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Elastic Stack using containers" 5 | repository: "https://github.com/badsectorlabs/ludus_elastic_container" 6 | author: "Bad Sector Labs" 7 | installation_method: "ludus ansible role add badsectorlabs.ludus_elastic_container" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_elastic_container_install_path: 12 | type: "string" 13 | default: "/opt/elastic_container" 14 | description: "Installation path for Elastic container" 15 | 16 | ludus_elastic_password: 17 | type: "string" 18 | default: "elasticpassword" 19 | description: "Password for Elastic user" 20 | 21 | ludus_elastic_stack_version: 22 | type: "string" 23 | default: "8.12.2" 24 | description: "Elastic Stack version" 25 | 26 | ludus_elastic_container_branch: 27 | type: "string" 28 | default: "05c0b91a36a0918d095c28295a9c64a9def275f5" 29 | description: "Known good commit, 2024-07-03" -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "lib": ["ES2020"], 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "declaration": true, 15 | "declarationMap": true, 16 | "sourceMap": true, 17 | "removeComments": false, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": false, 22 | "noUnusedParameters": false, 23 | "exactOptionalPropertyTypes": true, 24 | "resolveJsonModule": true, 25 | "isolatedModules": true, 26 | "allowJs": false, 27 | "checkJs": false, 28 | "types": ["node"] 29 | }, 30 | "include": [ 31 | "src/**/*" 32 | ], 33 | "exclude": [ 34 | "node_modules", 35 | "dist" 36 | ], 37 | "ts-node": { 38 | "esm": true 39 | } 40 | } -------------------------------------------------------------------------------- /schemas/aleemladha.wazuh_server_install.yaml: -------------------------------------------------------------------------------- 1 | name: aleemladha.wazuh_server_install 2 | type: role 3 | version: "1.0.0" 4 | description: "Installing wazuh SIEM Unified XDR and SIEM protection with SOC Fortress Rules" 5 | repository: "https://github.com/aleemladha/wazuh_server_install" 6 | author: "Aleem Ladha (@LadhaAleem)" 7 | installation_method: "ludus ansible role add aleemladha.wazuh_server_install" 8 | dependencies: [] 9 | 10 | note: "Only works with Ubuntu or Kali - Wazuh does not support Debian 11/12" 11 | 12 | variables: 13 | wazuh_install_script_url: 14 | type: "string" 15 | default: "https://packages.wazuh.com/4.7/wazuh-install.sh" 16 | description: "Wazuh installation script URL" 17 | 18 | socfortress_rules_script_url: 19 | type: "string" 20 | default: "https://raw.githubusercontent.com/aaladha/Wazuh-Rules/main/wazuh_socfortress_rules.sh" 21 | description: "SOCFORTRESS Wazuh rules script URL" 22 | 23 | wazuh_admin_password: 24 | type: "string" 25 | default: "Wazuh-123" 26 | description: "Optional force admin password (auto-generated if not set)" -------------------------------------------------------------------------------- /base-configs/adtimeline-splunk-lab.yml: -------------------------------------------------------------------------------- 1 | ludus: 2 | - vm_name: "{{ range_id }}-adtimeline-host" 3 | hostname: "{{ range_id }}-adtimeline" 4 | template: debian-12-x64-server-template 5 | vlan: 10 6 | ip_last_octet: 11 7 | ram_gb: 8 8 | cpus: 4 9 | linux: true 10 | testing: 11 | snapshot: true 12 | block_internet: false 13 | roles: 14 | - geerlingguy.docker 15 | - mojeda101.ludus_adtimeline_syncthing 16 | role_vars: 17 | # Docker configuration for geerlingguy.docker role 18 | docker_edition: ce 19 | docker_users: 20 | - debian 21 | 22 | # Syncthing configuration 23 | ludus_adtimeline_sync_puid: 1000 24 | ludus_adtimeline_sync_pgid: 1000 25 | ludus_adtimeline_sync_tz: "UTC" 26 | ludus_adtimeline_sync_gui_port: 8384 27 | ludus_adtimeline_sync_port_tcp: 22000 28 | ludus_adtimeline_sync_port_udp: 22000 29 | ludus_adtimeline_sync_discovery_port: 21027 30 | 31 | # Splunk configuration 32 | ludus_adtimeline_splunk_web_port: 8000 33 | ludus_adtimeline_splunk_password: "forensics" 34 | -------------------------------------------------------------------------------- /schemas/aleemladha.ludus_exchange.yaml: -------------------------------------------------------------------------------- 1 | name: aleemladha.ludus_exchange 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Microsoft Exchange Server 2019" 5 | repository: "https://github.com/aleemladha/ludus_exchange" 6 | author: "Aleem Ladha (@LadhaAleem)" 7 | installation_method: "ludus ansible role add aleemladha.ludus_exchange" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_exchange_domain: 12 | type: "string" 13 | default: "{{ auto-detected }}" 14 | description: "Auto-detected domain name from Ludus config" 15 | 16 | ludus_exchange_dc: 17 | type: "string" 18 | default: "{{ auto-detected }}" 19 | description: "Auto-detected primary DC hostname" 20 | 21 | ludus_exchange_host: 22 | type: "string" 23 | default: "{{ auto-detected }}" 24 | description: "Auto-detected host from Ludus config" 25 | 26 | ludus_exchange_domain_username: 27 | type: "string" 28 | default: "{{ auto-detected }}" 29 | description: "Domain admin username" 30 | 31 | ludus_exchange_domain_password: 32 | type: "string" 33 | default: "{{ auto-detected }}" 34 | description: "Domain admin password" -------------------------------------------------------------------------------- /schemas/bagelByt3s.ludus_adfs.yaml: -------------------------------------------------------------------------------- 1 | name: bagelByt3s.ludus_adfs 2 | type: collection 3 | version: "1.0.0" 4 | description: "Collection for installing and configuring ADFS" 5 | repository: "https://github.com/bagelByt3s/ludus_adfs" 6 | author: "bagelByt3s" 7 | installation_method: "ludus ansible collection add bagelByt3s.ludus_adfs" 8 | dependencies: [] 9 | 10 | roles: 11 | install_adfs: 12 | variables: 13 | ludus_install_directory: 14 | type: "string" 15 | default: "/opt/ludus" 16 | description: "Ludus installation directory" 17 | 18 | install_adcs: 19 | variables: {} 20 | 21 | import_root_cert: 22 | variables: 23 | ludus_install_directory: 24 | type: "string" 25 | default: "/opt/ludus" 26 | description: "Ludus installation directory" 27 | 28 | adfs_CA: 29 | type: "string" 30 | default: "ludus-CA" 31 | description: "ADFS Certificate Authority name" 32 | 33 | entra_prep: 34 | variables: 35 | ludus_install_directory: 36 | type: "string" 37 | default: "/opt/ludus" 38 | description: "Ludus installation directory" -------------------------------------------------------------------------------- /schemas/ludus_velociraptor_server.yaml: -------------------------------------------------------------------------------- 1 | name: ludus_velociraptor_server 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Velociraptor server" 5 | repository: "https://github.com/fmurer/ludus_velociraptor_server" 6 | author: "fmurer" 7 | installation_method: "ludus ansible role add -d /path/to/directory" 8 | dependencies: [] 9 | 10 | variables: 11 | velociraptor_version: 12 | type: "string" 13 | default: "v0.72" 14 | description: "Velociraptor version" 15 | 16 | velociraptor_version_patch: 17 | type: "string" 18 | default: "v0.72.0" 19 | description: "Velociraptor version with patch level" 20 | 21 | velociraptor_admin_user: 22 | type: "string" 23 | default: "admin" 24 | description: "Admin username" 25 | 26 | velociraptor_admin_password: 27 | type: "string" 28 | default: "pleasechangeme" 29 | description: "Admin password" 30 | 31 | velociraptor_host: 32 | type: "string" 33 | default: "{{ ansible_default_ipv4.address }}" 34 | description: "Server host IP address" 35 | 36 | velociraptor_install_path: 37 | type: "string" 38 | default: "/opt/velociraptor" 39 | description: "Installation path" -------------------------------------------------------------------------------- /base-configs/Splunk-Attack-Range-Sample.yaml: -------------------------------------------------------------------------------- 1 | # sample splunk attack range config requires p4t12ick.ludus_ar_splunk,p4t12ick.ludus_ar_windows,p4t12ick.ludus_ar_linux roles 2 | ludus: 3 | - vm_name: "{{ range_id }}-ar-splunk" 4 | hostname: "{{ range_id }}-ar-splunk" 5 | template: ubuntu-22.04-x64-server-template 6 | vlan: 20 7 | ip_last_octet: 1 8 | ram_gb: 16 9 | cpus: 8 10 | linux: true 11 | roles: 12 | - p4t12ick.ludus_ar_splunk 13 | 14 | - vm_name: "{{ range_id }}-ar-windows" 15 | hostname: "{{ range_id }}-ar-windows" 16 | template: win2022-server-x64-template 17 | vlan: 20 18 | ip_last_octet: 3 19 | ram_gb: 8 20 | cpus: 4 21 | windows: 22 | sysprep: false 23 | roles: 24 | - p4t12ick.ludus_ar_windows 25 | role_vars: 26 | ludus_ar_windows_splunk_ip: "10.2.20.1" 27 | 28 | - vm_name: "{{ range_id }}-ar-linux" 29 | hostname: "{{ range_id }}-ar-linux" 30 | template: ubuntu-22.04-x64-server-template 31 | vlan: 20 32 | ip_last_octet: 2 33 | ram_gb: 8 34 | cpus: 4 35 | linux: true 36 | roles: 37 | - p4t12ick.ludus_ar_linux 38 | role_vars: 39 | ludus_ar_linux_splunk_ip: "10.2.20.1" 40 | -------------------------------------------------------------------------------- /schemas/badsectorlabs.ludus_vulhub.yaml: -------------------------------------------------------------------------------- 1 | name: badsectorlabs.ludus_vulhub 2 | type: role 3 | version: "1.0.0" 4 | description: "Runs Vulhub environments on a Linux system" 5 | repository: "https://github.com/badsectorlabs/ludus_vulhub" 6 | author: "Bad Sector Labs" 7 | installation_method: "ludus ansible role add badsectorlabs.ludus_vulhub" 8 | dependencies: 9 | - "geerlingguy.docker" 10 | 11 | variables: 12 | vulhub_git_url: 13 | type: "string" 14 | default: "https://github.com/vulhub/vulhub" 15 | description: "URL to clone Vulhub repository from" 16 | 17 | vulhub_install_path: 18 | type: "string" 19 | default: "/opt/vulhub" 20 | description: "Installation path for Vulhub" 21 | 22 | vulhub_branch: 23 | type: "string" 24 | default: "master" 25 | description: "Git branch to checkout" 26 | 27 | vulhub_envs: 28 | type: "array" 29 | required: true 30 | default: [] 31 | description: "Array of vulhub environments to deploy" 32 | example: 33 | - "confluence/CVE-2023-22527" 34 | - "airflow/CVE-2020-11978" 35 | 36 | vulhub_persistent: 37 | type: "boolean" 38 | default: true 39 | description: "Whether environments persist across reboots" -------------------------------------------------------------------------------- /base-configs/malware-lab.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://docs.ludus.cloud/schemas/range-config.json 2 | # Malware Analysis Lab with XZ Backdoor 3 | # Isolated environment with analysis tools (REMnux + FlareVM) 4 | 5 | ludus: 6 | - vm_name: "{{ range_id }}-xz-backdoor" 7 | hostname: "{{ range_id }}-xz-backdoor" 8 | template: debian-12-x64-server-template 9 | vlan: 99 10 | ip_last_octet: 1 11 | ram_gb: 2 12 | cpus: 2 13 | linux: true 14 | testing: 15 | snapshot: true 16 | block_internet: true 17 | 18 | - vm_name: "{{ range_id }}-REMnux" 19 | hostname: "{{ range_id }}-REMnux" 20 | template: ubuntu-20.04-x64-server-template 21 | vlan: 99 22 | ip_last_octet: 2 23 | ram_gb: 4 24 | cpus: 2 25 | linux: true 26 | testing: 27 | snapshot: true 28 | block_internet: true 29 | roles: 30 | - badsectorlabs.ludus_remnux 31 | 32 | - vm_name: "{{ range_id }}-flare" 33 | hostname: "{{ range_id }}-FLARE" 34 | template: win11-22h2-x64-enterprise-template 35 | vlan: 99 36 | ip_last_octet: 3 37 | ram_gb: 4 38 | cpus: 2 39 | windows: 40 | install_additional_tools: false 41 | testing: 42 | snapshot: true 43 | block_internet: true 44 | roles: 45 | - badsectorlabs.ludus_flarevm -------------------------------------------------------------------------------- /schemas/NocteDefensor.ludus_tailscale.yaml: -------------------------------------------------------------------------------- 1 | NocteDefensor.ludus_tailscale: 2 | name: "NocteDefensor.ludus_tailscale" 3 | type: "role" 4 | version: "1.0.0" 5 | description: "Installs and configures Tailscale VPN" 6 | repository: "https://github.com/NocteDefensor/ludus_tailscale" 7 | author: "NocteDefensor" 8 | installation_method: "ludus ansible role add NocteDefensor.ludus_tailscale" 9 | dependencies: [] 10 | installation_note: "Can also be installed as 'ludus_tailscale' using local directory - user should verify which role name they have installed" 11 | variables: 12 | tailscale_state: 13 | type: "string" 14 | default: "present" 15 | description: "State of Tailscale installation" 16 | valid_options: 17 | - "present" 18 | - "absent" 19 | tailscale_authkey: 20 | type: "string" 21 | default: "tskey-auth-" 22 | description: "Tailscale authentication key" 23 | tailscale_api_key: 24 | type: "string" 25 | default: "tskey-api-" 26 | description: "Tailscale API key" 27 | tailscale_ssh: 28 | type: "boolean" 29 | default: false 30 | description: "Enable/disable Tailscale SSH access" 31 | tailscale_dns: 32 | type: "boolean" 33 | default: false 34 | description: "Enable/disable Tailscale DNS configuration" 35 | -------------------------------------------------------------------------------- /schemas/ludus-local-users.yaml: -------------------------------------------------------------------------------- 1 | name: ludus-local-users 2 | type: role 3 | version: "1.0.0" 4 | description: "Creates and manages local users and groups" 5 | repository: "https://github.com/Cyblex-Consulting/ludus-local-users" 6 | author: "Cyblex-Consulting" 7 | installation_method: "ludus ansible role add -d /path/to/directory" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_local_users.login: 12 | type: "string" 13 | default: "jdoe" 14 | description: "User login name" 15 | 16 | ludus_local_users.password: 17 | type: "string" 18 | default: "aA8MaQBCtBtPYAFh" 19 | description: "User password" 20 | 21 | ludus_local_users.groups: 22 | type: "string" 23 | default: "sudo,adm" 24 | description: "Groups to put user into (Linux only, comma separated)" 25 | 26 | ludus_local_users.sudo_nopasswd: 27 | type: "boolean" 28 | default: true 29 | description: "Whether to set NOPASSWD in sudoers (Linux only)" 30 | 31 | ludus_local_users.name: 32 | type: "string" 33 | default: "jdoe" 34 | description: "Group to modify (Windows only)" 35 | 36 | ludus_local_users.members: 37 | type: "array" 38 | default: 39 | - "my_local_user" 40 | - "MYRANGE\\Developers" 41 | - "MYRANGE\\Tier 1 Admins" 42 | description: "List of accounts or groups to add to local group (Windows only)" -------------------------------------------------------------------------------- /base-configs/elastic-security.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://docs.ludus.cloud/schemas/range-config.json 2 | # Elastic Security Lab 3 | # Elastic server with agents on Windows and Linux endpoints 4 | 5 | ludus: 6 | - vm_name: "{{ range_id }}-elastic" 7 | hostname: "{{ range_id }}-elastic" 8 | template: debian-12-x64-server-template 9 | vlan: 20 10 | ip_last_octet: 1 11 | ram_gb: 8 12 | cpus: 4 13 | linux: true 14 | testing: 15 | snapshot: false 16 | block_internet: false 17 | roles: 18 | - badsectorlabs.ludus_elastic_container 19 | role_vars: 20 | ludus_elastic_password: "thisisapassword" 21 | 22 | - vm_name: "{{ range_id }}-debian" 23 | hostname: "{{ range_id }}-debian" 24 | template: debian-12-x64-server-template 25 | vlan: 20 26 | ip_last_octet: 20 27 | ram_gb: 4 28 | cpus: 2 29 | linux: true 30 | testing: 31 | snapshot: false 32 | block_internet: false 33 | roles: 34 | - badsectorlabs.ludus_elastic_agent 35 | 36 | - vm_name: "{{ range_id }}-win11-22h2-enterprise-x64-1" 37 | hostname: "{{ range_id }}-WIN11-22H2-1" 38 | template: win11-22h2-x64-enterprise-template 39 | vlan: 10 40 | ip_last_octet: 21 41 | ram_gb: 8 42 | cpus: 4 43 | windows: 44 | install_additional_tools: false 45 | roles: 46 | - badsectorlabs.ludus_elastic_agent -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'eslint:recommended', 5 | '@typescript-eslint/recommended', 6 | ], 7 | plugins: ['@typescript-eslint'], 8 | parserOptions: { 9 | ecmaVersion: 2020, 10 | sourceType: 'module', 11 | project: './tsconfig.json', 12 | }, 13 | rules: { 14 | // Prefer explicit types for tool parameters and responses 15 | '@typescript-eslint/explicit-function-return-type': 'warn', 16 | '@typescript-eslint/explicit-module-boundary-types': 'warn', 17 | 18 | // Allow unused vars with underscore prefix (common in MCP tools) 19 | '@typescript-eslint/no-unused-vars': ['error', { 20 | 'argsIgnorePattern': '^_', 21 | 'varsIgnorePattern': '^_' 22 | }], 23 | 24 | // Relaxed rules for CLI wrapper flexibility 25 | '@typescript-eslint/no-explicit-any': 'warn', 26 | '@typescript-eslint/ban-ts-comment': 'warn', 27 | 28 | // Enforce consistent code style 29 | 'semi': ['error', 'always'], 30 | 'quotes': ['error', 'single'], 31 | 'indent': ['error', 2], 32 | 'comma-dangle': ['error', 'always-multiline'], 33 | 34 | // Allow console.log for development 35 | 'no-console': 'warn', 36 | }, 37 | env: { 38 | node: true, 39 | es2020: true, 40 | jest: true, 41 | }, 42 | ignorePatterns: ['dist/', 'node_modules/', '*.js'], 43 | }; -------------------------------------------------------------------------------- /schemas/curi0usjack.ludus_enable_mdi_gpo.yaml: -------------------------------------------------------------------------------- 1 | # Schema for ludus_enable_mdi_gpo Role 2 | # Enables Microsoft Defender for Identity auditing settings via Group Policy 3 | 4 | name: curi0usjack.ludus_enable_mdi_gpo 5 | type: role 6 | version: "1.0.0" 7 | description: "Creates GPOs with recommended auditing settings for Microsoft Defender for Identity (MDI) rollout" 8 | repository: "https://github.com/curi0usJack/Ludus-MDE-MDI-Roles/tree/main/ludus_enable_mdi_gpo" 9 | author: "curi0usJack (@curi0usJack)" 10 | installation_method: "ludus ansible role add -d /path/to/directory" 11 | note: "This role requires a Windows Domain Controller with Active Directory module available. Installs DefenderForIdentity PowerShell module." 12 | warning: "This role modifies domain-wide Group Policy settings." 13 | 14 | # This role has no configurable variables - it automatically enables predefined MDI configurations 15 | variables: {} 16 | 17 | # MDI Configurations enabled by this role: 18 | # - AdvancedAuditPolicyDCs: Advanced audit policy settings for Domain Controllers 19 | # - DomainObjectAuditing: Auditing for domain object changes 20 | # - NTLMAuditing: NTLM authentication auditing 21 | # 22 | # Additional configurations available via Set-MDIConfiguration: 23 | # See: https://learn.microsoft.com/en-us/powershell/module/defenderforidentity/set-mdiconfiguration 24 | # 25 | # Role performs these actions: 26 | # 1. Installs NuGet package provider 27 | # 2. Installs DefenderForIdentity PowerShell module 28 | # 3. Imports Active Directory module 29 | # 4. Creates GPOs with MDI prefix using Set-MDIConfiguration 30 | # 5. Forces Group Policy update with gpupdate /force 31 | -------------------------------------------------------------------------------- /schemas/badsectorlabs.ludus_bloodhound_ce.yaml: -------------------------------------------------------------------------------- 1 | name: badsectorlabs.ludus_bloodhound_ce 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Bloodhound CE on a Debian based system" 5 | repository: "https://github.com/badsectorlabs/ludus_bloodhound_ce" 6 | author: "Bad Sector Labs" 7 | installation_method: "ludus ansible role add badsectorlabs.ludus_bloodhound_ce" 8 | dependencies: 9 | - "geerlingguy.docker" 10 | 11 | variables: 12 | ludus_bloodhound_ce_install_path: 13 | type: "string" 14 | default: "/opt/bloodhound" 15 | description: "Path where docker-compose.yml and admin creds are placed" 16 | 17 | ludus_bloodhound_listen_only_localhost: 18 | type: "boolean" 19 | default: false 20 | description: "Expose bloodhound web UI to 0.0.0.0:8080 if set to false" 21 | 22 | ludus_bloodhound_port: 23 | type: "string" 24 | default: "8080" 25 | description: "The port bloodhound CE listens on" 26 | 27 | ludus_bloodhound_admin_password: 28 | type: "string" 29 | default: "bloodhoundpassword" 30 | description: "The default admin password for bloodhound" 31 | 32 | ludus_bloodhound_admin_principal_name: 33 | type: "string" 34 | default: "admin" 35 | description: "Admin principal name" 36 | 37 | ludus_bloodhound_admin_email_address: 38 | type: "string" 39 | default: "admin@ludus.domain" 40 | description: "Admin email address" 41 | 42 | ludus_bloodhound_admin_first_name: 43 | type: "string" 44 | default: "Bloodhound" 45 | description: "Admin first name" 46 | 47 | ludus_bloodhound_admin_last_name: 48 | type: "string" 49 | default: "Admin" 50 | description: "Admin last name" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ludus-mcp", 3 | "version": "1.0.24", 4 | "type": "module", 5 | "description": "MCP server for managing Ludus cybersecurity training environments through natural language commands", 6 | "main": "dist/server.js", 7 | "bin": "./dist/server.js", 8 | "scripts": { 9 | "build": "tsc", 10 | "dev": "tsx src/server.ts", 11 | "start": "node dist/server.js", 12 | "lint": "eslint src/**/*.ts", 13 | "clean": "rm -rf dist", 14 | "postinstall": "npm run build", 15 | "inspector": "npx @modelcontextprotocol/inspector dist/server.js" 16 | }, 17 | "keywords": [ 18 | "mcp", 19 | "model-context-protocol", 20 | "ludus", 21 | "cybersecurity", 22 | "training", 23 | "virtualization", 24 | "cli-wrapper" 25 | ], 26 | "author": "Ludus MCP Contributors", 27 | "license": "GPL-3.0", 28 | "dependencies": { 29 | "@modelcontextprotocol/sdk": "^0.6.0", 30 | "@types/js-yaml": "^4.0.9", 31 | "@types/node": "^20.0.0", 32 | "@types/ssh2": "^1.15.5", 33 | "ajv": "^8.17.1", 34 | "ajv-draft-04": "^1.0.0", 35 | "ajv-formats": "^3.0.1", 36 | "js-yaml": "^4.1.0", 37 | "keytar": "^7.9.0", 38 | "ssh2": "^1.16.0", 39 | "typescript": "^5.0.0" 40 | }, 41 | "devDependencies": { 42 | "@typescript-eslint/eslint-plugin": "^6.0.0", 43 | "@typescript-eslint/parser": "^6.0.0", 44 | "eslint": "^8.0.0", 45 | "tsx": "^4.0.0" 46 | }, 47 | "engines": { 48 | "node": ">=18.0.0" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/NocteDefensor/LudusMCP.git" 53 | }, 54 | "bugs": { 55 | "url": "https://github.com/NocteDefensor/LudusMCP/issues" 56 | }, 57 | "homepage": "https://github.com/NocteDefensor/LudusMCP#readme" 58 | } 59 | -------------------------------------------------------------------------------- /schemas/badsectorlabs.ludus_emux.yaml: -------------------------------------------------------------------------------- 1 | name: badsectorlabs.ludus_emux 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs EMUX and runs an emulated device on Debian based hosts" 5 | repository: "https://github.com/badsectorlabs/ludus_emux" 6 | author: "Bad Sector Labs" 7 | installation_method: "ludus ansible role add badsectorlabs.ludus_emux" 8 | dependencies: 9 | - "geerlingguy.docker" 10 | 11 | note: "Ports 80/443 mapped to emulated device - don't use host for other services" 12 | 13 | variables: 14 | ludus_emux_path: 15 | type: "string" 16 | default: "/opt/emux" 17 | description: "Installation path for EMUX" 18 | 19 | ludus_emux_device: 20 | type: "integer" 21 | default: 4 22 | description: "Device number to emulate" 23 | valid_options: 24 | 0: "DV-ARM (Damn Vulnerable ARM Router)" 25 | 1: "DV-MIPSEL (Damn Vulnerable MIPS Router - Little Endian)" 26 | 2: "DV-MIPSEB (Damn Vulnerable MIPS Router - Big Endian)" 27 | 3: "PH0WNCTF (R0 Port Controller - Ph0wn 2021 CTF Challenge)" 28 | 4: "TRI227WF (Trivision NC-227-WF IP Camera)" 29 | 5: "AC15 (Tenda AC15 Wi-Fi Router)" 30 | 6: "ARCHERC9 (Archer C9 Wi-Fi Router)" 31 | 7: "DIR615C (D-Link DIR615C Wi-Fi Router)" 32 | 8: "DCS935L (D-Link DCS-935L Camera)" 33 | 34 | ludus_emux_devices_map: 35 | type: "object" 36 | default: 37 | "0": "DV-ARM" 38 | "1": "DV-MIPSEL" 39 | "2": "DV-MIPSEB" 40 | "3": "PH0WNCTF" 41 | "4": "TRI227WF" 42 | "5": "AC15" 43 | "6": "ARCHERC9" 44 | "7": "DIR615C" 45 | "8": "DCS935L" 46 | description: "Device ID to device name mapping - DO NOT MODIFY THIS VAR" 47 | readonly: true 48 | note: "Reference mapping for device numbers to device names" -------------------------------------------------------------------------------- /schemas/curi0usjack.ludus_badblood.yaml: -------------------------------------------------------------------------------- 1 | # Schema for ludus_badblood Role 2 | # Populates Active Directory domain with vulnerable data from BadBlood project 3 | 4 | name: curi0usjack.ludus_badblood 5 | type: role 6 | version: "1.0.0" 7 | description: "Outfits Ludus AD domain with vulnerable objects and configurations from BadBlood project for security testing" 8 | repository: "https://github.com/curi0usJack/ludus_badblood" 9 | author: "curi0usJack (@curi0usJack)" 10 | installation_method: "ludus ansible role add -d /path/to/directory" 11 | note: "This role requires a Windows Domain Controller with internet access. BadBlood execution takes significant time to complete." 12 | warning: "This role intentionally creates vulnerable AD configurations for testing purposes. Only use in isolated lab environments." 13 | 14 | # This role has no configurable variables - it automatically downloads and runs BadBlood 15 | variables: {} 16 | 17 | # BadBlood project information: 18 | # Original project: https://github.com/davidprowe/BadBlood 19 | # Purpose: Fills Active Directory with realistic but vulnerable user accounts, groups, and configurations 20 | # 21 | # Role performs these actions: 22 | # 1. Checks for Git installation on target system 23 | # 2. Downloads Git installer if not present and installs silently 24 | # 3. Clones BadBlood repository from GitHub to C:\Windows\Temp\BadBlood 25 | # 4. Executes Invoke-BadBlood.ps1 script to populate AD with vulnerable data 26 | # 27 | # BadBlood creates: 28 | # - Hundreds of realistic user accounts with weak passwords 29 | # - Complex group membership structures with privilege escalation paths 30 | # - Misconfigured permissions and ACLs 31 | # - Vulnerable service accounts and SPNs 32 | # - Other AD security misconfigurations for testing detection tools 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.24] - 2025-08-16 4 | 5 | ### Documentation 6 | 7 | - Added update instructions to README for both NPM global and local development installs 8 | - Added changelog documentation 9 | 10 | ## [1.0.23] - 2025-08-16 11 | 12 | ### Bug Fixes 13 | 14 | - Fixed `insert_creds_range_config` help parameter functionality 15 | - Fixed path resolution bug in `insert_creds_range_config` - relative paths now correctly resolve to `range-config-templates` directory 16 | - Fixed credential injection not saving files - credentials are now actually written to disk 17 | - Fixed `get_credential_from_user` help parameter functionality 18 | - Fixed server-side help mode handling for both tools 19 | 20 | ### Changes 21 | 22 | - Removed `validateOnly` parameter from `insert_creds_range_config` (breaking change) 23 | - Tool now always injects credentials and saves files 24 | - Made `credName` optional in `get_credential_from_user` when using help mode 25 | - Made `configPath` and `credentialMappings` optional in `insert_creds_range_config` when using help mode 26 | - Updated tool descriptions with better usage examples 27 | - Added installation command examples to role collection schemas 28 | 29 | ### Breaking Changes 30 | 31 | - `insert_creds_range_config`: Removed `validateOnly` parameter. Tool now always saves files after credential injection. 32 | 33 | ### Migration 34 | 35 | If using `validateOnly: true`, make a copy of your config file before injection: 36 | - Before: `{ "configPath": "...", "credentialMappings": {...}, "validateOnly": true }` 37 | - After: `{ "configPath": "...", "credentialMappings": {...} }` 38 | 39 | ## [1.0.18] - Previous Release 40 | 41 | - Base functionality for Ludus MCP server 42 | - Core tools for range management, credential handling, and configuration 43 | -------------------------------------------------------------------------------- /schemas/badsectorlabs.ludus_elastic_agent.yaml: -------------------------------------------------------------------------------- 1 | name: badsectorlabs.ludus_elastic_agent 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Elastic Agent for log collection" 5 | repository: "https://github.com/badsectorlabs/ludus_elastic_agent" 6 | author: "Bad Sector Labs" 7 | installation_method: "ludus ansible role add badsectorlabs.ludus_elastic_agent" 8 | dependencies: [] 9 | variables: 10 | ludus_install_directory: 11 | type: "string" 12 | default: "/opt/ludus" 13 | description: "Ludus installation directory" 14 | 15 | ludus_elastic_container_install_path: 16 | type: "string" 17 | default: "/opt/elastic_container" 18 | description: "Path on server VM for automatic token extraction" 19 | 20 | ludus_elastic_fleet_port: 21 | type: "string" 22 | default: "8220" 23 | description: "Port for fleet service on server VM" 24 | 25 | ludus_elastic_enrollment_token: 26 | type: "string" 27 | default: "" 28 | description: "Enrollment token for Elastic Fleet. You can retrieve this from kibana or via the deployment logs if watching" 29 | 30 | ludus_elastic_fleet_server: 31 | type: "string" 32 | default: "" 33 | description: "The badsectorlabs.ludus_elastic_agent will automatically find the enrollment token and URL for the elastic server and enroll the agent. You can set the token and URL manually using role_vars if you wish. Ex https://10.{{ range_second_octet }}.30.10:8220" 34 | 35 | ludus_elastic_agent_version: 36 | type: "string" 37 | default: "9.0.1" 38 | description: "Elastic Agent version" 39 | 40 | ludus_elastic_install_sysmon: 41 | type: "boolean" 42 | default: true 43 | description: "Whether or not to install Sysmon" 44 | 45 | ludus_elastic_sysmon_path: 46 | type: "string" 47 | default: "C:\\Program Files (x86)\\Sysmon" 48 | description: "Sysmon Install Path" 49 | -------------------------------------------------------------------------------- /base-configs/veeam-backup-lab.yml: -------------------------------------------------------------------------------- 1 | ludus: 2 | - vm_name: "{{ range_id }}-veeam-server" 3 | hostname: "{{ range_id }}-VBR01" 4 | template: win2022-server-x64-template 5 | vlan: 10 6 | ip_last_octet: 25 7 | ram_gb: 8 8 | cpus: 4 9 | windows: 10 | sysprep: true 11 | install_additional_tools: true 12 | testing: 13 | snapshot: true 14 | block_internet: false 15 | roles: 16 | - mojeda101.ludus_veeam_vbr 17 | role_vars: 18 | ludus_veeam_vbr_version: "12" 19 | ludus_veeam_vbr_iso_download: true 20 | ludus_veeam_vbr_sql_install_username: "sql_install" 21 | ludus_veeam_vbr_sql_install_password: "VeeamSQL123!" 22 | ludus_veeam_vbr_sql_service_username: "svc_sql" 23 | ludus_veeam_vbr_sql_service_password: "VeeamSQL123!" 24 | ludus_veeam_vbr_sql_username: "postgres" 25 | ludus_veeam_vbr_sql_password: "VeeamDB123!" 26 | ludus_veeam_vbr_sql_authentication: "1" 27 | 28 | # Additional Windows Server for backup target testing 29 | - vm_name: "{{ range_id }}-backup-target" 30 | hostname: "{{ range_id }}-BKUP-TGT01" 31 | template: win2022-server-x64-template 32 | vlan: 10 33 | ip_last_octet: 26 34 | ram_gb: 4 35 | cpus: 2 36 | windows: 37 | sysprep: true 38 | install_additional_tools: true 39 | testing: 40 | snapshot: true 41 | block_internet: false 42 | 43 | # Windows workstation for backup source testing 44 | - vm_name: "{{ range_id }}-backup-source" 45 | hostname: "{{ range_id }}-BKUP-WS01" 46 | template: win11-22h2-x64-enterprise-template 47 | vlan: 10 48 | ip_last_octet: 27 49 | ram_gb: 4 50 | cpus: 2 51 | windows: 52 | sysprep: false 53 | install_additional_tools: true 54 | chocolatey_packages: 55 | - notepadplusplus 56 | - 7zip 57 | testing: 58 | snapshot: true 59 | block_internet: false 60 | -------------------------------------------------------------------------------- /base-configs/basic-ad-network.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://docs.ludus.cloud/schemas/range-config.json 2 | # Basic Active Directory Network - Default Ludus configuration 3 | # DC + Win11 client + Kali attacker with network rules 4 | 5 | ludus: 6 | - vm_name: "{{ range_id }}-ad-dc-win2022-server-x64" 7 | hostname: "{{ range_id }}-DC01-2022" 8 | template: win2022-server-x64-template 9 | vlan: 10 10 | ip_last_octet: 11 11 | ram_gb: 8 12 | cpus: 4 13 | windows: 14 | sysprep: false 15 | domain: 16 | fqdn: ludus.domain 17 | role: primary-dc 18 | - vm_name: "{{ range_id }}-ad-win11-22h2-enterprise-x64-1" 19 | hostname: "{{ range_id }}-WIN11-22H2-1" 20 | template: win11-22h2-x64-enterprise-template 21 | vlan: 10 22 | ip_last_octet: 21 23 | ram_gb: 8 24 | cpus: 4 25 | windows: 26 | install_additional_tools: true 27 | chocolatey_ignore_checksums: true 28 | office_version: 2019 29 | office_arch: 64bit 30 | domain: 31 | fqdn: ludus.domain 32 | role: member 33 | - vm_name: "{{ range_id }}-kali" 34 | hostname: "{{ range_id }}-kali" 35 | template: kali-x64-desktop-template 36 | vlan: 99 37 | ip_last_octet: 1 38 | ram_gb: 8 39 | cpus: 4 40 | linux: true 41 | testing: 42 | snapshot: false 43 | block_internet: false 44 | 45 | network: 46 | inter_vlan_default: REJECT 47 | rules: 48 | - name: Only allow windows to kali on 443 49 | vlan_src: 10 50 | vlan_dst: 99 51 | protocol: tcp 52 | ports: 443 53 | action: ACCEPT 54 | - name: Only allow windows to kali on 80 55 | vlan_src: 10 56 | vlan_dst: 99 57 | protocol: tcp 58 | ports: 80 59 | action: ACCEPT 60 | - name: Only allow windows to kali on 8080 61 | vlan_src: 10 62 | vlan_dst: 99 63 | protocol: tcp 64 | ports: 8080 65 | action: ACCEPT 66 | - name: Allow kali to all windows 67 | vlan_src: 99 68 | vlan_dst: 10 69 | protocol: all 70 | ports: all 71 | action: ACCEPT -------------------------------------------------------------------------------- /schemas/ludus_child_domain_join.yaml: -------------------------------------------------------------------------------- 1 | name: ludus_child_domain_join 2 | type: role 3 | version: "1.0.0" 4 | description: "Join a machine to the child domain created from ludus_child_domain" 5 | repository: "https://github.com/NocteDefensor/ludus_ansible_roles" 6 | author: "ChoiSG (@_choisec) - Fork maintained by NocteDefensor" 7 | dependencies: [] 8 | installation_method: "ludus ansible role add -d /path/to/directory" 9 | note: "Must install from directory - required because Ludus backend doesn't support 3rd party domain/controllers" 10 | IMPORTANT_DOMAIN_CONFIG_NOTE: "DO NOT include 'domain:' section in VM configuration when using ludus_child_domain_join role. The role handles domain joining internally. Example: Do NOT add 'domain: { fqdn: prod.test.local, role: member }' to VMs using this role." 11 | variables: 12 | dc_ip: 13 | type: "string" 14 | default: "10.2.30.10" 15 | description: "desired IP address" 16 | 17 | dns_domain_name: 18 | type: "string" 19 | default: "dev.test.local" 20 | description: "desired subdomain.parent.domain" 21 | 22 | domain_admin_user: 23 | type: "string" 24 | default: "administrator@dev.test.local" 25 | description: "@ format. Use `administrator`, since ludus's domainadmin default user is not created with ludus_child_domain" 26 | 27 | domain_admin_password: 28 | type: "string" 29 | default: "password" 30 | description: " always use password" 31 | 32 | install_rsat_tools: 33 | type: "boolean" 34 | default: true 35 | description: "Whether to install RSAT AD tools. Set to false to skip RSAT installation entirely." 36 | 37 | rsat_install_method: 38 | type: "string" 39 | default: "auto" 40 | description: "RSAT installation method: 'feature' (Windows Server), 'powershell' (Windows Client/workstation), 'auto' (try both), or any other value to skip" 41 | enum: ["feature", "powershell", "auto", "skip"] 42 | note: "Use 'dism' for Windows 10/11 workstations to avoid ServerManager module errors. Use 'feature' for Windows Server. Use 'auto' to try both methods." -------------------------------------------------------------------------------- /schemas/badsectorlabs.ludus_mssql.yaml: -------------------------------------------------------------------------------- 1 | name: badsectorlabs.ludus_mssql 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs MSSQL on Windows systems" 5 | repository: "https://github.com/badsectorlabs/ludus_mssql" 6 | author: "Bad Sector Labs" 7 | installation_method: "ludus ansible role add badsectorlabs.ludus_mssql" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_mssql_version: 12 | type: "string" 13 | default: "2019" 14 | description: "MSSQL version to install" 15 | valid_options: 16 | - "2019" 17 | - "2022" 18 | 19 | ludus_install_directory: 20 | type: "string" 21 | default: "/opt/ludus" 22 | description: "Ludus installation directory" 23 | 24 | ludus_mssql_iso_directory: 25 | type: "string" 26 | default: "C:\\ludus" 27 | description: "Directory for MSSQL ISO files" 28 | 29 | ludus_mssql_sql_config_path: 30 | type: "string" 31 | default: "{{ ludus_mssql_iso_directory }}\\sqlsrv_{{ ludus_mssql_version }}_config.ini" 32 | description: "Path to SQL Server configuration file" 33 | 34 | ludus_mssql_iso_url: 35 | type: "string" 36 | default: "https://archive.org/download/en_sql_server_2019_standard_x64_dvd_814b57aa_202211/en_sql_server_2019_standard_x64_dvd_814b57aa.iso" 37 | description: "URL to download MSSQL ISO" 38 | 39 | ludus_mssql_iso_checksum: 40 | type: "string" 41 | default: "sha256:1e56705b3544e77039584b3b38461df0321834822776aef8e50847fdd9edad44" 42 | description: "Checksum for MSSQL ISO verification" 43 | 44 | ludus_mssql_instance_name: 45 | type: "string" 46 | default: "MSSQLSERVER" 47 | description: "SQL Server instance name" 48 | 49 | ludus_mssql_sql_license_key: 50 | type: "string" 51 | default: null 52 | description: "Optional SQL Server license key" 53 | 54 | ludus_mssql_ssms_url: 55 | type: "string" 56 | default: "https://aka.ms/ssmsfullsetup" 57 | description: "URL to download SQL Server Management Studio" 58 | 59 | ludus_mssql_install_ssms: 60 | type: "boolean" 61 | default: true 62 | description: "Whether to install SQL Server Management Studio" -------------------------------------------------------------------------------- /base-configs/k3s-cluster-lab.yml: -------------------------------------------------------------------------------- 1 | ludus: 2 | # k3s Server (Control Plane) 3 | - vm_name: "{{ range_id }}-k3s-server" 4 | hostname: "{{ range_id }}-k3s-server" 5 | template: debian-12-x64-server-template 6 | vlan: 10 7 | ip_last_octet: 10 8 | ram_gb: 4 9 | cpus: 2 10 | linux: true 11 | testing: 12 | snapshot: true 13 | block_internet: false 14 | roles: 15 | - netpenguins.ludus_k3s 16 | role_vars: 17 | ludus_k3s_role: server 18 | ludus_k3s_disable_components: 19 | - traefik 20 | ludus_k3s_dashboard: true 21 | ludus_k3s_dashboard_type: "kubernetes" 22 | 23 | # k3s Agent 1 (Worker Node) 24 | - vm_name: "{{ range_id }}-k3s-agent-1" 25 | hostname: "{{ range_id }}-k3s-agent-1" 26 | template: debian-12-x64-server-template 27 | vlan: 10 28 | ip_last_octet: 11 29 | ram_gb: 2 30 | cpus: 2 31 | linux: true 32 | testing: 33 | snapshot: true 34 | block_internet: false 35 | roles: 36 | - netpenguins.ludus_k3s 37 | role_vars: 38 | ludus_k3s_role: agent 39 | ludus_k3s_server_url: "https://10.{{ range_second_octet }}.10.10:6443" 40 | 41 | # k3s Agent 2 (Worker Node) 42 | - vm_name: "{{ range_id }}-k3s-agent-2" 43 | hostname: "{{ range_id }}-k3s-agent-2" 44 | template: debian-12-x64-server-template 45 | vlan: 10 46 | ip_last_octet: 12 47 | ram_gb: 2 48 | cpus: 2 49 | linux: true 50 | testing: 51 | snapshot: true 52 | block_internet: false 53 | roles: 54 | - netpenguins.ludus_k3s 55 | role_vars: 56 | ludus_k3s_role: agent 57 | ludus_k3s_server_url: "https://10.{{ range_second_octet }}.10.10:6443" 58 | 59 | # Windows management host with Headlamp 60 | - vm_name: "{{ range_id }}-win11-mgmt" 61 | hostname: "{{ range_id }}-WIN11-MGMT" 62 | template: win11-22h2-x64-enterprise-template 63 | vlan: 10 64 | ip_last_octet: 21 65 | ram_gb: 8 66 | cpus: 4 67 | windows: 68 | install_additional_tools: true 69 | chocolatey_ignore_checksums: true 70 | chocolatey_packages: 71 | - vscodium 72 | - headlamp 73 | - kubernetes-helm 74 | testing: 75 | snapshot: true 76 | block_internet: false 77 | -------------------------------------------------------------------------------- /schemas/ludus-ad-content.yaml: -------------------------------------------------------------------------------- 1 | name: ludus-ad-content 2 | type: role 3 | version: "1.0.0" 4 | description: "Creates Active Directory organizational units, groups, and users" 5 | repository: "https://github.com/Cyblex-Consulting/ludus-ad-content" 6 | author: "Cyblex-Consulting" 7 | installation_method: "ludus ansible role add -d /path/to/directory" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_ad.ous.name: 12 | type: "string" 13 | default: "France" 14 | description: "OU name" 15 | 16 | ludus_ad.ous.path: 17 | type: "string" 18 | default: "DC=myrange,DC=corp" 19 | description: "OU path" 20 | 21 | ludus_ad.ous.description: 22 | type: "string" 23 | default: "jdoe" 24 | description: "OU description" 25 | 26 | ludus_ad.groups.name: 27 | type: "string" 28 | default: "France" 29 | description: "Group name" 30 | 31 | ludus_ad.groups.scope: 32 | type: "string" 33 | default: "global" 34 | description: "Group scope" 35 | 36 | ludus_ad.groups.path: 37 | type: "string" 38 | default: "OU=France,DC=myrange,DC=corp" 39 | description: "Group path" 40 | 41 | ludus_ad.groups.description: 42 | type: "string" 43 | default: "jdoe" 44 | description: "Group description" 45 | 46 | ludus_ad.users.name: 47 | type: "string" 48 | default: "France" 49 | description: "User name" 50 | 51 | ludus_ad.users.firstname: 52 | type: "string" 53 | default: "John" 54 | description: "User first name" 55 | 56 | ludus_ad.users.surname: 57 | type: "string" 58 | default: "Doe" 59 | description: "User surname" 60 | 61 | ludus_ad.users.display_name: 62 | type: "string" 63 | default: "display_name" 64 | description: "User display name" 65 | 66 | ludus_ad.users.password: 67 | type: "string" 68 | default: "GFVfPS432QkKN2YdQwJL" 69 | description: "User password" 70 | 71 | ludus_ad.users.path: 72 | type: "string" 73 | default: "OU=France,DC=myrange,DC=corp" 74 | description: "User path" 75 | 76 | ludus_ad.users.description: 77 | type: "string" 78 | default: "IT System Administrator" 79 | description: "User description" 80 | 81 | ludus_ad.users.groups: 82 | type: "array" 83 | default: 84 | - "Group 1" 85 | - "Group 2" 86 | description: "List of groups to add user to" -------------------------------------------------------------------------------- /schemas/Sample-schema.yaml: -------------------------------------------------------------------------------- 1 | # Sample Schema Template for Ludus Roles and Collections 2 | # This is a template showing the standard format for YAML schema files 3 | 4 | name: sample_role_name 5 | type: role # or "collection" for role collections 6 | version: "1.0.0" 7 | description: "Brief description of what this role/collection does" 8 | repository: "https://github.com/author/repo_name" 9 | author: "Author Name (@github_handle)" 10 | 11 | # Optional fields 12 | dependencies: 13 | - "other.required.role" 14 | - "another.dependency" 15 | 16 | installation_method: "ludus ansible role/collection add " # or "-d /path/to/directory if must be installed from directory 17 | note: "Any important notes about usage or limitations" 18 | warning: "Any critical warnings (e.g., 'This role deploys malware')" 19 | 20 | # For single roles, use "variables" directly 21 | variables: 22 | variable_name: 23 | type: "string" # string, boolean, integer, array, object 24 | required: true # or false 25 | default: "default_value" 26 | description: "Description of what this variable does" 27 | valid_options: # optional - for enumerated values 28 | - "option1" 29 | - "option2" 30 | example: # optional - example usage 31 | - "example_value1" 32 | - "example_value2" 33 | 34 | another_variable: 35 | type: "boolean" 36 | default: false 37 | description: "Another example variable" 38 | 39 | # For collections, use "roles" with nested variables 40 | # roles: 41 | # role_name_1: 42 | # variables: 43 | # role1_var: 44 | # type: "string" 45 | # default: "value" 46 | # description: "Variable for role 1" 47 | # role_name_2: 48 | # variables: 49 | # role2_var: 50 | # type: "integer" 51 | # default: 80 52 | # description: "Variable for role 2" 53 | 54 | # Special fields for collections only 55 | # IMPORTANT_DOMAIN_CONFIG_NOTE: "Special notes about domain configuration" 56 | # readonly: true # for variables that should not be modified 57 | 58 | # Common variable types and patterns: 59 | # - External credentials: Use placeholders like "tskey-auth-" 60 | # - Paths: Use sensible defaults like "/opt/application" 61 | # - Ports: Use integers with reasonable defaults 62 | # - URLs: Provide working default URLs when possible 63 | # - Auto-detected: Use "{{ auto-detected }}" for Ludus-managed values -------------------------------------------------------------------------------- /base-configs/parent-child-template-config.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://docs.ludus.cloud/schemas/range-config.json 2 | # Parent-Child Domain Template Configuration 3 | # Demonstrates parent domain with child domain and domain joining 4 | # 5 | # TOPOLOGY: 6 | # - VLAN 10: Parent domain (example.local) with primary DC 7 | # - VLAN 20: Child domain (kr.example.local) with child DC and member server 8 | # 9 | # IMPORTANT NOTES: 10 | # - Do NOT add 'domain:' configuration to VMs using ludus_child_domain_reborn or ludus_child_domain_join roles 11 | # - These roles handle domain operations internally 12 | # - Use 'administrator@domain' format for child domain operations (not domainadmin) 13 | 14 | ludus: 15 | - vm_name: "dc01-example-local" 16 | hostname: "dc01" 17 | template: win2022-rapt 18 | vlan: 10 19 | ip_last_octet: 10 20 | ram_gb: 2 21 | cpus: 1 22 | windows: 23 | sysprep: true 24 | domain: 25 | fqdn: example.local 26 | role: primary-dc 27 | 28 | - vm_name: "kdc01-kr-example-local" 29 | hostname: "kdc01" 30 | template: win2022-rapt 31 | vlan: 20 32 | ip_last_octet: 10 33 | ram_gb: 2 34 | cpus: 1 35 | windows: 36 | sysprep: true 37 | roles: 38 | - ludus_child_domain_reborn 39 | role_vars: 40 | dns_domain_name: kr.example.local 41 | domain_admin_user: domainadmin@example.local 42 | domain_admin_password: password 43 | parent_domain_name: example.local 44 | safe_mode_password: password 45 | create_dns_delegation: true 46 | parent_dc_ip: "10.{{ range_second_octet }}.10.10" 47 | current_host_ip: "10.{{ range_second_octet }}.20.10" 48 | reboot: true 49 | 50 | # Ludus_Child_Domain_Join to the child domain controller from above 51 | - vm_name: "{{ range_id }}-Internal-VLAN20-fs01" 52 | hostname: "fs01" 53 | template: win2016-server-x64-template 54 | vlan: 20 55 | ip_last_octet: 20 56 | ram_gb: 2 57 | cpus: 1 58 | windows: 59 | sysprep: true 60 | roles: 61 | - ludus_child_domain_join 62 | role_vars: 63 | dc_ip: "10.{{ range_second_octet }}.20.10" 64 | dns_domain_name: "kr.example.local" 65 | domain_admin_user: "administrator@kr.example.local" # @ format. Use administrator, since ludus's domainadmin default user is not created with ludus_child_domain 66 | domain_admin_password: "password" -------------------------------------------------------------------------------- /schemas/curi0usjack.ludus_enable_asr.yaml: -------------------------------------------------------------------------------- 1 | # Schema for ludus_enable_asr Role 2 | # Enables Windows Defender Attack Surface Reduction rules 3 | 4 | name: curi0usjack.ludus_enable_asr 5 | type: role 6 | version: "1.0.0" 7 | description: "Enables 16 Windows Defender Attack Surface Reduction (ASR) rules to harden endpoint security" 8 | repository: "https://github.com/curi0usJack/Ludus-MDE-MDI-Roles/tree/main/ludus_enable_asr" 9 | author: "curi0usJack (@curi0usJack)" 10 | installation_method: "ludus ansible role add -d /path/to/directory" 11 | note: "This role requires Windows hosts with Windows Defender available. No variables required - all ASR rules are enabled automatically." 12 | warning: "ASR rules may impact legitimate applications. Test in non-production environments first." 13 | 14 | # This role has no configurable variables - it automatically enables all 16 ASR rules 15 | variables: {} 16 | 17 | # ASR Rules enabled by this role: 18 | # - Block Office Child Process Creation (D4F940AB-401B-4EFC-AADC-AD5F3C50688A) 19 | # - Block Process Injection (75668C1F-73B5-4CF0-BB93-3ECF5CB7CC84) 20 | # - Block Win32 API calls in macros (92E97FA1-2EDF-4476-BDD6-9DD0B4DDDC7B) 21 | # - Block Office from creating executables (3B576869-A4EC-4529-8536-B80A7769E899) 22 | # - Block execution of potentially obfuscated scripts (5BEB7EFE-FD9A-4556-801D-275E5FFC04CC) 23 | # - Block executable content from email client and webmail (BE9BA2D9-53EA-4CDC-84E5-9B1EEEE46550) 24 | # - Block JavaScript or VBScript from launching downloaded executable content (D3E037E1-3EB8-44C8-A917-57927947596D) 25 | # - Block lsass cred theft (9e6c4e1f-7d60-472f-ba1a-a39ef669e4b2) 26 | # - Block untrusted and unsigned processes that run from USB (b2b3f03d-6a65-4f7b-a9c7-1c7ef74a9ba4) 27 | # - Block Adobe Reader from creating child processes (7674ba52-37eb-4a4f-a9a1-f0f9a1619a2c) 28 | # - Block persistence through WMI event subscription (e6db77e5-3df2-4cf1-b95a-636979351e5b) 29 | # - Block process creations originating from PSExec and WMI commands (d1e49aac-8f56-4280-b9ba-993a6d77406c) 30 | # - Block unsigned driver abuse (56a863a9-875e-4185-98a7-b882c64b5ce5) 31 | # - Block executable files from running unless they meet a prevalence, age, or trusted list criterion (01443614-cd74-433a-b99e-2ecdc07bfc25) 32 | # - Block use of copied or impersonated system tools (c0033c00-d16d-4114-a5a0-dc9b3a7d2ceb) 33 | # - Use advanced protection against ransomware (c1db55ab-c21a-4637-bb3f-a12568109d35) 34 | -------------------------------------------------------------------------------- /schemas/ludus-gitlab-ce.yaml: -------------------------------------------------------------------------------- 1 | name: ludus-gitlab-ce 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs and configures GitLab Community Edition" 5 | repository: "https://github.com/Cyblex-Consulting/ludus-gitlab-ce" 6 | author: "Cyblex-Consulting" 7 | installation_method: "ludus ansible role add -d /path/to/directory" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_gitlab.url: 12 | type: "string" 13 | default: "http://localhost:8080" 14 | description: "URL where the instance will be exposed" 15 | 16 | ludus_gitlab.displayname: 17 | type: "string" 18 | default: "MyCompany Gitlab" 19 | description: "Name of the instance" 20 | 21 | ludus_gitlab.smtp: 22 | type: "string" 23 | default: "127.0.0.1" 24 | description: "SMTP server for sending emails" 25 | 26 | ludus_gitlab.email_from: 27 | type: "string" 28 | default: "gitlab@example.com" 29 | description: "From email address for GitLab messages" 30 | 31 | ludus_gitlab.replyto: 32 | type: "string" 33 | default: "no-reply@example.com" 34 | description: "Reply-to email address" 35 | 36 | ludus_gitlab.emailroot: 37 | type: "string" 38 | default: "admin@example.com" 39 | description: "Root email account" 40 | 41 | ludus_gitlab.version: 42 | type: "string" 43 | default: "16.7.0" 44 | description: "Optional GitLab version to install" 45 | 46 | ludus_gitlab.groups.name: 47 | type: "string" 48 | default: "MyGroup" 49 | description: "Optional name of group to create" 50 | 51 | ludus_gitlab.users.name: 52 | type: "string" 53 | default: "jdoe" 54 | description: "Optional login of user to create" 55 | 56 | ludus_gitlab.users.display_name: 57 | type: "string" 58 | default: "John Doe" 59 | description: "Optional display name of user" 60 | 61 | ludus_gitlab.users.password: 62 | type: "string" 63 | default: "aA8MaQBCtBtPYAFh" 64 | description: "Optional password of user" 65 | 66 | ludus_gitlab.users.email: 67 | type: "string" 68 | default: "jdoe@myrange.corp" 69 | description: "Optional email of user" 70 | 71 | ludus_gitlab.users.group: 72 | type: "string" 73 | default: "MyGroup" 74 | description: "Optional group to apply to user" 75 | 76 | ludus_gitlab.users.access_level: 77 | type: "string" 78 | default: "maintainer" 79 | description: "Optional access level of user" -------------------------------------------------------------------------------- /src/prompts/index.ts: -------------------------------------------------------------------------------- 1 | // ============================================================================ 2 | // LUDUS MCP PROMPTS - STATIC PROMPT EXPORTS 3 | // ============================================================================ 4 | 5 | import { Prompt } from '@modelcontextprotocol/sdk/types.js'; 6 | 7 | export const createLudusRangePrompt: Prompt = { 8 | name: "create-ludus-range", 9 | description: "Create a complete Ludus cyber range from your requirements. Handles the entire workflow from planning to validation.", 10 | arguments: [ 11 | { 12 | name: "requirements", 13 | description: "Describe what you want to build in natural language (e.g., 'AD range with one workstation, dedicated file server, SCCM, and Elastic watching the workstation joined to Tailscale')", 14 | required: true 15 | }, 16 | { 17 | name: "roles", 18 | description: "Optional: Specify desired Ludus roles/collections to use (e.g., 'ludus_sccm, ludus_elastic_agent, ludus_tailscale')", 19 | required: false 20 | }, 21 | { 22 | name: "save_config", 23 | description: "Whether to save the generated configuration to file (true/false)", 24 | required: false 25 | } 26 | ] 27 | }; 28 | 29 | export const executeLudusCmdPrompt: Prompt = { 30 | name: "execute-ludus-cmd", 31 | description: "Safely execute Ludus CLI commands with comprehensive safety protocols, tool preference checking, and destructive action confirmation.", 32 | arguments: [ 33 | { 34 | name: "command_intent", 35 | description: "Describe what you want to accomplish with the CLI command (e.g., 'check range status', 'get user information', 'abort stuck deployment')", 36 | required: true 37 | }, 38 | { 39 | name: "target_user", 40 | description: "Target user for admin operations (leave empty for current user operations)", 41 | required: false 42 | }, 43 | { 44 | name: "confirm_destructive", 45 | description: "Set to true only AFTER user explicitly confirms destructive operations (type: true/false, TRUE/FALSE, or t/f)", 46 | required: false 47 | } 48 | ] 49 | }; 50 | 51 | // Export all prompts as an array for easy registration 52 | export const ALL_PROMPTS: Prompt[] = [ 53 | createLudusRangePrompt, 54 | executeLudusCmdPrompt 55 | ]; 56 | 57 | // Re-export handlers for use in server 58 | export { handleCreateLudusRangePrompt } from './createLudusRange.js'; 59 | export { handleExecuteLudusCmdPrompt } from './executeLudusCmd.js'; -------------------------------------------------------------------------------- /base-configs/ADFS-sample.yaml: -------------------------------------------------------------------------------- 1 | # requires ludus_adfs collection at git clone https://github.com/bagelByt3s/ludus_adfs /opt/ludus_adfs && ludus ansible collection add /opt/ludus_adfs 2 | ludus: 3 | - vm_name: "DC01-WinServer2022" 4 | hostname: "DC01" 5 | template: win2022-server-x64-template 6 | vlan: 10 7 | ip_last_octet: 10 8 | ram_gb: 8 9 | cpus: 4 10 | windows: 11 | sysprep: true 12 | domain: 13 | fqdn: ludus.nuketown 14 | role: primary-dc 15 | roles: 16 | - bagelByt3s.ludus_adfs.install_adcs 17 | 18 | - vm_name: "ADFS-WinServer2022" 19 | hostname: "ADFS" 20 | template: win2022-server-x64-template 21 | vlan: 10 22 | ip_last_octet: 11 23 | ram_gb: 8 24 | ram_min_gb: 2 25 | cpus: 4 26 | windows: 27 | sysprep: true 28 | domain: 29 | fqdn: ludus.nuketown 30 | role: member 31 | roles: 32 | - bagelByt3s.ludus_adfs.import_root_cert 33 | - bagelByt3s.ludus_adfs.install_adfs 34 | role_vars: 35 | adfs_service_account: 'adfs_svc' 36 | adfs_service_account_password: 'password' 37 | adfs_service_display_name: 'ADFS Service' 38 | adfs_hostname: 'adfs.ludus.nuketown' 39 | adfs_FQDN_CA: 'DC01.ludus.nuketown\ludus-CA' 40 | adfs_CA: "ludus-CA" 41 | adfs_certificate_subject: "CN=adfs.ludus.nuketown, O=Ludus, C=US" 42 | 43 | - vm_name: "EntraConnect-WinServer2022" 44 | hostname: "EntraConnect" 45 | template: win2022-server-x64-template 46 | vlan: 10 47 | ip_last_octet: 12 48 | ram_gb: 8 49 | ram_min_gb: 2 50 | cpus: 4 51 | windows: 52 | sysprep: true 53 | domain: 54 | fqdn: ludus.nuketown 55 | role: member 56 | roles: 57 | - bagelByt3s.ludus_adfs.import_root_cert 58 | - bagelByt3s.ludus_adfs.entra_prep 59 | role_vars: 60 | adfs_CA: "ludus-CA" 61 | ludus_entra_join_svc_account: "entra_svc" 62 | ludus_entra_join_svc_account_password: "password" 63 | ludus_entra_join_alt_upn: "" #Example: domain.onmicrosoft.com or leave blank to skip 64 | 65 | - vm_name: "Workstation-Win11" 66 | hostname: "Workstation" 67 | template: win11-22h2-x64-enterprise-template 68 | vlan: 10 69 | ip_last_octet: 13 70 | ram_gb: 4 71 | ram_min_gb: 2 72 | cpus: 4 73 | windows: 74 | sysprep: true 75 | domain: 76 | fqdn: ludus.nuketown 77 | role: member 78 | roles: 79 | - bagelByt3s.ludus_adfs.import_root_cert 80 | role_vars: 81 | adfs_CA: "ludus-CA" -------------------------------------------------------------------------------- /schemas/mojeda101.ludus_veeam_vbr.yaml: -------------------------------------------------------------------------------- 1 | name: mojeda101.ludus_veeam_vbr 2 | type: role 3 | version: "1.0.0" 4 | description: "Thin Ludus-ready wrapper that invokes upstream veeamhub.veeam.veeam_vas role to install Veeam Backup & Replication Community Edition v12+" 5 | repository: "https://github.com/mojeda101/ludus_veeam_vbr" 6 | author: "mojeda101 (@mojeda101)" 7 | 8 | dependencies: 9 | - "veeamhub.veeam" 10 | - "ansible.windows" 11 | - "community.windows" 12 | 13 | installation_method: "ludus ansible role add mojeda101.ludus_veeam_vbr" 14 | note: "Requires Windows Server with WinRM configured. System must reach Internet to download upstream ISO. Installation status check task takes considerable time" 15 | warning: "Installation process is lengthy due to Veeam's comprehensive setup requirements" 16 | 17 | variables: 18 | ludus_veeam_vbr_version: 19 | type: "string" 20 | required: false 21 | default: "12" 22 | description: "Veeam Backup & Replication version to install" 23 | example: 24 | - "12" 25 | - "11" 26 | 27 | ludus_veeam_vbr_iso_download: 28 | type: "boolean" 29 | required: false 30 | default: true 31 | description: "Download Veeam ISO from Internet during installation" 32 | 33 | ludus_veeam_vbr_sql_install_username: 34 | type: "string" 35 | required: false 36 | default: "sql_install" 37 | description: "Username for SQL Server installation" 38 | 39 | ludus_veeam_vbr_sql_install_password: 40 | type: "string" 41 | required: false 42 | default: "ChangeM3!" 43 | description: "Password for SQL Server installation user" 44 | 45 | ludus_veeam_vbr_sql_service_username: 46 | type: "string" 47 | required: false 48 | default: "svc_sql" 49 | description: "Username for SQL Server service account" 50 | 51 | ludus_veeam_vbr_sql_service_password: 52 | type: "string" 53 | required: false 54 | default: "ChangeM3!" 55 | description: "Password for SQL Server service account" 56 | 57 | ludus_veeam_vbr_sql_username: 58 | type: "string" 59 | required: false 60 | default: "postgres" 61 | description: "Database username for Veeam application" 62 | 63 | ludus_veeam_vbr_sql_password: 64 | type: "string" 65 | required: false 66 | default: "ChangeM3!" 67 | description: "Database password for Veeam application" 68 | 69 | ludus_veeam_vbr_sql_authentication: 70 | type: "string" 71 | required: false 72 | default: "1" 73 | description: "SQL Server authentication mode (1 = SQL Server authentication)" 74 | -------------------------------------------------------------------------------- /base-configs/fake-config-files-lab.yml: -------------------------------------------------------------------------------- 1 | ludus: 2 | - vm_name: "{{ range_id }}-winfileserver-2022" 3 | hostname: "{{ range_id }}-FS01-2022" 4 | template: win2022-server-x64-template 5 | vlan: 10 6 | ip_last_octet: 40 7 | ram_gb: 4 8 | cpus: 4 9 | windows: 10 | sysprep: false 11 | domain: 12 | fqdn: ludus.domain 13 | role: member 14 | testing: 15 | snapshot: true 16 | block_internet: true 17 | roles: 18 | - mojeda101.ludus_fake_configs 19 | role_vars: 20 | # Where to place the fake config files (multi-path supported) 21 | fake_cfg_paths: 22 | - 'C:\shares\all' 23 | - 'C:\shares\public' 24 | - 'C:\temp\configs' 25 | 26 | # Create the fake directories if missing 27 | fake_cfg_create_path: true 28 | 29 | # Recreate each deploy (set to false later for idempotence) 30 | fake_cfg_force: true 31 | fake_cfg_force_mode: matching # or 'all' to wipe everything under those paths 32 | 33 | # How many files to end up with (per path) 34 | fake_cfg_count: 120 35 | 36 | fake_cfg_prefix: "config" 37 | 38 | # File size range 39 | fake_cfg_min_kib: 1 40 | fake_cfg_max_kib: 32 41 | 42 | # Backdate timestamps to look realistic 43 | fake_cfg_date_back_days_max: 120 44 | 45 | # File extensions to generate 46 | fake_cfg_extensions: 47 | - ini 48 | - yaml 49 | - yml 50 | - json 51 | - xml 52 | - conf 53 | - properties 54 | 55 | # Optional subfolders 56 | fake_cfg_subdirs: 57 | - app 58 | - services 59 | - agents 60 | - drivers 61 | - development 62 | - configs 63 | 64 | # Embed creds in some files (for deception) 65 | fake_cfg_credentials: 66 | username: "adminuser" 67 | password: "P@ssw0rd123" 68 | fake_cfg_creds_embed_mode: ratio # none | all | count | ratio 69 | fake_cfg_creds_embed_ratio: 0.10 # ~10% of newly created files 70 | 71 | # File extensions eligible for credential embedding 72 | fake_cfg_creds_embed_exts: 73 | - ini 74 | - yaml 75 | - yml 76 | - json 77 | - xml 78 | - conf 79 | - properties 80 | 81 | # Filenames that will always get credentials embedded 82 | fake_cfg_creds_embed_names: 83 | - "(?i)credential" 84 | - "(?i)auth" 85 | - "(?i)login" 86 | - "(?i)secrets?" 87 | -------------------------------------------------------------------------------- /src/tools/getTags.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { Logger } from '../utils/logger.js'; 3 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 4 | 5 | export interface GetTagsArgs { 6 | user?: string; 7 | help?: boolean; 8 | } 9 | 10 | export function createGetTagsTool(logger: Logger, cliWrapper: LudusCliWrapper): Tool { 11 | return { 12 | name: 'get_tags', 13 | description: 'Get the ansible tags available for use with deploy. This shows all available deployment tags that can be used with the deploy_range tool. Requires admin privileges to get tags for other users.', 14 | inputSchema: { 15 | type: 'object', 16 | properties: { 17 | user: { 18 | type: 'string', 19 | description: 'User ID to get available tags for (admin only). If omitted, gets tags for current user.' 20 | }, 21 | help: { 22 | type: 'boolean', 23 | description: 'Show help information for the get_tags command', 24 | default: false 25 | } 26 | }, 27 | required: [] 28 | } 29 | }; 30 | } 31 | 32 | export async function handleGetTags( 33 | args: GetTagsArgs, 34 | logger: Logger, 35 | cliWrapper: LudusCliWrapper 36 | ): Promise { 37 | const { user, help = false } = args; 38 | 39 | // Handle help request 40 | if (help) { 41 | logger.info('Getting help for get_tags command', { user }); 42 | const result = await cliWrapper.executeArbitraryCommand('range', ['gettags', '--help']); 43 | 44 | if (result.success) { 45 | return { 46 | success: true, 47 | message: 'Help information for get_tags command', 48 | help: true, 49 | content: result.rawOutput || result.message 50 | }; 51 | } else { 52 | throw new Error(`Failed to get help: ${result.message}`); 53 | } 54 | } 55 | 56 | try { 57 | logger.info('Getting available deployment tags', { user }); 58 | 59 | const result = await cliWrapper.getTags(user); 60 | 61 | if (!result.success) { 62 | throw new Error(`Failed to get deployment tags: ${result.message}`); 63 | } 64 | 65 | const targetUser = user || 'current user'; 66 | 67 | return { 68 | success: true, 69 | message: `Available deployment tags retrieved for ${targetUser}`, 70 | user: targetUser, 71 | tags: result.data, 72 | rawOutput: result.rawOutput 73 | }; 74 | } catch (error: any) { 75 | logger.error('Get tags failed', { error: error.message, user }); 76 | throw error; 77 | } 78 | } -------------------------------------------------------------------------------- /schemas/mojeda101.ludus_adtimeline_syncthing.yaml: -------------------------------------------------------------------------------- 1 | name: mojeda101.ludus_adtimeline_syncthing 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs ADTimeline Splunk app in a Splunk Docker container with Syncthing sidecar for automatic ingestion folder synchronization. Used for personal testing and learning ADTimeline in labs" 5 | repository: "https://github.com/mojeda101/ludus_adtimeline_syncthing" 6 | author: "mojeda101 (@mojeda101)" 7 | 8 | dependencies: 9 | - "geerlingguy.docker" 10 | - "community.docker" 11 | 12 | installation_method: "ludus ansible role add mojeda101.ludus_adtimeline_syncthing" 13 | note: "Default Splunk web GUI credentials are admin:forensics. ADTimeline files can be placed in /opt/adtimeline/adtimeline_input to skip Syncthing" 14 | warning: "Syncthing web GUI has no authentication by default - configure manually. Permissions set for debian user with ID 1000:1000" 15 | 16 | variables: 17 | ludus_adtimeline_sync_puid: 18 | type: "integer" 19 | required: false 20 | default: 1000 21 | description: "Process user ID for Syncthing container" 22 | 23 | ludus_adtimeline_sync_pgid: 24 | type: "integer" 25 | required: false 26 | default: 1000 27 | description: "Process group ID for Syncthing container" 28 | 29 | ludus_adtimeline_sync_tz: 30 | type: "string" 31 | required: false 32 | default: "UTC" 33 | description: "Timezone for Syncthing container" 34 | example: 35 | - "UTC" 36 | - "America/New_York" 37 | - "Europe/London" 38 | 39 | ludus_adtimeline_sync_gui_port: 40 | type: "integer" 41 | required: false 42 | default: 8384 43 | description: "Port for Syncthing web GUI" 44 | 45 | ludus_adtimeline_sync_port_tcp: 46 | type: "integer" 47 | required: false 48 | default: 22000 49 | description: "TCP port for Syncthing synchronization" 50 | 51 | ludus_adtimeline_sync_port_udp: 52 | type: "integer" 53 | required: false 54 | default: 22000 55 | description: "UDP port for Syncthing synchronization" 56 | 57 | ludus_adtimeline_sync_discovery_port: 58 | type: "integer" 59 | required: false 60 | default: 21027 61 | description: "Port for Syncthing local discovery" 62 | 63 | ludus_adtimeline_splunk_web_port: 64 | type: "integer" 65 | required: false 66 | default: 8000 67 | description: "Port for Splunk web interface" 68 | 69 | ludus_adtimeline_splunk_password: 70 | type: "string" 71 | required: false 72 | default: "forensics" 73 | description: "Password for Splunk admin user (username: admin)" 74 | -------------------------------------------------------------------------------- /schemas/aleemladha.ludus_exchange2016.yaml: -------------------------------------------------------------------------------- 1 | name: aleemladha.ludus_exchange2016 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Microsoft Exchange Server 2016" 5 | repository: "https://github.com/aleemladha/ludus_exchange2016" 6 | author: "Aleem Ladha (@LadhaAleem)" 7 | installation_method: "ludus ansible role add aleemladha.ludus_exchange2016" 8 | dependencies: [] 9 | 10 | variables: 11 | exchange_dotnet_install_path: 12 | type: "string" 13 | default: "https://download.visualstudio.microsoft.com/download/pr/2d6bb6b2-226a-4baa-bdec-798822606ff1/8494001c276a4b96804cde7829c04d7f/ndp48-x86-x64-allos-enu.exe" 14 | description: ".NET Framework installer URL" 15 | 16 | vcredist2013_install_path: 17 | type: "string" 18 | default: "https://download.microsoft.com/download/2/E/6/2E61CFA4-993B-4DD4-91DA-3737CD5CD6E3/vcredist_x64.exe" 19 | description: "Visual C++ 2013 redistributable URL" 20 | 21 | rewrite_module_path: 22 | type: "string" 23 | default: "https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi" 24 | description: "IIS URL Rewrite module URL" 25 | 26 | ucma_runtime_path: 27 | type: "string" 28 | default: "https://download.microsoft.com/download/2/C/4/2C47A5C1-A1F3-4843-B9FE-84C0032C61EC/UcmaRuntimeSetup.exe" 29 | description: "UCMA runtime setup URL" 30 | 31 | exchange_iso_url: 32 | type: "string" 33 | default: "https://download.microsoft.com/download/2/5/8/258D30CF-CA4C-433A-A618-FB7E6BCC4EEE/ExchangeServer2016-x64-cu12.iso" 34 | description: "Exchange 2016 ISO download URL" 35 | 36 | exchange_iso_path: 37 | type: "string" 38 | default: "C:\\exchange2016\\ExchangeServer2016-x64-cu12.iso" 39 | description: "Local path for Exchange ISO" 40 | 41 | ludus_exchange_domain: 42 | type: "string" 43 | default: "{{ auto-detected }}" 44 | description: "Auto-detected domain name from Ludus config" 45 | 46 | ludus_exchange_dc: 47 | type: "string" 48 | default: "{{ auto-detected }}" 49 | description: "Auto-detected primary DC hostname" 50 | 51 | ludus_exchange_host: 52 | type: "string" 53 | default: "{{ auto-detected }}" 54 | description: "Auto-detected host from Ludus config" 55 | 56 | ludus_exchange_domain_username: 57 | type: "string" 58 | default: "{{ auto-detected }}" 59 | description: "Domain admin username" 60 | 61 | ludus_exchange_domain_password: 62 | type: "string" 63 | default: "{{ auto-detected }}" 64 | description: "Domain admin password" -------------------------------------------------------------------------------- /schemas/ludus_child_domain.yaml: -------------------------------------------------------------------------------- 1 | name: ludus_child_domain 2 | type: role 3 | version: "1.0.0" 4 | description: "Create a child domain and domain controller using microsoft.ad.child_domain module" 5 | repository: "https://github.com/ChoiSG/ludus_ansible_roles" 6 | author: "ChoiSG (@_choisec)" 7 | 8 | dependencies: [] 9 | 10 | installation_method: "ludus ansible role add -d /path/to/directory" 11 | 12 | note: "Must install from directory - not Ansible Galaxy. Do NOT use with primary-dc or secondary-dc roles when creating child domains." 13 | 14 | IMPORTANT_DOMAIN_CONFIG_NOTE: "DO NOT include 'domain:' section in VM configuration when using ludus_child_domain role. The role handles domain creation internally. Example: Do NOT add 'domain: { fqdn: prod.test.local, role: member }' to VMs using this role." 15 | 16 | variables: 17 | dns_domain_name: 18 | type: "string" 19 | default: "test.dev.raccoon" 20 | description: "The full DNS name of the child domain to create" 21 | 22 | domain_admin_user: 23 | type: "string" 24 | default: "domainadmin@dev.raccoon" 25 | description: "Username of a domain admin for the parent domain" 26 | 27 | domain_admin_password: 28 | type: "string" 29 | default: "password" 30 | description: "Password for the specified parent domain admin user" 31 | 32 | safe_mode_password: 33 | type: "string" 34 | default: "password" 35 | description: "Safe mode password for the domain controller" 36 | 37 | parent_dc_ip: 38 | type: "string" 39 | default: "1.1.1.1" 40 | description: "IP address of the parent domain controller" 41 | 42 | current_host_ip: 43 | type: "string" 44 | default: "2.2.2.2" 45 | description: "IP address of the current host that will become the child domain controller" 46 | 47 | create_dns_delegation: 48 | type: "boolean" 49 | default: true 50 | description: "Whether to create a DNS delegation that references the new DNS server. Valid for Active Directory-integrated DNS only" 51 | 52 | domain_mode: 53 | type: "string" 54 | default: "WinThreshold" 55 | description: "Specifies the domain functional level of the child domain. Cannot be lower than the forest functional level" 56 | valid_options: 57 | - "Win2003" 58 | - "Win2008" 59 | - "Win2008R2" 60 | - "Win2012" 61 | - "Win2012R2" 62 | - "WinThreshold" 63 | - "Win2025" 64 | 65 | reboot: 66 | type: "boolean" 67 | default: true 68 | description: "Whether to reboot the host after domain controller installation if required" 69 | 70 | install_dns: 71 | type: "boolean" 72 | default: true 73 | description: "Whether to install the DNS service when creating the domain controller" -------------------------------------------------------------------------------- /schemas/ludus-ad-vulns.yaml: -------------------------------------------------------------------------------- 1 | name: ludus-ad-vulns 2 | type: role 3 | version: 1.0.0 4 | description: Configures various Active Directory vulnerabilities 5 | repository: https://github.com/Primusinterp/ludus-ad-vulns 6 | author: Primusinterp 7 | installation_method: "ludus ansible role add -d /path/to/directory" 8 | dependencies: [] 9 | variables: 10 | user.identity: 11 | type: string 12 | default: "fives" 13 | description: "SAM account name for Kerberoastable user" 14 | user.service_principal_name: 15 | type: string 16 | default: "HTTP/ArcTraining" 17 | description: "Service Principal Name (SPN) value" 18 | machine_name: 19 | type: string 20 | default: "maldev-srv1-2022" 21 | description: "Machine name for unconstrained delegation" 22 | acl.value.for: 23 | type: string 24 | default: "cptrex" 25 | description: "Object to assign ACL for" 26 | acl.value.to: 27 | type: string 28 | default: "CN=Bounty Hunters,OU=Bounty Hunters,DC=maldev,DC=local" 29 | description: "Object to assign ACL to" 30 | acl.value.right: 31 | type: string 32 | default: "GenericAll" 33 | description: "ACL to apply" 34 | acl.value.inheritance: 35 | type: string 36 | default: "None" 37 | description: "ACL inheritance setting" 38 | ludus_ad_vulns_openshares: 39 | type: boolean 40 | default: false 41 | description: "Enable open shares vulnerability" 42 | ludus_ad_vulns_kerberoasting: 43 | type: boolean 44 | default: false 45 | description: "Enable Kerberoasting vulnerability" 46 | ludus_ad_vulns_unconstrained_delegation_user: 47 | type: boolean 48 | default: false 49 | description: "Enable unconstrained delegation for user" 50 | ludus_ad_vulns_set_acl: 51 | type: boolean 52 | default: false 53 | description: "Enable ACL vulnerability" 54 | ludus_ad_vulns_unconstrained_delegation_machine: 55 | type: boolean 56 | default: false 57 | description: "Enable unconstrained delegation for machine" 58 | ludus_domain_val: 59 | type: string 60 | default: "{{ (ludus | selectattr('vm_name', 'match', inventory_hostname))[0].domain.fqdn.split('.')[0] }}" 61 | description: "Domain short name extracted from VM configuration. Hard code full domain name for child domains such as ludus_domain_val: \"dev.nocte.defensor\"" 62 | ludus_AD_domain_admin: 63 | type: string 64 | default: "{{ ludus_domain_val }}\\{{ defaults.ad_domain_admin }}" 65 | description: "Fully qualified domain admin username. Hard code these for child domains such as ludus_AD_domain_admin: \"dev.nocte.defensor\\administrator\"" 66 | ludus_AD_domain_admin_password: 67 | type: string 68 | default: "{{ defaults.ad_domain_admin_password }}" 69 | description: "Domain admin password. Hard code these for child domains such as ludus_AD_domain_admin_password: \"password\"" -------------------------------------------------------------------------------- /base-configs/malware-analysis-lab.yml: -------------------------------------------------------------------------------- 1 | ludus: 2 | # Windows 11 Malware Analysis Workstation 1 3 | - vm_name: "{{ range_id }}-malware-ws01" 4 | hostname: "{{ range_id }}-MAL-WS01" 5 | template: win11-22h2-x64-enterprise-template 6 | vlan: 10 7 | ip_last_octet: 10 8 | ram_gb: 8 9 | cpus: 4 10 | windows: 11 | sysprep: false 12 | install_additional_tools: true 13 | chocolatey_ignore_checksums: true 14 | chocolatey_packages: 15 | - notepadplusplus 16 | - 7zip 17 | - wireshark 18 | - procmon 19 | - autoruns 20 | testing: 21 | snapshot: true 22 | block_internet: false 23 | roles: 24 | - professor-moody.ludus_litterbox 25 | role_vars: 26 | ludus_litterbox_install: true 27 | ludus_litterbox_host: "0.0.0.0" # Allow network access 28 | ludus_litterbox_port: 1337 29 | ludus_litterbox_disable_defender: true 30 | ludus_litterbox_analysis_timeout: 600 # 10 minutes 31 | ludus_litterbox_debug: false 32 | ludus_litterbox_enable_static: true 33 | ludus_litterbox_enable_dynamic: true 34 | ludus_litterbox_enable_yara: true 35 | 36 | # Windows Server 2022 Analysis Server 37 | - vm_name: "{{ range_id }}-malware-srv01" 38 | hostname: "{{ range_id }}-MAL-SRV01" 39 | template: win2022-server-x64-template 40 | vlan: 10 41 | ip_last_octet: 11 42 | ram_gb: 8 43 | cpus: 4 44 | windows: 45 | sysprep: false 46 | install_additional_tools: true 47 | testing: 48 | snapshot: true 49 | block_internet: false 50 | roles: 51 | - professor-moody.ludus_litterbox 52 | role_vars: 53 | ludus_litterbox_install: true 54 | ludus_litterbox_host: "0.0.0.0" 55 | ludus_litterbox_port: 8080 # Different port 56 | ludus_litterbox_disable_defender: true 57 | ludus_litterbox_install_dir: "D:\\Security\\LitterBox" 58 | ludus_litterbox_analysis_timeout: 900 # 15 minutes 59 | ludus_litterbox_workers: 6 60 | ludus_litterbox_max_file_size: 209715200 # 200MB 61 | ludus_litterbox_cleanup_days: 7 # Shorter retention 62 | 63 | # Windows 10 Isolated Analysis Box 64 | - vm_name: "{{ range_id }}-malware-iso01" 65 | hostname: "{{ range_id }}-MAL-ISO01" 66 | template: win10-22h2-x64-enterprise-template 67 | vlan: 10 68 | ip_last_octet: 12 69 | ram_gb: 4 70 | cpus: 2 71 | windows: 72 | sysprep: false 73 | testing: 74 | snapshot: true 75 | block_internet: true # Isolated for dangerous samples 76 | roles: 77 | - professor-moody.ludus_litterbox 78 | role_vars: 79 | ludus_litterbox_install: true 80 | ludus_litterbox_host: "127.0.0.1" # Local only 81 | ludus_litterbox_port: 1337 82 | ludus_litterbox_disable_defender: true 83 | ludus_litterbox_enable_dynamic: false # Static only for isolation 84 | ludus_litterbox_debug: true 85 | ludus_litterbox_log_level: "DEBUG" 86 | -------------------------------------------------------------------------------- /src/tools/getRangeStatus.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { Logger } from '../utils/logger.js'; 3 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 4 | 5 | export interface GetRangeStatusArgs { 6 | user?: string; 7 | help?: boolean; 8 | } 9 | 10 | export function createGetRangeStatusTool(logger: Logger, cliWrapper: LudusCliWrapper): Tool { 11 | return { 12 | name: 'get_range_status', 13 | description: 'Get the current status and details of a Ludus range. Shows deployment state, VM information, and range configuration. Requires admin privileges to check other users\' ranges.', 14 | inputSchema: { 15 | type: 'object', 16 | properties: { 17 | user: { 18 | type: 'string', 19 | description: 'User ID to get range status for (admin only). If omitted, gets status for current user.' 20 | }, 21 | help: { 22 | type: 'boolean', 23 | description: 'Show help information for the get_range_status command', 24 | default: false 25 | } 26 | }, 27 | required: [] 28 | } 29 | }; 30 | } 31 | 32 | export async function handleGetRangeStatus( 33 | args: GetRangeStatusArgs, 34 | logger: Logger, 35 | cliWrapper: LudusCliWrapper 36 | ): Promise { 37 | const { user, help = false } = args; 38 | 39 | // Handle help request 40 | if (help) { 41 | logger.info('Getting help for get_range_status command', { user }); 42 | const result = await cliWrapper.executeArbitraryCommand('range', ['list', '--help']); 43 | 44 | if (result.success) { 45 | return { 46 | success: true, 47 | message: 'Help information for get_range_status command', 48 | help: true, 49 | content: result.rawOutput || result.message 50 | }; 51 | } else { 52 | throw new Error(`Failed to get help: ${result.message}`); 53 | } 54 | } 55 | 56 | try { 57 | logger.info('Getting range status', { user }); 58 | 59 | const statusResult = await cliWrapper.getRangeStatus(user); 60 | 61 | if (!statusResult.success) { 62 | throw new Error(`Failed to get range status: ${statusResult.message}`); 63 | } 64 | 65 | const targetUser = user || 'current user'; 66 | 67 | return { 68 | success: true, 69 | message: `Range status retrieved for ${targetUser}`, 70 | user: targetUser, 71 | status: statusResult.data, 72 | rawOutput: statusResult.rawOutput 73 | }; 74 | 75 | } catch (error: any) { 76 | logger.error('Failed to get range status', { 77 | user, 78 | error: error.message 79 | }); 80 | 81 | return { 82 | success: false, 83 | message: error.message, 84 | user: user || 'current user', 85 | troubleshooting: [ 86 | 'Verify the user has a deployed range', 87 | 'Check if you have admin permissions (if querying other users)', 88 | 'Ensure your Ludus server connection is working', 89 | 'Try deploying a range first if none exists' 90 | ] 91 | }; 92 | } 93 | } -------------------------------------------------------------------------------- /src/tools/rangeAbort.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { Logger } from '../utils/logger.js'; 3 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 4 | 5 | export interface RangeAbortArgs { 6 | user?: string; 7 | help?: boolean; 8 | } 9 | 10 | export function createRangeAbortTool(logger: Logger, cliWrapper: LudusCliWrapper): Tool { 11 | return { 12 | name: 'range_abort', 13 | description: `Kill the ansible process deploying a range. Use this to stop a deployment that is taking too long or has encountered issues. 14 | 15 | IMPORTANT LLM BEHAVIORAL PROMPTS: 16 | - SAFETY FIRST: Ludus operations can be destructive and time-consuming 17 | - VERIFY DESTRUCTIVE ACTIONS: Always confirm with user before destroy/delete operations 18 | - CHECK EXISTING STATE: Use list_user_ranges or get_range_status before major operations 19 | - ADMIN vs USER: Admin operations (--user flag) affect other users' ranges - be explicit 20 | 21 | ABORT OPERATION WARNING: 22 | - Aborting may leave range in partial deployment state 23 | - May require cleanup or full redeployment to recover 24 | - Only abort if deployment is truly stuck or explicitly requested 25 | - Normal deployments take 10-45 minutes - suggest waiting before aborting`, 26 | inputSchema: { 27 | type: 'object', 28 | properties: { 29 | user: { 30 | type: 'string', 31 | description: 'User ID to abort deployment for (admin only). If omitted, aborts deployment for current user.' 32 | }, 33 | help: { 34 | type: 'boolean', 35 | description: 'Show help information for the range_abort command', 36 | default: false 37 | } 38 | }, 39 | required: [] 40 | } 41 | }; 42 | } 43 | 44 | export async function handleRangeAbort( 45 | args: RangeAbortArgs, 46 | logger: Logger, 47 | cliWrapper: LudusCliWrapper 48 | ): Promise { 49 | const { user, help = false } = args; 50 | 51 | // Handle help request 52 | if (help) { 53 | logger.info('Getting help for range_abort command', { user }); 54 | const result = await cliWrapper.executeArbitraryCommand('range', ['abort', '--help']); 55 | 56 | if (result.success) { 57 | return { 58 | success: true, 59 | message: 'Help information for range_abort command', 60 | help: true, 61 | content: result.rawOutput || result.message 62 | }; 63 | } else { 64 | throw new Error(`Failed to get help: ${result.message}`); 65 | } 66 | } 67 | 68 | try { 69 | logger.info('Aborting range deployment', { user }); 70 | 71 | const result = await cliWrapper.abortRange(user); 72 | 73 | if (!result.success) { 74 | throw new Error(`Failed to abort range deployment: ${result.message}`); 75 | } 76 | 77 | const targetUser = user || 'current user'; 78 | 79 | return { 80 | success: true, 81 | message: `Range deployment aborted for ${targetUser}`, 82 | user: targetUser, 83 | result: result.data, 84 | rawOutput: result.rawOutput 85 | }; 86 | } catch (error: any) { 87 | logger.error('Range abort failed', { error: error.message, user }); 88 | throw error; 89 | } 90 | } -------------------------------------------------------------------------------- /schemas/netpenguins.ludus_k3s.yaml: -------------------------------------------------------------------------------- 1 | name: netpenguins.ludus_k3s 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs and configures a lightweight kubernetes (k3s) cluster on Linux hosts. Supports server (control plane) and agent (worker) installations" 5 | repository: "https://github.com/netpenguins/ludus_k3s" 6 | author: "netpenguins (@netpenguins)" 7 | 8 | installation_method: "ludus ansible role add netpenguins.ludus_k3s" 9 | note: "Targets Linux hosts (Ubuntu/Debian). Can deploy dashboard with kubernetes or headlamp options. Headlamp desktop app recommended for Windows management hosts" 10 | 11 | variables: 12 | ludus_k3s_role: 13 | type: "string" 14 | required: false 15 | default: "server" 16 | valid_options: 17 | - "server" 18 | - "agent" 19 | description: "Install mode: server (control plane) or agent (worker)" 20 | 21 | ludus_k3s_version: 22 | type: "string" 23 | required: false 24 | default: "" 25 | description: "k3s version to install (empty = latest stable)" 26 | example: 27 | - "" 28 | - "v1.28.4+k3s1" 29 | 30 | ludus_k3s_server_args: 31 | type: "string" 32 | required: false 33 | default: "" 34 | description: "Extra arguments passed to k3s server" 35 | 36 | ludus_k3s_agent_args: 37 | type: "string" 38 | required: false 39 | default: "" 40 | description: "Extra arguments passed to k3s agent" 41 | 42 | ludus_k3s_server_url: 43 | type: "string" 44 | required: false 45 | default: "" 46 | description: "k3s server URL (required when role is agent)" 47 | example: 48 | - "https://10.{{ range_second_octet }}.10.10:6443" 49 | 50 | ludus_k3s_token: 51 | type: "string" 52 | required: false 53 | default: "" 54 | description: "Cluster join token (servers auto-generate if blank, required for agents)" 55 | 56 | ludus_k3s_disable_components: 57 | type: "array" 58 | required: false 59 | default: [] 60 | description: "List of components to disable" 61 | valid_options: 62 | - "traefik" 63 | - "servicelb" 64 | - "local-storage" 65 | - "metrics-server" 66 | example: 67 | - ["traefik", "servicelb"] 68 | 69 | ludus_k3s_kubeconfig_mode: 70 | type: "string" 71 | required: false 72 | default: "644" 73 | description: "File mode for written kubeconfig" 74 | 75 | ludus_k3s_install_dir: 76 | type: "string" 77 | required: false 78 | default: "/usr/local/bin" 79 | description: "Directory to install k3s binary" 80 | 81 | ludus_k3s_data_dir: 82 | type: "string" 83 | required: false 84 | default: "/var/lib/rancher/k3s" 85 | description: "k3s data directory" 86 | 87 | ludus_k3s_dashboard: 88 | type: "boolean" 89 | required: false 90 | default: false 91 | description: "Whether to install a dashboard" 92 | 93 | ludus_k3s_dashboard_type: 94 | type: "string" 95 | required: false 96 | default: "" 97 | valid_options: 98 | - "kubernetes" 99 | - "headlamp" 100 | - "" 101 | description: "Dashboard type: kubernetes (default k8s dashboard), headlamp, or empty (none)" 102 | -------------------------------------------------------------------------------- /src/tools/ludusRolesSearch.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import fs from 'fs/promises'; 3 | import path from 'path'; 4 | import os from 'os'; 5 | import { Logger } from '../utils/logger.js'; 6 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 7 | 8 | const LudusRolesSchema = z.object({ 9 | help: z.boolean().optional().default(false).describe('Show help information') 10 | }); 11 | 12 | export function createLudusRolesDocsReadTool(logger: Logger, ludusCliWrapper: LudusCliWrapper) { 13 | return { 14 | name: 'ludus_roles_docs_read', 15 | description: `**ROLES DOCUMENTATION** - Direct Access to Complete Ludus Roles Documentation 16 | 17 | **DIRECT ACCESS TO:** 18 | - Complete roles listing and descriptions 19 | - Role variables and configuration options 20 | - Role dependencies and requirements 21 | - GitHub repositories and sources 22 | - Installation and usage examples 23 | 24 | **SIMPLIFIED FUNCTIONALITY:** 25 | - Always returns the complete roles documentation (docs/roles.md) 26 | - No search filtering - reads entire document 27 | - Immediate access to all role information 28 | 29 | **PERFECT FOR:** 30 | - Range planning (ludus_range_planner step 3) 31 | - Role research and selection 32 | - Finding role variables and GitHub links 33 | - Understanding role capabilities 34 | 35 | **TARGET**: Focuses specifically on \`docs/roles.md\` and related role documentation.`, 36 | inputSchema: { 37 | type: 'object', 38 | properties: { 39 | help: { 40 | type: 'boolean', 41 | default: false, 42 | description: 'Show help information' 43 | } 44 | }, 45 | required: [] 46 | }, 47 | }; 48 | } 49 | 50 | export async function handleLudusRolesDocsRead(args: any): Promise { 51 | try { 52 | const { help = false } = args; 53 | 54 | if (help) { 55 | return { 56 | content: [{ 57 | type: 'text', 58 | text: 'ludus_roles_docs_read - Read complete Ludus roles documentation\n\nThis tool reads the entire docs/roles.md file and returns all role information.\nNo parameters required - always returns complete documentation.' 59 | }] 60 | }; 61 | } 62 | 63 | // Always read the complete roles documentation 64 | const homeDir = os.homedir(); 65 | const docsDir = path.join(homeDir, '.ludus-mcp', 'docs'); 66 | const rolesFilePath = path.join(docsDir, 'roles.md'); 67 | 68 | try { 69 | const rolesContent = await fs.readFile(rolesFilePath, 'utf-8'); 70 | 71 | return { 72 | content: [{ 73 | type: 'text', 74 | text: `# Complete Ludus Roles Documentation\n\n${rolesContent}` 75 | }] 76 | }; 77 | } catch (error) { 78 | return { 79 | content: [{ 80 | type: 'text', 81 | text: `Error reading roles documentation: ${error instanceof Error ? error.message : 'Unknown error'}\n\nThe documentation may not be downloaded yet. Try running the server startup process to download docs.` 82 | }] 83 | }; 84 | } 85 | 86 | } catch (error) { 87 | return { 88 | content: [{ 89 | type: 'text', 90 | text: `Error in ludus_roles_docs_read: ${error instanceof Error ? error.message : 'Unknown error'}` 91 | }] 92 | }; 93 | } 94 | } -------------------------------------------------------------------------------- /src/tools/listAllUsers.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { Logger } from '../utils/logger.js'; 3 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 4 | 5 | export function createListAllUsersTool(logger: Logger, cliWrapper: LudusCliWrapper): Tool { 6 | return { 7 | name: 'list_all_users', 8 | description: 'List all users in the Ludus system (admin operation). Use this when you need to see all users, not just the current user. For individual user info, use the generic ludus_cli_execute tool with "users list".', 9 | inputSchema: { 10 | type: 'object', 11 | properties: { 12 | help: { 13 | type: 'boolean', 14 | description: 'Show help information for the list_all_users command', 15 | default: false 16 | } 17 | }, 18 | additionalProperties: false 19 | } 20 | }; 21 | } 22 | 23 | export async function handleListAllUsers( 24 | args: { help?: boolean }, 25 | logger: Logger, 26 | cliWrapper: LudusCliWrapper 27 | ): Promise { 28 | const { help = false } = args; 29 | 30 | // Handle help request 31 | if (help) { 32 | logger.info('Getting help for list_all_users command'); 33 | const result = await cliWrapper.executeArbitraryCommand('users', ['list', '--help']); 34 | 35 | if (result.success) { 36 | return { 37 | success: true, 38 | message: 'Help information for list_all_users command', 39 | help: true, 40 | content: result.rawOutput || result.message, 41 | usage: 'This tool runs "ludus users list all" to show all users in the system. No additional parameters needed.' 42 | }; 43 | } else { 44 | throw new Error(`Failed to get help: ${result.message}`); 45 | } 46 | } 47 | 48 | try { 49 | logger.info('Listing all users in the system'); 50 | 51 | // Use the new listAllUsers method 52 | const result = await cliWrapper.listAllUsers(); 53 | 54 | if (!result.success) { 55 | throw new Error(result.message); 56 | } 57 | 58 | // Parse the response if it's JSON 59 | let userData = result.data; 60 | if (typeof result.data === 'string') { 61 | try { 62 | userData = JSON.parse(result.data); 63 | } catch { 64 | // Not JSON, use raw data 65 | userData = result.data; 66 | } 67 | } 68 | 69 | return { 70 | success: true, 71 | message: 'Successfully retrieved all users', 72 | users: userData, 73 | rawOutput: result.rawOutput, 74 | usage: [ 75 | 'This shows all users in the Ludus system', 76 | 'For individual user details, use ludus_cli_execute with "users list --user "', 77 | 'To add/remove users, use ludus_cli_execute with "users add" or "users rm" commands' 78 | ] 79 | }; 80 | 81 | } catch (error: any) { 82 | logger.error('Failed to list all users', { error: error.message }); 83 | return { 84 | success: false, 85 | message: `Failed to list all users: ${error.message}`, 86 | troubleshooting: [ 87 | 'Ensure you have admin privileges to list all users', 88 | 'Check if the Ludus server is accessible', 89 | 'Verify your API key has the necessary permissions', 90 | 'Try using ludus_help with "users list" for more options' 91 | ] 92 | }; 93 | } 94 | } -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { FileLogger } from './fileLogger.js'; 2 | 3 | /** 4 | * Logger utility for structured logging with different levels 5 | */ 6 | export class Logger { 7 | private context: string; 8 | private isDebugEnabled: boolean; 9 | private fileLogger?: FileLogger; 10 | private isFileLoggingEnabled: boolean; 11 | 12 | constructor(context: string) { 13 | this.context = context; 14 | this.isDebugEnabled = process.env.LUDUS_DEBUG === 'true' || process.env.NODE_ENV === 'development'; 15 | // Always enable file logging for debugging purposes 16 | this.isFileLoggingEnabled = true; 17 | 18 | if (this.isFileLoggingEnabled) { 19 | try { 20 | this.fileLogger = new FileLogger('ludus-mcp'); 21 | } catch (error) { 22 | console.error('Failed to initialize file logging:', error); 23 | } 24 | } 25 | } 26 | 27 | /** 28 | * Log debug messages (only in debug mode) 29 | */ 30 | debug(message: string, meta?: Record): void { 31 | if (this.isDebugEnabled) { 32 | this.log('DEBUG', message, meta); 33 | } 34 | } 35 | 36 | /** 37 | * Log info messages 38 | */ 39 | info(message: string, meta?: Record): void { 40 | this.log('INFO', message, meta); 41 | } 42 | 43 | /** 44 | * Log warning messages 45 | */ 46 | warn(message: string, meta?: Record): void { 47 | this.log('WARN', message, meta); 48 | } 49 | 50 | /** 51 | * Log error messages 52 | */ 53 | error(message: string, error?: Error | unknown, meta?: Record): void { 54 | const errorMeta = error instanceof Error 55 | ? { 56 | message: error.message, 57 | stack: error.stack, 58 | name: error.name, 59 | ...meta 60 | } 61 | : { error: String(error), ...meta }; 62 | 63 | this.log('ERROR', message, errorMeta); 64 | } 65 | 66 | /** 67 | * Core logging method 68 | */ 69 | private log(level: string, message: string, meta?: Record): void { 70 | const timestamp = new Date().toISOString(); 71 | const logEntry = { 72 | timestamp, 73 | level, 74 | context: this.context, 75 | message, 76 | ...(meta && Object.keys(meta).length > 0 ? { meta } : {}), 77 | }; 78 | 79 | // Always write to file if enabled 80 | if (this.fileLogger) { 81 | this.fileLogger.log(level, this.context, message, meta); 82 | } 83 | 84 | // Output all logs to stderr to avoid interfering with MCP protocol on stdout 85 | const output = console.error; 86 | 87 | if (process.env.LUDUS_LOG_FORMAT === 'json') { 88 | output(JSON.stringify(logEntry)); 89 | } else { 90 | const metaStr = meta && Object.keys(meta).length > 0 91 | ? ` ${JSON.stringify(meta)}` 92 | : ''; 93 | output(`[${timestamp}] ${level} [${this.context}] ${message}${metaStr}`); 94 | } 95 | } 96 | 97 | /** 98 | * Get the file log path if file logging is enabled 99 | */ 100 | getLogPath(): string | null { 101 | return this.fileLogger?.getLogPath() || null; 102 | } 103 | 104 | /** 105 | * Get the log directory if file logging is enabled 106 | */ 107 | getLogDirectory(): string | null { 108 | return this.fileLogger?.getLogDirectory() || null; 109 | } 110 | } -------------------------------------------------------------------------------- /src/tools/listUserRanges.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { Logger } from '../utils/logger.js'; 3 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 4 | 5 | export interface ListUserRangesArgs { 6 | user?: string; 7 | help?: boolean; 8 | } 9 | 10 | export function createListUserRangesTool(logger: Logger, cliWrapper: LudusCliWrapper): Tool { 11 | return { 12 | name: 'list_user_ranges', 13 | description: 'List all ranges for a specific user or current user. Shows range details including status, configuration, and metadata. Requires admin privileges to list other users\' ranges.', 14 | inputSchema: { 15 | type: 'object', 16 | properties: { 17 | user: { 18 | type: 'string', 19 | description: 'User ID to list ranges for (admin only). If omitted, lists ranges for current user.' 20 | }, 21 | help: { 22 | type: 'boolean', 23 | description: 'Show help information for the list_user_ranges command', 24 | default: false 25 | } 26 | }, 27 | required: [] 28 | } 29 | }; 30 | } 31 | 32 | export async function handleListUserRanges( 33 | args: ListUserRangesArgs, 34 | logger: Logger, 35 | cliWrapper: LudusCliWrapper 36 | ): Promise { 37 | const { user, help = false } = args; 38 | 39 | // Handle help request 40 | if (help) { 41 | logger.info('Getting help for list_user_ranges command', { user }); 42 | const result = await cliWrapper.executeArbitraryCommand('range', ['list', '--help']); 43 | 44 | if (result.success) { 45 | return { 46 | success: true, 47 | message: 'Help information for list_user_ranges command', 48 | help: true, 49 | content: result.rawOutput || result.message 50 | }; 51 | } else { 52 | throw new Error(`Failed to get help: ${result.message}`); 53 | } 54 | } 55 | 56 | try { 57 | logger.info('Listing user ranges', { user }); 58 | 59 | const rangesResult = await cliWrapper.listUserRanges(user); 60 | 61 | if (!rangesResult.success) { 62 | throw new Error(`Failed to list ranges: ${rangesResult.message}`); 63 | } 64 | 65 | const targetUser = user || 'current user'; 66 | const ranges = rangesResult.data; 67 | 68 | // Provide helpful summary 69 | let summary = ''; 70 | if (Array.isArray(ranges)) { 71 | summary = `Found ${ranges.length} range(s) for ${targetUser}`; 72 | } else if (ranges) { 73 | summary = `Found range information for ${targetUser}`; 74 | } else { 75 | summary = `No ranges found for ${targetUser}`; 76 | } 77 | 78 | return { 79 | success: true, 80 | message: summary, 81 | user: targetUser, 82 | ranges: ranges, 83 | rawOutput: rangesResult.rawOutput, 84 | nextSteps: ranges ? [ 85 | 'Use get_range_status() to get detailed status information', 86 | 'Use get_connection_info() to get access credentials', 87 | 'Use deploy_range() to deploy a new range if needed' 88 | ] : [ 89 | 'Use deploy_range() to deploy your first range', 90 | 'Check available templates with list_templates()' 91 | ] 92 | }; 93 | 94 | } catch (error: any) { 95 | logger.error('Failed to list user ranges', { 96 | user, 97 | error: error.message 98 | }); 99 | 100 | return { 101 | success: false, 102 | message: error.message, 103 | user: user || 'current user', 104 | troubleshooting: [ 105 | 'Verify the user exists in the Ludus system', 106 | 'Check if you have admin permissions (if querying other users)', 107 | 'Ensure your Ludus server connection is working', 108 | 'Try deploying a range first if none exist' 109 | ] 110 | }; 111 | } 112 | } -------------------------------------------------------------------------------- /schemas/netpenguins.ludus_redirector.yaml: -------------------------------------------------------------------------------- 1 | # Schema for netpenguins.ludus_redirector Role 2 | # Apache-based redirector with SSL support and custom rewrite rules 3 | 4 | name: netpenguins.ludus_redirector 5 | type: role 6 | version: "1.0.0" 7 | description: "An Ansible role for Ludus to deploy and configure an Apache-based redirector, supporting custom rewrite rules. Designed for use in Ludus ranges and can be easily integrated with other roles and custom configurations." 8 | repository: "https://github.com/NetPenguins/ludus_redirector" 9 | author: "NetPenguins (@NetPenguins)" 10 | installation_method: "ludus ansible role add netpenguins.ludus_redirector" 11 | dependencies: [] 12 | 13 | installation_method: "galaxy" # ludus ansible role add netpenguins.ludus_redirector 14 | note: "Although many view apache2 modrewrite as overkill it is a solid skill to have when practicing opsec friendly infrastructure. This role gives the general skeleton of a standard redirector. The real fun is expanding on it!" 15 | warning: "This role is designed for red team exercises and penetration testing. Ensure proper authorization before deployment." 16 | 17 | variables: 18 | redirector_target_host: 19 | type: "string" 20 | required: false 21 | default: "c2.ludus" 22 | description: "Target host for proxy/rewrite operations. This is where traffic will be redirected to (typically your C2 server)." 23 | example: 24 | - "c2.ludus" 25 | - "teamserver.internal.local" 26 | - "10.0.200.5" 27 | 28 | redirector_host: 29 | type: "string" 30 | required: false 31 | default: "c2.redir.ludus" 32 | description: "The ServerName for Apache configuration. This is the hostname that the redirector will respond to." 33 | example: 34 | - "c2.redir.ludus" 35 | - "redirector.example.com" 36 | - "proxy.teamserver.local" 37 | 38 | kali_vlan_subnet: 39 | type: "string" 40 | required: false 41 | default: "10.{{ range_second_octet }}.200.0/24" 42 | description: "Subnet restriction for Apache access control. Only clients from this subnet will be allowed to access the redirector via Apache's 'Require ip' directive." 43 | example: 44 | - "10.{{ range_second_octet }}.200.0/24" 45 | - "192.168.100.0/24" 46 | - "10.0.0.0/8" 47 | 48 | ssl_certificate_file: 49 | type: "string" 50 | required: false 51 | default: "/etc/ssl/certs/ssl-cert-snakeoil.pem" 52 | description: "Path to SSL certificate file for HTTPS support." 53 | example: 54 | - "/etc/ssl/certs/ssl-cert-snakeoil.pem" 55 | - "/etc/ssl/certs/custom-cert.pem" 56 | - "/opt/ssl/redirector.crt" 57 | 58 | ssl_certificate_key_file: 59 | type: "string" 60 | required: false 61 | default: "/etc/ssl/private/ssl-cert-snakeoil.key" 62 | description: "Path to SSL certificate private key file." 63 | example: 64 | - "/etc/ssl/private/ssl-cert-snakeoil.key" 65 | - "/etc/ssl/private/custom-key.key" 66 | - "/opt/ssl/redirector.key" 67 | 68 | ssl_certificate_chain_file: 69 | type: "string" 70 | required: false 71 | default: "/etc/ssl/certs/chain.pem" 72 | description: "Path to SSL certificate chain file (optional). Uncomment in defaults/main.yml to use." 73 | example: 74 | - "/etc/ssl/certs/chain.pem" 75 | - "/etc/ssl/certs/intermediate.pem" 76 | - "/opt/ssl/ca-bundle.crt" 77 | 78 | rewrite_rules: 79 | type: "array" 80 | required: false 81 | default: [] 82 | description: "List of Apache RewriteRule strings for custom traffic routing. These rules control which routes are proxied or redirected to your target host. The range_second_octet variable allows dynamic IP configuration." 83 | example: 84 | - "RewriteRule ^/l33t(/.*)?$ https://10.{{ range_second_octet }}.200.5/l33t$1 [P,L]" 85 | - "RewriteRule ^/g3t(/.*)?$ http://10.{{ range_second_octet }}.200.5/g3t$1 [P,L]" 86 | - "RewriteRule ^/getsome(/.*)?$ https://{{ redirector_target_host }}/getsome$1 [P,L]" 87 | - "RewriteRule ^/api(/.*)?$ https://backend.internal.local/api$1 [P,L]" -------------------------------------------------------------------------------- /src/tools/ludusNetworkingSearch.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import fs from 'fs/promises'; 3 | import path from 'path'; 4 | import os from 'os'; 5 | import { Logger } from '../utils/logger.js'; 6 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 7 | 8 | const LudusNetworkingSchema = z.object({ 9 | help: z.boolean().optional().default(false).describe('Show help information') 10 | }); 11 | 12 | export function createLudusNetworkingDocsReadTool(logger: Logger, ludusCliWrapper: LudusCliWrapper) { 13 | return { 14 | name: 'ludus_networking_docs_read', 15 | description: `**NETWORKING DOCUMENTATION** - Direct Access to Complete Ludus Networking Documentation 16 | 17 | **DIRECT ACCESS TO:** 18 | - Network topology configurations 19 | - VLAN and subnet configurations 20 | - Network interface setup 21 | - Firewall and routing rules 22 | - Inter-VM communication patterns 23 | - Network isolation strategies 24 | - DNS and DHCP configurations 25 | 26 | **SIMPLIFIED FUNCTIONALITY:** 27 | - Always returns the complete networking documentation 28 | - No search filtering - reads entire document 29 | - Immediate access to all networking information 30 | 31 | **PERFECT FOR:** 32 | - Range planning (ludus_range_planner step 3) 33 | - Network architecture design 34 | - Understanding connectivity requirements 35 | - Troubleshooting network issues 36 | 37 | **TARGET**: Focuses specifically on networking-related documentation files.`, 38 | inputSchema: { 39 | type: 'object', 40 | properties: { 41 | help: { 42 | type: 'boolean', 43 | default: false, 44 | description: 'Show help information' 45 | } 46 | }, 47 | required: [] 48 | }, 49 | }; 50 | } 51 | 52 | export async function handleLudusNetworkingDocsRead(args: any): Promise { 53 | try { 54 | const { help = false } = args; 55 | 56 | if (help) { 57 | return { 58 | content: [{ 59 | type: 'text', 60 | text: 'ludus_networking_docs_read - Read complete Ludus networking documentation\n\nThis tool reads the entire networking documentation and returns all networking configuration information.\nNo parameters required - always returns complete documentation.' 61 | }] 62 | }; 63 | } 64 | 65 | // Always read the complete networking documentation 66 | const homeDir = os.homedir(); 67 | const docsDir = path.join(homeDir, '.ludus-mcp', 'docs'); 68 | 69 | try { 70 | const networkingContent = await readNetworkingDocs(docsDir); 71 | 72 | return { 73 | content: [{ 74 | type: 'text', 75 | text: `# Complete Ludus Networking Documentation\n\n${networkingContent}` 76 | }] 77 | }; 78 | } catch (error) { 79 | return { 80 | content: [{ 81 | type: 'text', 82 | text: `Error reading networking documentation: ${error instanceof Error ? error.message : 'Unknown error'}\n\nThe documentation may not be downloaded yet. Try running the server startup process to download docs.` 83 | }] 84 | }; 85 | } 86 | 87 | } catch (error) { 88 | return { 89 | content: [{ 90 | type: 'text', 91 | text: `Error in ludus_networking_docs_read: ${error instanceof Error ? error.message : 'Unknown error'}` 92 | }] 93 | }; 94 | } 95 | } 96 | 97 | async function readNetworkingDocs(docsDir: string): Promise { 98 | const networkingFiles = [ 99 | 'networking.md', 100 | 'configuration/networking.md', 101 | 'quick-start/networking.md' 102 | ]; 103 | 104 | let allContent = ''; 105 | 106 | for (const file of networkingFiles) { 107 | const filePath = path.join(docsDir, file); 108 | try { 109 | const content = await fs.readFile(filePath, 'utf-8'); 110 | allContent += `\n## From ${file}\n\n${content}\n`; 111 | } catch (error) { 112 | // File doesn't exist, continue 113 | } 114 | } 115 | 116 | if (!allContent) { 117 | throw new Error('No networking documentation files found'); 118 | } 119 | 120 | return allContent; 121 | } -------------------------------------------------------------------------------- /src/utils/keyring.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Keyring utility for secure credential storage 3 | * Uses OS-level keyring services for maximum security 4 | */ 5 | 6 | import keytar from 'keytar'; 7 | 8 | const SERVICE_NAME = 'ludus-mcp'; 9 | 10 | /** 11 | * Credential keys used by the MCP server 12 | */ 13 | export const CREDENTIAL_KEYS = { 14 | ADMIN_USER: 'admin-user', 15 | CONNECTION_METHOD: 'connection-method', 16 | WIREGUARD_CONFIG_PATH: 'wireguard-config-path', 17 | API_KEY: 'api-key', 18 | SSH_HOST: 'ssh-host', 19 | SSH_USER: 'ssh-user', 20 | SSH_AUTH_METHOD: 'ssh-auth-method', 21 | SSH_PASSWORD: 'ssh-password', 22 | SSH_KEY_PATH: 'ssh-key-path', 23 | SSH_KEY_PASSPHRASE: 'ssh-key-passphrase', 24 | } as const; 25 | 26 | /** 27 | * Store a credential in the OS keyring 28 | */ 29 | export async function storeCredential(key: string, value: string): Promise { 30 | try { 31 | await keytar.setPassword(SERVICE_NAME, key, value); 32 | } catch (error) { 33 | const errorMessage = error instanceof Error ? error.message : String(error); 34 | throw new Error(`Failed to store credential '${key}': ${errorMessage}`); 35 | } 36 | } 37 | 38 | /** 39 | * Retrieve a credential from the OS keyring 40 | */ 41 | export async function getCredential(key: string): Promise { 42 | try { 43 | return await keytar.getPassword(SERVICE_NAME, key); 44 | } catch (error) { 45 | const errorMessage = error instanceof Error ? error.message : String(error); 46 | throw new Error(`Failed to retrieve credential '${key}': ${errorMessage}`); 47 | } 48 | } 49 | 50 | /** 51 | * Delete a credential from the OS keyring 52 | */ 53 | export async function deleteCredential(key: string): Promise { 54 | try { 55 | return await keytar.deletePassword(SERVICE_NAME, key); 56 | } catch (error) { 57 | const errorMessage = error instanceof Error ? error.message : String(error); 58 | throw new Error(`Failed to delete credential '${key}': ${errorMessage}`); 59 | } 60 | } 61 | 62 | /** 63 | * Check if a credential exists in the keyring 64 | */ 65 | export async function hasCredential(key: string): Promise { 66 | const value = await getCredential(key); 67 | return value !== null; 68 | } 69 | 70 | /** 71 | * Store multiple credentials at once 72 | */ 73 | export async function storeCredentials(credentials: Record): Promise { 74 | const promises = Object.entries(credentials).map(([key, value]) => 75 | storeCredential(key, value) 76 | ); 77 | await Promise.all(promises); 78 | } 79 | 80 | /** 81 | * Retrieve multiple credentials at once 82 | */ 83 | export async function getCredentials(keys: string[]): Promise> { 84 | const promises = keys.map(async (key) => ({ 85 | key, 86 | value: await getCredential(key) 87 | })); 88 | 89 | const results = await Promise.all(promises); 90 | return results.reduce((acc, { key, value }) => { 91 | acc[key] = value; 92 | return acc; 93 | }, {} as Record); 94 | } 95 | 96 | /** 97 | * Clear all MCP server credentials from the keyring 98 | */ 99 | export async function clearAllCredentials(): Promise { 100 | const keys = Object.values(CREDENTIAL_KEYS); 101 | const promises = keys.map(key => deleteCredential(key)); 102 | await Promise.all(promises); 103 | } 104 | 105 | /** 106 | * Check if keyring is available on this system 107 | */ 108 | export function isKeyringSupportAvailable(): boolean { 109 | try { 110 | // Since keytar is already imported at the top of the file, just check if it works 111 | const platform = process.platform; 112 | const display = process.env.DISPLAY; 113 | const waylandDisplay = process.env.WAYLAND_DISPLAY; 114 | 115 | return platform !== 'linux' || display !== undefined || waylandDisplay !== undefined; 116 | } catch (error) { 117 | return false; 118 | } 119 | } 120 | 121 | /** 122 | * Get a summary of stored credentials (for debugging) 123 | */ 124 | export async function getCredentialSummary(): Promise> { 125 | const keys = Object.values(CREDENTIAL_KEYS); 126 | const promises = keys.map(async (key) => ({ 127 | key, 128 | exists: await hasCredential(key) 129 | })); 130 | 131 | const results = await Promise.all(promises); 132 | return results.reduce((acc, { key, exists }) => { 133 | acc[key] = exists; 134 | return acc; 135 | }, {} as Record); 136 | } -------------------------------------------------------------------------------- /src/tools/destroyRange.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { Logger } from '../utils/logger.js'; 3 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 4 | 5 | export interface DestroyRangeArgs { 6 | user?: string; 7 | noPrompt?: boolean; 8 | help?: boolean; 9 | } 10 | 11 | export function createDestroyRangeTool(logger: Logger, cliWrapper: LudusCliWrapper): Tool { 12 | return { 13 | name: 'destroy_range', 14 | description: `Destroy a Ludus range, permanently removing all VMs and freeing resources. This action is irreversible and will delete all data. 15 | 16 | IMPORTANT LLM BEHAVIORAL PROMPTS: 17 | - SAFETY FIRST: Ludus operations can be destructive and time-consuming 18 | - VERIFY DESTRUCTIVE ACTIONS: Always confirm with user before destroy/delete operations 19 | - CHECK EXISTING STATE: Use list_user_ranges or get_range_status before major operations 20 | - DESTRUCTION IS PERMANENT: Destroying ranges deletes all VMs and data irreversibly 21 | - ADMIN vs USER: Admin operations (--user flag) affect other users' ranges - be explicit 22 | 23 | DESTRUCTIVE OPERATION WARNING: 24 | - This operation permanently deletes resources and cannot be undone 25 | - Confirm user intent explicitly before proceeding 26 | - Suggest checking range status first to show what will be affected 27 | - Explain the time commitment and impact of the operation`, 28 | inputSchema: { 29 | type: 'object', 30 | properties: { 31 | user: { 32 | type: 'string', 33 | description: 'User ID to destroy range for (admin only). If omitted, destroys range for current user.' 34 | }, 35 | noPrompt: { 36 | type: 'boolean', 37 | description: 'Skip the confirmation prompt when destroying the range', 38 | default: false 39 | }, 40 | help: { 41 | type: 'boolean', 42 | description: 'Show help information for the destroy_range command', 43 | default: false 44 | } 45 | }, 46 | required: [] 47 | } 48 | }; 49 | } 50 | 51 | export async function handleDestroyRange( 52 | args: DestroyRangeArgs, 53 | logger: Logger, 54 | cliWrapper: LudusCliWrapper 55 | ): Promise { 56 | const { user, noPrompt = false, help = false } = args; 57 | 58 | // Handle help request 59 | if (help) { 60 | logger.info('Getting help for destroy range command', { user, noPrompt }); 61 | const result = await cliWrapper.executeArbitraryCommand('range', ['rm', '--help']); 62 | 63 | if (result.success) { 64 | return { 65 | success: true, 66 | message: 'Help information for ludus range rm command', 67 | help: true, 68 | content: result.rawOutput || result.message 69 | }; 70 | } else { 71 | throw new Error(`Failed to get help: ${result.message}`); 72 | } 73 | } 74 | 75 | // Execute the destroy range command 76 | logger.info('Executing destroy range command', { user, noPrompt }); 77 | 78 | try { 79 | const destroyResult = await cliWrapper.destroyRange(user, noPrompt); 80 | 81 | if (destroyResult.success) { 82 | logger.info('Destroy range command completed successfully', { 83 | user: user || 'current user', 84 | result: destroyResult.message 85 | }); 86 | 87 | return { 88 | success: true, 89 | message: destroyResult.message, 90 | data: destroyResult.data, 91 | rawOutput: destroyResult.rawOutput, 92 | user: user || 'current user', 93 | operation: 'destroy_range', 94 | timestamp: new Date().toISOString() 95 | }; 96 | } else { 97 | logger.error('Destroy range command failed', { 98 | user: user || 'current user', 99 | error: destroyResult.message 100 | }); 101 | 102 | return { 103 | success: false, 104 | message: `Failed to destroy range: ${destroyResult.message}`, 105 | error: destroyResult.message, 106 | user: user || 'current user', 107 | operation: 'destroy_range', 108 | timestamp: new Date().toISOString() 109 | }; 110 | } 111 | } catch (error) { 112 | const errorMessage = error instanceof Error ? error.message : String(error); 113 | logger.error('Exception during destroy range operation', { 114 | user: user || 'current user', 115 | error: errorMessage 116 | }); 117 | 118 | throw new Error(`Destroy range operation failed: ${errorMessage}`); 119 | } 120 | } -------------------------------------------------------------------------------- /schemas/netpenguins.ludus_sliver.yaml: -------------------------------------------------------------------------------- 1 | # Schema for netpenguins.ludus_sliver Role 2 | # Builds and deploys Sliver C2 framework from source with systemd service integration 3 | 4 | name: netpenguins.ludus_sliver 5 | type: role 6 | version: "1.0.0" 7 | description: "An Ansible role to build and deploy the Sliver C2 framework from source, with optional systemd service integration and configuration file support. Designed for use in Ludus ranges, but works anywhere you want a fresh, up-to-date Sliver install!" 8 | repository: "https://github.com/NetPenguins/ludus_sliver" 9 | author: "NetPenguins (@NetPenguins)" 10 | dependencies: [] 11 | 12 | installation_method: "ludus ansible role add netpenguins.ludus_sliver" 13 | note: "Installs all build dependencies (Go, git, build tools), clones Sliver repo, builds both sliver-server and sliver-client from source. Supports custom config files via role's files/ directory." 14 | warning: "This role deploys the Sliver C2 framework which is designed for red team exercises and penetration testing. Ensure proper authorization before deployment. Requires internet access to fetch Sliver and dependencies." 15 | 16 | variables: 17 | sliver_repo_url: 18 | type: "string" 19 | required: false 20 | default: "https://github.com/BishopFox/sliver.git" 21 | description: "Sliver GitHub repository URL. Typically unchanged unless using a fork or mirror." 22 | example: 23 | - "https://github.com/BishopFox/sliver.git" 24 | - "https://github.com/yourfork/sliver.git" 25 | - "https://internal-git.company.com/sliver.git" 26 | 27 | sliver_version: 28 | type: "string" 29 | required: false 30 | default: "stable" 31 | description: "Version to install. 'stable' uses latest release tag, 'latest' uses master branch, or specify any specific tag/branch/commit." 32 | valid_options: 33 | - "stable" 34 | - "latest" 35 | example: 36 | - "stable" 37 | - "latest" 38 | - "v1.5.42" 39 | - "dev" 40 | - "feature-branch" 41 | - "abc123def456" 42 | 43 | sliver_install_path: 44 | type: "string" 45 | required: false 46 | default: "/usr/local/bin" 47 | description: "Directory where Sliver binaries will be installed. Binaries are also symlinked to /usr/local/bin for system-wide access." 48 | example: 49 | - "/usr/local/bin" 50 | - "/opt/sliver" 51 | - "/home/sliver/bin" 52 | - "/usr/bin" 53 | 54 | sliver_build_user: 55 | type: "string" 56 | required: false 57 | default: "root" 58 | description: "User account to perform the build process as. Must have sufficient permissions to install dependencies and write to install path." 59 | example: 60 | - "root" 61 | - "sliver" 62 | - "ansible" 63 | - "ubuntu" 64 | 65 | sliver_create_service: 66 | type: "boolean" 67 | required: false 68 | default: true 69 | description: "Whether to install and enable a systemd service for sliver-server. Service will be configured to start automatically on boot." 70 | example: 71 | - true 72 | - false 73 | 74 | sliver_service_name: 75 | type: "string" 76 | required: false 77 | default: "sliver-server" 78 | description: "Name of the systemd service for sliver-server. Used for service management commands (systemctl start/stop/status)." 79 | example: 80 | - "sliver-server" 81 | - "sliver" 82 | - "c2-server" 83 | - "sliver-daemon" 84 | 85 | sliver_service_user: 86 | type: "string" 87 | required: false 88 | default: "root" 89 | description: "User account that the sliver-server systemd service will run as. Should have appropriate permissions for C2 operations." 90 | example: 91 | - "root" 92 | - "sliver" 93 | - "c2user" 94 | - "operator" 95 | 96 | sliver_service_group: 97 | type: "string" 98 | required: false 99 | default: "root" 100 | description: "Group that the sliver-server systemd service will run as. Should match or be compatible with the service user." 101 | example: 102 | - "root" 103 | - "sliver" 104 | - "c2group" 105 | - "operators" 106 | 107 | sliver_service_args: 108 | type: "string" 109 | required: false 110 | default: "" 111 | description: "Extra command-line arguments to pass to sliver-server when started as a service. Useful for custom configurations or debugging." 112 | example: 113 | - "" 114 | - "--lhost 0.0.0.0" 115 | - "--lport 8443" 116 | - "--debug" 117 | - "--config /etc/sliver/config.yaml" -------------------------------------------------------------------------------- /src/tools/ludusCliExecute.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { Logger } from '../utils/logger.js'; 3 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 4 | 5 | export interface LudusCliExecuteArgs { 6 | command: string; 7 | args?: string[]; 8 | user?: string; 9 | } 10 | 11 | export function createLudusCliExecuteTool(logger: Logger, cliWrapper: LudusCliWrapper): Tool { 12 | return { 13 | name: 'ludus_cli_execute', 14 | description: `CRITICAL: Do NOT include "ludus" prefix in command - tool adds it automatically! 15 | 16 | **LUDUS CLI EXECUTOR** - Execute arbitrary Ludus CLI commands and return raw output. This provides full access to the Ludus CLI for advanced operations. 17 | 18 | CORRECT USAGE: This tool executes actual "ludus" CLI commands on the system. Use this when you need to run native Ludus commands. 19 | 20 | IMPORTANT LLM BEHAVIORAL PROMPTS: 21 | - SAFETY FIRST: Ludus operations can be destructive and time-consuming 22 | - PREFER SPECIFIC TOOLS: Use dedicated tools (deploy_range, destroy_range) instead of raw CLI when available 23 | - VERIFY DESTRUCTIVE ACTIONS: Always confirm with user before destroy/delete operations 24 | - CHECK EXISTING STATE: Use list_user_ranges or get_range_status before major operations 25 | - ADMIN vs USER: Admin operations (--user flag) affect other users' ranges - be explicit 26 | 27 | RAW CLI EXECUTION WARNINGS: 28 | - This tool executes raw Ludus commands without safety checks 29 | - Destructive commands (range rm, user rm) bypass normal confirmations 30 | - Use specific tools when available for better safety and validation 31 | - Explain command purpose and risks before executing 32 | 33 | HELP COMMAND GUIDANCE: 34 | - For help: Use dedicated "ludus_help" tool OR command="--help" (NOT command="help") 35 | - For command help: Use "ludus_help" tool OR command=" --help" (e.g., "range --help") 36 | - IMPORTANT: Ludus CLI has NO "help" subcommand - only uses --help flags`, 37 | inputSchema: { 38 | type: 'object', 39 | properties: { 40 | command: { 41 | type: 'string', 42 | description: 'The Ludus CLI command to execute (e.g., "--help", "range", "templates", "user"). Do not include "ludus" prefix.', 43 | examples: ['--help', 'range logs', 'templates list', 'user info'] 44 | }, 45 | args: { 46 | type: 'array', 47 | items: { type: 'string' }, 48 | description: 'Command arguments as array (e.g., ["logs", "-f"] for "range logs -f"). Optional if arguments are included in command string.', 49 | examples: [['logs', '-f'], ['deploy', '--tags', 'dns'], ['range', '--help']] 50 | }, 51 | user: { 52 | type: 'string', 53 | description: 'User ID to execute command for (admin only). If omitted, executes for current user.' 54 | } 55 | }, 56 | required: ['command'] 57 | } 58 | }; 59 | } 60 | 61 | export async function handleLudusCliExecute( 62 | args: LudusCliExecuteArgs, 63 | logger: Logger, 64 | cliWrapper: LudusCliWrapper 65 | ): Promise { 66 | const { command, args: cmdArgs = [], user } = args; 67 | 68 | try { 69 | // Build argument array securely (no string concatenation) 70 | let parsedCommand = command; 71 | let parsedArgs = [...cmdArgs]; 72 | 73 | // If command contains spaces, split it 74 | if (command.includes(' ')) { 75 | const parts = command.split(' '); 76 | parsedCommand = parts[0]; 77 | parsedArgs = [...parts.slice(1), ...cmdArgs]; 78 | } 79 | 80 | // Add user flag if provided 81 | if (user) { 82 | parsedArgs.push('--user', user); 83 | } 84 | 85 | // Execute the command 86 | const result = await cliWrapper.executeArbitraryCommand(parsedCommand, parsedArgs); 87 | 88 | // Build command string for logging (safe since only used for display) 89 | const commandForLogging = `ludus ${parsedCommand} ${parsedArgs.join(' ')}`; 90 | 91 | // Return structured response 92 | return { 93 | success: result.success, 94 | command: commandForLogging, 95 | user: user || 'current user', 96 | output: result.rawOutput || result.message, 97 | data: result.data, 98 | exitCode: result.success ? 0 : 1 99 | }; 100 | 101 | } catch (error: any) { 102 | const commandForLogging = `ludus ${command} ${cmdArgs.join(' ')}`; 103 | logger.error('Ludus CLI execution failed', { 104 | command: commandForLogging, 105 | error: error.message, 106 | user 107 | }); 108 | 109 | return { 110 | success: false, 111 | command: commandForLogging, 112 | user: user || 'current user', 113 | output: error.message, 114 | data: null, 115 | exitCode: 1, 116 | error: error.message 117 | }; 118 | } 119 | } -------------------------------------------------------------------------------- /schemas/ludus_filigran_opencti.yaml: -------------------------------------------------------------------------------- 1 | name: ludus_filigran_opencti 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs Filigran OpenCTI threat intelligence platform" 5 | repository: "https://github.com/frack113/ludus_filigran_opencti" 6 | author: "frack113" 7 | installation_method: "ludus ansible role add ludus_filigran_opencti" 8 | dependencies: [] 9 | 10 | variables: 11 | ludus_filigran_docker_redis: 12 | type: "string" 13 | default: "7.4.2" 14 | description: "Redis Docker version" 15 | 16 | ludus_filigran_docker_elastic: 17 | type: "string" 18 | default: "8.17.4" 19 | description: "Elasticsearch Docker version" 20 | 21 | ludus_filigran_docker_minio: 22 | type: "string" 23 | default: "RELEASE.2024-05-28T17-19-04Z" 24 | description: "MinIO Docker version" 25 | 26 | ludus_filigran_docker_rabbitmq: 27 | type: "string" 28 | default: "4.0-management" 29 | description: "RabbitMQ Docker version" 30 | 31 | ludus_filigran_docker_postgres: 32 | type: "string" 33 | default: "17-alpine" 34 | description: "PostgreSQL Docker version" 35 | 36 | ludus_filigran_opencti_version: 37 | type: "string" 38 | default: "6.6.10" 39 | description: "OpenCTI version" 40 | 41 | ludus_filigran_openbas_version: 42 | type: "string" 43 | default: "1.16.0" 44 | description: "OpenBAS version" 45 | 46 | ludus_filigran_opencti_ADMIN_EMAIL: 47 | type: "string" 48 | default: "admin@domain.com" 49 | description: "OpenCTI admin email" 50 | 51 | ludus_filigran_opencti_ADMIN_PASSWORD: 52 | type: "string" 53 | default: "password" 54 | description: "OpenCTI admin password" 55 | 56 | ludus_filigran_opencti_ADMIN_TOKEN: 57 | type: "string" 58 | default: "9079c861-460e-49ba-9948-54137cfeb8ca" 59 | description: "OpenCTI admin token" 60 | 61 | ludus_filigran_opencti_HEALTHCHECK_ACCESS_KEY: 62 | type: "string" 63 | default: "9813287b-4234-4b9c-8382-73938f640455" 64 | description: "OpenCTI healthcheck access key" 65 | 66 | ludus_filigran_opencti_SMTP_HOSTNAME: 67 | type: "string" 68 | default: "localhost" 69 | description: "SMTP hostname" 70 | 71 | ludus_filigran_opencti_MINIO_ROOT_USER: 72 | type: "string" 73 | default: "d5e955ed-be95-4220-b828-d3d62c00cab3" 74 | description: "MinIO root user" 75 | 76 | ludus_filigran_opencti_MINIO_ROOT_PASSWORD: 77 | type: "string" 78 | default: "a46c6ee2-d3a8-4cbc-a39e-6f03a1e53524" 79 | description: "MinIO root password" 80 | 81 | ludus_filigran_opencti_RABBITMQ_DEFAULT_USER: 82 | type: "string" 83 | default: "guest" 84 | description: "RabbitMQ default user" 85 | 86 | ludus_filigran_opencti_RABBITMQ_DEFAULT_PASS: 87 | type: "string" 88 | default: "guest" 89 | description: "RabbitMQ default password" 90 | 91 | ludus_filigran_opencti_ELASTIC_MEMORY_SIZE: 92 | type: "string" 93 | default: "4G" 94 | description: "Elasticsearch memory size" 95 | 96 | ludus_filigran_opencti_POSTGRES_USER: 97 | type: "string" 98 | default: "ChangeMe" 99 | description: "PostgreSQL user" 100 | 101 | ludus_filigran_opencti_POSTGRES_PASSWORD: 102 | type: "string" 103 | default: "ChangeMe" 104 | description: "PostgreSQL password" 105 | 106 | ludus_filigran_opencti_external_import: 107 | type: "array" 108 | default: 109 | - "cisa-known-exploited-vulnerabilities" 110 | - "mitre" 111 | description: "CTI connectors for external import" 112 | 113 | ludus_filigran_opencti_internal_enrichment: 114 | type: "array" 115 | default: [] 116 | description: "Internal enrichment connectors" 117 | 118 | ludus_filigran_opencti_internal_export_file: 119 | type: "array" 120 | default: 121 | - "export-file-csv" 122 | - "export-file-stix" 123 | - "export-file-txt" 124 | - "export-report-pdf" 125 | - "export-ttps-file-navigator" 126 | description: "Internal export file connectors" 127 | 128 | ludus_filigran_opencti_internal_import_file: 129 | type: "array" 130 | default: 131 | - "import-document" 132 | - "import-file-misp" 133 | - "import-file-stix" 134 | - "import-file-yara" 135 | description: "Internal import file connectors" 136 | 137 | ludus_filigran_opencti_stream: 138 | type: "array" 139 | default: [] 140 | description: "Stream connectors" 141 | 142 | ludus_filigran_openbas_collectors: 143 | type: "array" 144 | default: 145 | - "atomic-red-team" 146 | - "mitre-attack" 147 | description: "BAS collectors" 148 | 149 | ludus_filigran_openbas_injectors: 150 | type: "array" 151 | default: 152 | - "http-query" 153 | - "nmap" 154 | description: "BAS injectors" -------------------------------------------------------------------------------- /schemas/mojeda101.ludus_fake_configs.yaml: -------------------------------------------------------------------------------- 1 | name: mojeda101.ludus_fake_configs 2 | type: role 3 | version: "1.0.0" 4 | description: "Creates random, fake configuration files on Windows across one or more paths. Optionally embeds credentials into files for lab noise or testing tools like manspider" 5 | repository: "https://github.com/mojeda101/ludus_fake_configs" 6 | author: "mojeda101 (@mojeda101)" 7 | 8 | installation_method: "ludus ansible role add mojeda101.ludus_fake_configs" 9 | note: "Originally created to populate empty shares created by ludus-ad-vulns" 10 | warning: "fake_cfg_force_mode: 'all' deletes EVERYTHING under each path. Use with caution" 11 | 12 | variables: 13 | fake_cfg_paths: 14 | type: "array" 15 | required: false 16 | default: 17 | - "C:\\shares\\all" 18 | - "C:\\shares\\public" 19 | description: "One or more paths to populate with fake config files" 20 | example: 21 | - "C:\\shares\\all" 22 | - "C:\\shares\\public" 23 | - "C:\\temp\\configs" 24 | 25 | fake_cfg_create_path: 26 | type: "boolean" 27 | required: false 28 | default: true 29 | description: "Ensure target paths exist, create if missing" 30 | 31 | fake_cfg_use_pwsh: 32 | type: "boolean" 33 | required: false 34 | default: false 35 | description: "Prefer PowerShell 7 if installed, falls back to Windows PowerShell" 36 | 37 | fake_cfg_count: 38 | type: "integer" 39 | required: false 40 | default: 50 41 | description: "Total files to end up with per path (idempotent unless forced)" 42 | 43 | fake_cfg_extensions: 44 | type: "array" 45 | required: false 46 | default: ["ini", "yaml", "yml", "json", "xml", "conf", "properties"] 47 | description: "Pool of file extensions to generate" 48 | 49 | fake_cfg_min_kib: 50 | type: "integer" 51 | required: false 52 | default: 1 53 | description: "Minimum approximate file size in KiB" 54 | 55 | fake_cfg_max_kib: 56 | type: "integer" 57 | required: false 58 | default: 32 59 | description: "Maximum approximate file size in KiB" 60 | 61 | fake_cfg_subdirs: 62 | type: "array" 63 | required: false 64 | default: ["app", "services", "agents", "drivers", "development", "configs"] 65 | description: "Optional subfolders to create under paths. Empty array for none" 66 | 67 | fake_cfg_prefix: 68 | type: "string" 69 | required: false 70 | default: "config" 71 | description: "Prefix for generated config filenames" 72 | 73 | fake_cfg_name_patterns: 74 | type: "array" 75 | required: false 76 | default: 77 | - "{{ prefix }}-{{ rand }}.{{ ext }}" 78 | - "{{ prefix }}_{{ rand }}.{{ ext }}" 79 | - "{{ rand }}-{{ prefix }}.{{ ext }}" 80 | description: "File name patterns using tokens: {{ prefix }}, {{ rand }}, {{ ext }}" 81 | 82 | fake_cfg_date_back_days_max: 83 | type: "integer" 84 | required: false 85 | default: 120 86 | description: "Maximum days to backdate file timestamps. 0 disables backdating" 87 | 88 | fake_cfg_force: 89 | type: "boolean" 90 | required: false 91 | default: true 92 | description: "Purge existing files first, then recreate" 93 | 94 | fake_cfg_force_mode: 95 | type: "string" 96 | required: false 97 | default: "matching" 98 | valid_options: 99 | - "matching" 100 | - "all" 101 | description: "Force mode: 'matching' deletes only fake configs, 'all' deletes EVERYTHING under paths" 102 | 103 | fake_cfg_credentials: 104 | type: "object" 105 | required: false 106 | default: 107 | username: "svc_labuser" 108 | password: "P@ssw0rd!123" 109 | description: "Username and password to embed in configuration files" 110 | 111 | fake_cfg_creds_embed_mode: 112 | type: "string" 113 | required: false 114 | default: "ratio" 115 | valid_options: 116 | - "none" 117 | - "all" 118 | - "count" 119 | - "ratio" 120 | description: "How to embed credentials: none=never, all=every file, count=exact number, ratio=percentage" 121 | 122 | fake_cfg_creds_embed_count: 123 | type: "integer" 124 | required: false 125 | default: 50 126 | description: "Exact number of files per path to embed credentials when mode is 'count'" 127 | 128 | fake_cfg_creds_embed_ratio: 129 | type: "integer" 130 | required: false 131 | default: 0.1 132 | description: "Ratio of files to embed credentials when mode is 'ratio' (0.1 = 10%)" 133 | 134 | fake_cfg_creds_embed_exts: 135 | type: "array" 136 | required: false 137 | default: ["ini", "yaml", "yml", "json", "xml", "conf", "properties"] 138 | description: "File extensions eligible for credential embedding" 139 | 140 | fake_cfg_creds_embed_names: 141 | type: "array" 142 | required: false 143 | default: ["(?i)credential", "(?i)auth", "(?i)login", "(?i)secrets?"] 144 | description: "Regex patterns for filenames that will ALWAYS get credentials embedded" 145 | -------------------------------------------------------------------------------- /src/ludusMCP/config.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { homedir } from 'os'; 4 | import yaml from 'js-yaml'; 5 | import { Logger } from '../utils/logger.js'; 6 | 7 | export interface LudusConfigOptions { 8 | url?: string; 9 | apiKey?: string; 10 | skipCertVerification?: boolean; 11 | proxyUrl?: string; 12 | timeout?: number; 13 | sshHost?: string; 14 | sshUser?: string; 15 | } 16 | 17 | export class LudusConfig { 18 | private logger: Logger; 19 | private config: LudusConfigOptions; 20 | 21 | constructor(logger: Logger) { 22 | this.logger = logger; 23 | this.config = this.loadConfig(); 24 | } 25 | 26 | private loadConfig(): LudusConfigOptions { 27 | const config: LudusConfigOptions = {}; 28 | 29 | // Load from config file first 30 | const configPath = join(homedir(), '.config', 'ludus', 'config.yml'); 31 | if (existsSync(configPath)) { 32 | try { 33 | const fileContent = readFileSync(configPath, 'utf8'); 34 | const fileConfig = yaml.load(fileContent) as any; 35 | 36 | // Map config file keys to our interface 37 | if (fileConfig.url) config.url = fileConfig.url; 38 | if (fileConfig.verify !== undefined) config.skipCertVerification = !fileConfig.verify; 39 | if (fileConfig.proxy) config.proxyUrl = fileConfig.proxy; 40 | if (fileConfig.ssh_host) config.sshHost = fileConfig.ssh_host; 41 | if (fileConfig.ssh_user) config.sshUser = fileConfig.ssh_user; 42 | this.logger.debug('Loaded config from file', { path: configPath }); 43 | } catch (error) { 44 | this.logger.warn('Failed to load config file', { 45 | path: configPath, 46 | error: error instanceof Error ? { 47 | name: error.name, 48 | message: error.message, 49 | stack: error.stack 50 | } : String(error) 51 | }); 52 | } 53 | } 54 | 55 | // Override with environment variables 56 | if (process.env.LUDUS_URL) { 57 | config.url = process.env.LUDUS_URL; 58 | } 59 | if (process.env.LUDUS_API_KEY) { 60 | config.apiKey = process.env.LUDUS_API_KEY; 61 | } 62 | if (process.env.LUDUS_VERIFY) { 63 | config.skipCertVerification = process.env.LUDUS_VERIFY === 'false'; 64 | } 65 | if (process.env.LUDUS_PROXY) { 66 | config.proxyUrl = process.env.LUDUS_PROXY; 67 | } 68 | if (process.env.LUDUS_TIMEOUT) { 69 | config.timeout = parseInt(process.env.LUDUS_TIMEOUT, 10); 70 | } 71 | if (process.env.LUDUS_SSH_HOST) { 72 | config.sshHost = process.env.LUDUS_SSH_HOST; 73 | } 74 | if (process.env.LUDUS_SSH_USER) { 75 | config.sshUser = process.env.LUDUS_SSH_USER; 76 | } 77 | 78 | // Set defaults 79 | if (!config.skipCertVerification) { 80 | config.skipCertVerification = false; 81 | } 82 | if (!config.timeout) { 83 | config.timeout = 30000; // 30 seconds 84 | } 85 | 86 | return config; 87 | } 88 | 89 | public getUrl(): string { 90 | if (!this.config.url) { 91 | throw new Error('Ludus URL not configured. Set LUDUS_URL environment variable or configure in ~/.config/ludusMCP/config.yml'); 92 | } 93 | return this.config.url; 94 | } 95 | 96 | public getApiKey(): string { 97 | if (!this.config.apiKey) { 98 | throw new Error('Ludus API key not configured. Set LUDUS_API_KEY environment variable or configure in ~/.config/ludusMCP/config.yml'); 99 | } 100 | return this.config.apiKey; 101 | } 102 | 103 | public getSkipCertVerification(): boolean { 104 | return this.config.skipCertVerification || false; 105 | } 106 | 107 | public getProxyUrl(): string | undefined { 108 | return this.config.proxyUrl; 109 | } 110 | 111 | public getTimeout(): number { 112 | return this.config.timeout || 30000; 113 | } 114 | 115 | public getSSHHost(): string | undefined { 116 | return this.config.sshHost; 117 | } 118 | 119 | public getSSHUser(): string | undefined { 120 | return this.config.sshUser; 121 | } 122 | 123 | public validateConfig(): void { 124 | const url = this.getUrl(); 125 | const apiKey = this.getApiKey(); 126 | 127 | // Validate URL format 128 | try { 129 | new URL(url); 130 | } catch (error) { 131 | throw new Error(`Invalid Ludus URL format: ${url}`); 132 | } 133 | 134 | // Validate API key format (USERID.{40-char-key}) 135 | const apiKeyPattern = /^[^.]+\.[a-zA-Z0-9]{40}$/; 136 | if (!apiKeyPattern.test(apiKey)) { 137 | throw new Error('Invalid API key format. Expected format: USERID.{40-character-key}'); 138 | } 139 | 140 | this.logger.info('Configuration validated successfully', { 141 | url, 142 | apiKeyUser: apiKey.split('.')[0], 143 | skipCertVerification: this.getSkipCertVerification(), 144 | timeout: this.getTimeout() 145 | }); 146 | } 147 | 148 | public getConfig(): LudusConfigOptions { 149 | return { ...this.config }; 150 | } 151 | } -------------------------------------------------------------------------------- /src/tools/ludusHelp.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { Logger } from '../utils/logger.js'; 3 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 4 | 5 | export interface LudusHelpArgs { 6 | command?: string; 7 | subcommand?: string; 8 | user?: string; 9 | } 10 | 11 | export function createLudusHelpTool(logger: Logger, cliWrapper: LudusCliWrapper): Tool { 12 | return { 13 | name: 'ludus_help', 14 | description: 'Get help information for Ludus CLI commands. Use this to discover available commands, learn syntax, and understand options. Can show general help or specific command help.', 15 | inputSchema: { 16 | type: 'object', 17 | properties: { 18 | command: { 19 | type: 'string', 20 | description: 'Specific command to get help for (e.g., "range", "templates", "user"). If omitted, shows general help.', 21 | examples: ['range', 'templates', 'user', 'testing'] 22 | }, 23 | subcommand: { 24 | type: 'string', 25 | description: 'Subcommand to get help for (e.g., "deploy", "logs", "list"). Requires command to be specified.', 26 | examples: ['deploy', 'logs', 'list', 'add', 'remove'] 27 | }, 28 | user: { 29 | type: 'string', 30 | description: 'User ID to get help for (admin only). If omitted, gets help for current user context.' 31 | } 32 | }, 33 | required: [] 34 | } 35 | }; 36 | } 37 | 38 | export async function handleLudusHelp( 39 | args: LudusHelpArgs, 40 | logger: Logger, 41 | cliWrapper: LudusCliWrapper 42 | ): Promise { 43 | const { command, subcommand, user } = args; 44 | 45 | try { 46 | // Build help command based on what's requested 47 | let result; 48 | let fullCommand: string; 49 | 50 | if (command) { 51 | // For specific commands: ludus [subcommand] --help 52 | const helpArgs: string[] = []; 53 | if (subcommand) { 54 | helpArgs.push(subcommand); 55 | } 56 | helpArgs.push('--help'); 57 | 58 | // Add user context if provided 59 | if (user) { 60 | helpArgs.push('--user', user); 61 | } 62 | 63 | fullCommand = subcommand 64 | ? `ludus ${command} ${subcommand} --help` 65 | : `ludus ${command} --help`; 66 | 67 | logger.info('Getting Ludus CLI help', { command: fullCommand, user }); 68 | result = await cliWrapper.executeArbitraryCommand(command, helpArgs); 69 | } else { 70 | // For general help: ludus --help 71 | fullCommand = 'ludus --help'; 72 | logger.info('Getting Ludus CLI help', { command: fullCommand, user }); 73 | result = await cliWrapper.executeCommand('--help', []); 74 | } 75 | 76 | if (!result.success) { 77 | throw new Error(`Failed to get help: ${result.message}`); 78 | } 79 | 80 | // Format response 81 | const targetUser = user || 'current user'; 82 | const helpType = command && subcommand 83 | ? `${command} ${subcommand}` 84 | : command 85 | ? command 86 | : 'general'; 87 | 88 | return { 89 | success: true, 90 | command: fullCommand, 91 | user: targetUser, 92 | helpType: helpType, 93 | content: result.rawOutput || result.message, 94 | 95 | // Structured for easy parsing 96 | sections: parseHelpOutput(result.rawOutput || result.message) 97 | }; 98 | 99 | } catch (error: any) { 100 | logger.error('Ludus help failed', { 101 | command: command || 'help', 102 | subcommand, 103 | error: error.message, 104 | user 105 | }); 106 | 107 | return { 108 | success: false, 109 | command: command || 'help', 110 | user: user || 'current user', 111 | error: error.message, 112 | content: error.message 113 | }; 114 | } 115 | } 116 | 117 | /** 118 | * Parse help output into structured sections 119 | */ 120 | function parseHelpOutput(output: string): any { 121 | const sections: any = {}; 122 | 123 | try { 124 | const lines = output.split('\n'); 125 | let currentSection = 'description'; 126 | let currentContent: string[] = []; 127 | 128 | for (const line of lines) { 129 | const trimmed = line.trim(); 130 | 131 | // Detect section headers 132 | if (trimmed.match(/^(Usage|Available Commands|Flags|Examples|Global Flags|Aliases):/)) { 133 | // Save previous section 134 | if (currentContent.length > 0) { 135 | sections[currentSection] = currentContent.join('\n').trim(); 136 | } 137 | 138 | // Start new section 139 | currentSection = trimmed.replace(':', '').toLowerCase().replace(/\s+/g, '_'); 140 | currentContent = []; 141 | } else if (trimmed.length > 0) { 142 | currentContent.push(line); 143 | } 144 | } 145 | 146 | // Save last section 147 | if (currentContent.length > 0) { 148 | sections[currentSection] = currentContent.join('\n').trim(); 149 | } 150 | 151 | return sections; 152 | } catch (error) { 153 | return { raw: output }; 154 | } 155 | } -------------------------------------------------------------------------------- /src/utils/fileLogger.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import * as os from 'os'; 5 | 6 | export class FileLogger { 7 | private logDir: string; 8 | private logFile: string; 9 | private maxFileSize: number; 10 | private maxFiles: number; 11 | 12 | constructor( 13 | logName: string = 'ludus-mcp', 14 | maxFileSize: number = 10 * 1024 * 1024, // 10MB 15 | maxFiles: number = 5 16 | ) { 17 | // Create logs directory in user's temp/home directory 18 | this.logDir = path.join(os.homedir(), '.ludus-mcp', 'logs'); 19 | this.logFile = path.join(this.logDir, `${logName}.log`); 20 | this.maxFileSize = maxFileSize; 21 | this.maxFiles = maxFiles; 22 | 23 | this.ensureLogDirectory(); 24 | } 25 | 26 | private ensureLogDirectory(): void { 27 | try { 28 | if (!fs.existsSync(this.logDir)) { 29 | fs.mkdirSync(this.logDir, { recursive: true }); 30 | } 31 | } catch (error) { 32 | console.error('Failed to create log directory:', error); 33 | } 34 | } 35 | 36 | private rotateIfNeeded(): void { 37 | try { 38 | if (!fs.existsSync(this.logFile)) { 39 | return; 40 | } 41 | 42 | const stats = fs.statSync(this.logFile); 43 | if (stats.size >= this.maxFileSize) { 44 | this.rotateFiles(); 45 | } 46 | } catch (error) { 47 | console.error('Error checking log file size:', error); 48 | } 49 | } 50 | 51 | private rotateFiles(): void { 52 | try { 53 | // Move current log to .1, .1 to .2, etc. 54 | for (let i = this.maxFiles - 1; i >= 1; i--) { 55 | const oldFile = `${this.logFile}.${i}`; 56 | const newFile = `${this.logFile}.${i + 1}`; 57 | 58 | if (fs.existsSync(oldFile)) { 59 | if (i === this.maxFiles - 1) { 60 | fs.unlinkSync(oldFile); // Delete oldest 61 | } else { 62 | fs.renameSync(oldFile, newFile); 63 | } 64 | } 65 | } 66 | 67 | // Move current log to .1 68 | if (fs.existsSync(this.logFile)) { 69 | fs.renameSync(this.logFile, `${this.logFile}.1`); 70 | } 71 | } catch (error) { 72 | console.error('Error rotating log files:', error); 73 | } 74 | } 75 | 76 | private formatValue(value: any): string { 77 | if (value === null) return 'null'; 78 | if (value === undefined) return 'undefined'; 79 | if (typeof value === 'string') return value; 80 | if (typeof value === 'number' || typeof value === 'boolean') return String(value); 81 | 82 | // Handle Error objects specially 83 | if (value instanceof Error) { 84 | return `${value.name}: ${value.message}\n${value.stack || 'No stack trace'}`; 85 | } 86 | 87 | // Handle objects that might have circular references or non-enumerable properties 88 | try { 89 | // First try regular JSON.stringify 90 | return JSON.stringify(value, null, 2); 91 | } catch (jsonError) { 92 | // Fallback: try to extract common error properties manually 93 | if (value && typeof value === 'object') { 94 | const errorProps: string[] = []; 95 | 96 | // Try to get common error properties 97 | const commonProps = ['message', 'code', 'errno', 'syscall', 'path', 'name', 'stack', 'signal', 'status', 'stderr', 'stdout']; 98 | for (const prop of commonProps) { 99 | if (prop in value && value[prop] !== undefined) { 100 | errorProps.push(`${prop}: ${String(value[prop])}`); 101 | } 102 | } 103 | 104 | // If we found some properties, use them 105 | if (errorProps.length > 0) { 106 | return errorProps.join('\n'); 107 | } 108 | 109 | // Last resort: try to get all enumerable properties 110 | try { 111 | const props = Object.keys(value).map(key => `${key}: ${String(value[key])}`); 112 | if (props.length > 0) { 113 | return props.join('\n'); 114 | } 115 | } catch (e) { 116 | // Even this failed 117 | } 118 | } 119 | 120 | // Final fallback 121 | return `[Complex object - toString: ${String(value)}]`; 122 | } 123 | } 124 | 125 | log(level: string, context: string, message: string, meta?: Record): void { 126 | try { 127 | this.rotateIfNeeded(); 128 | 129 | const timestamp = new Date().toISOString(); 130 | let logLine = `[${timestamp}] ${level.toUpperCase()} [${context}] ${message}`; 131 | 132 | if (meta && Object.keys(meta).length > 0) { 133 | logLine += '\n Metadata:'; 134 | for (const [key, value] of Object.entries(meta)) { 135 | const formattedValue = this.formatValue(value); 136 | logLine += `\n ${key}: ${formattedValue}`; 137 | } 138 | } 139 | 140 | logLine += '\n\n'; 141 | 142 | fs.appendFileSync(this.logFile, logLine, 'utf8'); 143 | } catch (error) { 144 | console.error('Failed to write to log file:', error); 145 | } 146 | } 147 | 148 | getLogPath(): string { 149 | return this.logFile; 150 | } 151 | 152 | getLogDirectory(): string { 153 | return this.logDir; 154 | } 155 | } -------------------------------------------------------------------------------- /schemas/professor-moody.ludus_litterbox.yaml: -------------------------------------------------------------------------------- 1 | name: professor-moody.ludus_litterbox 2 | type: role 3 | version: "1.0.0" 4 | description: "Deploys LitterBox - a comprehensive malware analysis sandbox on Windows systems for static and dynamic analysis with web-based interface" 5 | repository: "https://github.com/professor-moody/ludus_litterbox_role" 6 | author: "professor-moody (@professor-moody)" 7 | 8 | installation_method: "ludus ansible role add professor-moody.ludus_litterbox" 9 | note: "Provides static/dynamic malware analysis, YARA scanning, PE analysis, and API integration via GrumpyCats client. Web interface on port 1337" 10 | warning: "LAB USE ONLY! Disables Windows Defender, handles malicious files, never expose to production networks" 11 | 12 | variables: 13 | ludus_litterbox_install: 14 | type: "boolean" 15 | required: false 16 | default: true 17 | description: "Enable or disable LitterBox installation" 18 | 19 | ludus_litterbox_install_dir: 20 | type: "string" 21 | required: false 22 | default: "C:\\Tools\\LitterBox" 23 | description: "Installation directory for LitterBox" 24 | 25 | ludus_litterbox_python_version: 26 | type: "string" 27 | required: false 28 | default: "3.11.9" 29 | description: "Python version to install if not already present" 30 | 31 | ludus_litterbox_install_python: 32 | type: "boolean" 33 | required: false 34 | default: true 35 | description: "Enable or disable Python installation" 36 | 37 | ludus_litterbox_repo_url: 38 | type: "string" 39 | required: false 40 | default: "https://github.com/BlackSnufkin/LitterBox.git" 41 | description: "LitterBox repository URL" 42 | 43 | ludus_litterbox_host: 44 | type: "string" 45 | required: false 46 | default: "127.0.0.1" 47 | description: "Bind address for web interface (use 0.0.0.0 for network access)" 48 | example: 49 | - "127.0.0.1" 50 | - "0.0.0.0" 51 | 52 | ludus_litterbox_port: 53 | type: "integer" 54 | required: false 55 | default: 1337 56 | description: "Web interface port" 57 | 58 | ludus_litterbox_disable_defender: 59 | type: "boolean" 60 | required: false 61 | default: true 62 | description: "Disable Windows Defender (LAB USE ONLY!)" 63 | 64 | ludus_litterbox_defender_exclusions: 65 | type: "boolean" 66 | required: false 67 | default: true 68 | description: "Add Windows Defender exclusions for malware analysis" 69 | 70 | ludus_litterbox_require_admin: 71 | type: "boolean" 72 | required: false 73 | default: true 74 | description: "Require administrator privileges check" 75 | 76 | ludus_litterbox_firewall_rule: 77 | type: "boolean" 78 | required: false 79 | default: true 80 | description: "Create firewall exception for web interface" 81 | 82 | ludus_litterbox_desktop_shortcut: 83 | type: "boolean" 84 | required: false 85 | default: true 86 | description: "Create desktop shortcut" 87 | 88 | ludus_litterbox_debug: 89 | type: "boolean" 90 | required: false 91 | default: false 92 | description: "Enable debug mode" 93 | 94 | ludus_litterbox_analysis_timeout: 95 | type: "integer" 96 | required: false 97 | default: 300 98 | description: "Analysis timeout in seconds" 99 | 100 | ludus_litterbox_max_file_size: 101 | type: "integer" 102 | required: false 103 | default: 104857600 104 | description: "Maximum file size for analysis (100MB in bytes)" 105 | 106 | ludus_litterbox_allowed_extensions: 107 | type: "array" 108 | required: false 109 | default: ["exe", "dll", "sys", "scr", "com", "bat", "ps1", "vbs", "js", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf", "lnk"] 110 | description: "Supported file types for analysis" 111 | 112 | ludus_litterbox_enable_static: 113 | type: "boolean" 114 | required: false 115 | default: true 116 | description: "Enable static analysis" 117 | 118 | ludus_litterbox_enable_dynamic: 119 | type: "boolean" 120 | required: false 121 | default: true 122 | description: "Enable dynamic analysis" 123 | 124 | ludus_litterbox_enable_holygrail: 125 | type: "boolean" 126 | required: false 127 | default: true 128 | description: "Enable BYOVD (Bring Your Own Vulnerable Driver) detection" 129 | 130 | ludus_litterbox_enable_doppelganger: 131 | type: "boolean" 132 | required: false 133 | default: true 134 | description: "Enable process similarity analysis" 135 | 136 | ludus_litterbox_enable_yara: 137 | type: "boolean" 138 | required: false 139 | default: true 140 | description: "Enable YARA scanning" 141 | 142 | ludus_litterbox_log_level: 143 | type: "string" 144 | required: false 145 | default: "INFO" 146 | valid_options: 147 | - "DEBUG" 148 | - "INFO" 149 | - "WARNING" 150 | - "ERROR" 151 | description: "Logging level" 152 | 153 | ludus_litterbox_workers: 154 | type: "integer" 155 | required: false 156 | default: 4 157 | description: "Number of analysis workers" 158 | 159 | ludus_litterbox_cleanup_days: 160 | type: "integer" 161 | required: false 162 | default: 30 163 | description: "Days to keep analysis results before cleanup" 164 | 165 | ludus_litterbox_install_chocolatey: 166 | type: "boolean" 167 | required: false 168 | default: true 169 | description: "Install Chocolatey package manager" 170 | -------------------------------------------------------------------------------- /schemas/badsectorlabs.ludus_adcs.yaml: -------------------------------------------------------------------------------- 1 | name: badsectorlabs.ludus_adcs 2 | type: role 3 | version: "1.0.0" 4 | description: "Installs ADCS on Windows Server and optionally configures Certified Preowned templates" 5 | repository: "https://github.com/badsectorlabs/ludus_adcs" 6 | author: "Bad Sector Labs" 7 | installation_method: "ludus ansible role add badsectorlabs.ludus_adcs" 8 | dependencies: [] 9 | 10 | note: "Not idempotent - setting ESC values to false after true won't remove templates" 11 | 12 | variables: 13 | ludus_adcs_domain: 14 | type: "string" 15 | default: "{{ auto-detected }}" 16 | description: "Auto-detected domain name from Ludus config" 17 | 18 | ludus_adcs_dc: 19 | type: "string" 20 | default: "{{ auto-detected }}" 21 | description: "Auto-detected primary DC hostname" 22 | 23 | ludus_adcs_ca_host: 24 | type: "string" 25 | default: "{{ auto-detected }}" 26 | description: "Auto-detected CA host from Ludus config" 27 | 28 | ludus_adcs_domain_username: 29 | type: "string" 30 | default: "{{ auto-detected }}" 31 | description: "Domain admin username" 32 | 33 | ludus_adcs_domain_password: 34 | type: "string" 35 | default: "{{ auto-detected }}" 36 | description: "Domain admin password" 37 | 38 | ludus_adcs_ca_common_name: 39 | type: "string" 40 | default: "{{ ludus_adcs_domain }}-CA" 41 | description: "CA certificate common name" 42 | 43 | ludus_adcs_esc1: 44 | type: "boolean" 45 | default: true 46 | description: "Enable ESC1 template vulnerability" 47 | 48 | ludus_adcs_esc2: 49 | type: "boolean" 50 | default: true 51 | description: "Enable ESC2 template vulnerability" 52 | 53 | ludus_adcs_esc3: 54 | type: "boolean" 55 | default: true 56 | description: "Enable ESC3 template vulnerability" 57 | 58 | ludus_adcs_esc3_cra: 59 | type: "boolean" 60 | default: true 61 | description: "Enable ESC3 CRA template vulnerability" 62 | 63 | ludus_adcs_esc4: 64 | type: "boolean" 65 | default: true 66 | description: "Enable ESC4 template vulnerability" 67 | 68 | ludus_adcs_esc5: 69 | type: "boolean" 70 | default: true 71 | description: "Enable ESC5 template vulnerability" 72 | 73 | ludus_adcs_esc6: 74 | type: "boolean" 75 | default: true 76 | description: "Enable ESC6 template vulnerability" 77 | 78 | ludus_adcs_esc7: 79 | type: "boolean" 80 | default: true 81 | description: "Enable ESC7 template vulnerability" 82 | 83 | ludus_adcs_esc8: 84 | type: "boolean" 85 | default: true 86 | description: "Enable ESC8 template vulnerability" 87 | 88 | ludus_adcs_esc9: 89 | type: "boolean" 90 | default: true 91 | description: "Enable ESC9 template vulnerability" 92 | 93 | ludus_adcs_esc11: 94 | type: "boolean" 95 | default: true 96 | description: "Enable ESC11 template vulnerability" 97 | 98 | ludus_adcs_esc13: 99 | type: "boolean" 100 | default: true 101 | description: "Enable ESC13 template vulnerability" 102 | 103 | ludus_adcs_esc15: 104 | type: "boolean" 105 | default: true 106 | description: "Enable ESC15 template vulnerability" 107 | 108 | ludus_adcs_esc16: 109 | type: "boolean" 110 | default: true 111 | description: "Enable ESC16 template vulnerability" 112 | 113 | ludus_adcs_esc5_user: 114 | type: "string" 115 | default: "esc5user" 116 | description: "Username for ESC5 exploitation" 117 | 118 | ludus_adcs_esc5_password: 119 | type: "string" 120 | default: "ESC5password" 121 | description: "Password for ESC5 user" 122 | 123 | ludus_adcs_esc7_ca_manager_user: 124 | type: "string" 125 | default: "esc7_camgr_user" 126 | description: "CA manager user for ESC7" 127 | 128 | ludus_adcs_esc7_ca_manager_password: 129 | type: "string" 130 | default: "ESC7password" 131 | description: "CA manager password for ESC7" 132 | 133 | ludus_adcs_esc7_cert_manager_user: 134 | type: "string" 135 | default: "esc7_certmgr_user" 136 | description: "Certificate manager user for ESC7" 137 | 138 | ludus_adcs_esc7_cert_manager_password: 139 | type: "string" 140 | default: "ESC7password" 141 | description: "Certificate manager password for ESC7" 142 | 143 | ludus_adcs_esc9_user: 144 | type: "string" 145 | default: "esc9user" 146 | description: "Username for ESC9 exploitation" 147 | 148 | ludus_adcs_esc9_password: 149 | type: "string" 150 | default: "ESC9password" 151 | description: "Password for ESC9 user" 152 | 153 | ludus_adcs_esc13_user: 154 | type: "string" 155 | default: "esc13user" 156 | description: "Username for ESC13 exploitation" 157 | 158 | ludus_adcs_esc13_password: 159 | type: "string" 160 | default: "ESC13password" 161 | description: "Password for ESC13 user" 162 | 163 | ludus_adcs_esc13_group: 164 | type: "string" 165 | default: "esc13group" 166 | description: "Group for ESC13 exploitation" 167 | 168 | ludus_adcs_esc13_template: 169 | type: "string" 170 | default: "ESC13" 171 | description: "Template name for ESC13" 172 | 173 | ludus_adcs_esc16_user: 174 | type: "string" 175 | default: "esc16user" 176 | description: "Username for ESC16 exploitation" 177 | 178 | ludus_adcs_esc16_password: 179 | type: "string" 180 | default: "ESC16password" 181 | description: "Password for ESC16 user" -------------------------------------------------------------------------------- /src/prompts/executeLudusCmd.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | // Schema for prompt arguments 4 | const ExecuteLudusCmdArgsSchema = z.object({ 5 | command_intent: z.string().describe('What you want to accomplish with the CLI command'), 6 | target_user: z.string().optional().describe('Target user for admin operations (leave empty for current user)'), 7 | confirm_destructive: z.boolean().optional().default(false).describe('Confirmation for destructive operations') 8 | }); 9 | 10 | export type ExecuteLudusCmdArgs = z.infer; 11 | 12 | export async function handleExecuteLudusCmdPrompt(args: ExecuteLudusCmdArgs) { 13 | const { command_intent, target_user, confirm_destructive = false } = args; 14 | 15 | // Convert string input to proper boolean 16 | const isDestructiveConfirmed = typeof confirm_destructive === 'string' 17 | ? ['true', 'TRUE', 't', 'T', '1', 'yes', 'YES', 'y', 'Y'].includes(confirm_destructive) 18 | : confirm_destructive === true; 19 | 20 | return { 21 | messages: [ 22 | { 23 | role: "user", 24 | content: { 25 | type: "text", 26 | text: `Execute Ludus CLI command safely to: ${command_intent} 27 | ${target_user ? `\nTarget user: ${target_user}` : '\nTarget: Current user'} 28 | ${isDestructiveConfirmed ? '\nDESTRUCTIVE ACTION CONFIRMED by user' : ''} 29 | 30 | LUDUS CLI EXECUTION SAFETY PROTOCOL: 31 | Follow this protocol exactly to ensure safe and effective CLI command execution using the ludus_cli_execute tool. 32 | 33 | STEP 1: SAFETY ASSESSMENT 34 | - Analyze the intent: "${command_intent}" 35 | - Determine if this is a: 36 | * INFORMATIONAL command (help, status, list, info) → LOW RISK 37 | * CONFIGURATION command (set, config, update) → MEDIUM RISK 38 | * DEPLOYMENT command (deploy, start, build) → HIGH RISK 39 | * DESTRUCTIVE command (destroy, delete, remove, abort) → CRITICAL RISK 40 | 41 | STEP 2: TOOL PREFERENCE CHECK 42 | Before using raw CLI, check if a dedicated MCP tool exists: 43 | - Range deployment → use \`deploy_range\` tool instead 44 | - Range destruction → use \`destroy_range\` tool instead 45 | - Range status → use \`get_range_status\` tool instead 46 | - User management → use \`list_user_ranges\` tool instead 47 | - Configuration → use \`set_range_config\` tool instead 48 | - Help information → use \`ludus_help\` tool instead 49 | 50 | ONLY use \`ludus_cli_execute\` when: 51 | - No dedicated tool exists for your specific need 52 | - You need advanced CLI features not covered by tools 53 | - Troubleshooting requires raw CLI access 54 | - User explicitly requests direct CLI access 55 | 56 | STEP 3: PRE-EXECUTION STATE CHECK 57 | For all non-informational commands: 58 | 1. Use \`get_range_status\` to check current range state 59 | 2. Use \`list_user_ranges\` to understand existing resources 60 | 3. Document what will be affected by your command 61 | 62 | STEP 4: DESTRUCTIVE ACTION PROTOCOL 63 | For destructive commands (destroy, delete, remove, abort): 64 | - MANDATORY: Explain exactly what will be destroyed/affected 65 | - MANDATORY: Ask for explicit user confirmation 66 | - MANDATORY: Wait for user to confirm before proceeding 67 | - NEVER execute destructive commands without confirmation 68 | - Set \`confirm_destructive: true\` only AFTER user confirms 69 | 70 | Examples of DESTRUCTIVE commands requiring confirmation: 71 | - \`range rm\` or \`range destroy\` → Deletes entire range permanently 72 | - \`user rm\` → Removes user account and all data 73 | - \`range abort\` → Stops deployment, may leave partial state 74 | 75 | STEP 5: ADMIN OPERATION AWARENESS 76 | ${target_user ? ` 77 | ADMIN OPERATION DETECTED: 78 | - Target user: "${target_user}" 79 | - This affects another user's resources 80 | - Ensure you have proper authorization 81 | - Explain impact to target user's environment 82 | ` : ` 83 | USER OPERATION: 84 | - Affects only current user's resources 85 | - No special permissions needed 86 | `} 87 | 88 | STEP 6: COMMAND EXECUTION WITH ludus_cli_execute 89 | Use the ludus_cli_execute MCP tool for all CLI command execution: 90 | - DO NOT include "ludus" prefix (tool adds it automatically) 91 | - Use: \`ludus_cli_execute({ command: "range status" })\` 92 | - NOT: \`ludus_cli_execute({ command: "ludus range status" })\` 93 | - For help: \`ludus_cli_execute({ command: "--help" })\` or \`ludus_cli_execute({ command: "range --help" })\` 94 | - NOT: \`ludus_cli_execute({ command: "help" })\` (this subcommand doesn't exist) 95 | ${target_user ? `- Include user parameter: \`ludus_cli_execute({ command: "range status", user: "${target_user}" })\`` : ''} 96 | 97 | STEP 7: ERROR HANDLING & TROUBLESHOOTING 98 | If ludus_cli_execute command fails: 99 | 1. Check command syntax with help: \`ludus_cli_execute({ command: "command --help" })\` 100 | 2. Verify connectivity and permissions 101 | 3. Try simpler related commands to isolate the issue 102 | 4. Suggest specific tools if available for the task 103 | 104 | CRITICAL BEHAVIORAL RULES: 105 | SAFETY FIRST: Always prioritize user safety and data protection 106 | CHECK BEFORE ACTING: Understand current state before making changes 107 | COMMUNICATE CLEARLY: Explain what commands will do before execution 108 | CONFIRM DESTRUCTIVE: Never assume destructive actions are wanted 109 | USE SPECIFIC TOOLS: Prefer dedicated tools over raw CLI 110 | ADMIN AWARENESS: Be explicit about admin operations affecting other users 111 | USE CLI WRAPPER: Execute all CLI commands through ludus_cli_execute tool 112 | 113 | EXECUTION GUIDANCE: 114 | ${isDestructiveConfirmed ? 115 | 'PROCEED with destructive action - user has confirmed' : 116 | 'If destructive: STOP and ask for confirmation first' 117 | } 118 | 119 | Execute the command now using the ludus_cli_execute tool following this protocol exactly.` 120 | } 121 | } 122 | ] 123 | }; 124 | } -------------------------------------------------------------------------------- /src/tools/ludusPower.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { Logger } from '../utils/logger.js'; 3 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 4 | 5 | export interface LudusPowerArgs { 6 | action: 'on' | 'off'; 7 | user?: string; 8 | vmNames?: string; 9 | confirmDestructiveAction?: boolean; 10 | help?: boolean; 11 | } 12 | 13 | export function createLudusPowerTool(logger: Logger, cliWrapper: LudusCliWrapper): Tool { 14 | return { 15 | name: 'ludus_power', 16 | description: 'Power management for Ludus range VMs. Can power on or off specific VMs or all VMs in a range. Power off operations require confirmation as they may interrupt running processes.', 17 | inputSchema: { 18 | type: 'object', 19 | properties: { 20 | action: { 21 | type: 'string', 22 | enum: ['on', 'off'], 23 | description: 'Power action to perform on range VMs' 24 | }, 25 | user: { 26 | type: 'string', 27 | description: 'User ID to manage power for (admin only). If omitted, manages power for current user.' 28 | }, 29 | vmNames: { 30 | type: 'string', 31 | description: 'VM name(s) to power on/off. Can be a single VM name, comma-separated list, or "all" for all VMs. Defaults to "all".', 32 | default: 'all' 33 | }, 34 | confirmDestructiveAction: { 35 | type: 'boolean', 36 | description: 'Required confirmation for power off operations. Must be true to power off VMs.', 37 | default: false 38 | }, 39 | help: { 40 | type: 'boolean', 41 | description: 'Show help information for the ludus power command', 42 | default: false 43 | } 44 | }, 45 | required: ['action'] 46 | } 47 | }; 48 | } 49 | 50 | export async function handleLudusPower( 51 | args: LudusPowerArgs, 52 | logger: Logger, 53 | cliWrapper: LudusCliWrapper 54 | ): Promise { 55 | const { action, user, vmNames = 'all', confirmDestructiveAction = false, help = false } = args; 56 | 57 | // Handle help request 58 | if (help) { 59 | logger.info('Getting help for ludus power command', { action, user, vmNames }); 60 | const result = await cliWrapper.executeArbitraryCommand('power', ['--help']); 61 | 62 | if (result.success) { 63 | return { 64 | success: true, 65 | message: 'Help information for ludus power command', 66 | help: true, 67 | content: result.rawOutput || result.message 68 | }; 69 | } else { 70 | throw new Error(`Failed to get help: ${result.message}`); 71 | } 72 | } 73 | 74 | // Safety check for power off operations 75 | if (action === 'off' && !confirmDestructiveAction) { 76 | return { 77 | success: false, 78 | message: 'Power off operation requires confirmation', 79 | action, 80 | user: user || 'current user', 81 | confirmationRequired: true, 82 | reason: 'Powering off VMs may interrupt running processes and could cause data loss', 83 | instructions: [ 84 | 'To confirm this action, call the tool again with confirmDestructiveAction: true', 85 | 'Example: ludus_power({ action: "off", confirmDestructiveAction: true })', 86 | 'This will power off all VMs in the range immediately' 87 | ] 88 | }; 89 | } 90 | 91 | try { 92 | logger.info('Executing power management command', { action, user, vmNames }); 93 | 94 | const targetUser = user || 'current user'; 95 | let result; 96 | 97 | if (action === 'on') { 98 | result = await cliWrapper.powerOnRange(user, vmNames); 99 | } else if (action === 'off') { 100 | result = await cliWrapper.powerOffRange(user, vmNames); 101 | } else { 102 | throw new Error(`Invalid action: ${action}. Must be 'on' or 'off'.`); 103 | } 104 | 105 | if (result.success) { 106 | const actionText = action === 'on' ? 'powered on' : 'powered off'; 107 | const statusEmoji = action === 'on' ? '🟢' : '🔴'; 108 | const vmTarget = vmNames === 'all' ? 'All VMs' : `VM(s): ${vmNames}`; 109 | 110 | return { 111 | success: true, 112 | message: `${vmTarget} successfully ${actionText} for ${targetUser}`, 113 | action, 114 | user: targetUser, 115 | vmNames, 116 | status: actionText, 117 | data: result.data, 118 | nextSteps: action === 'on' ? [ 119 | 'VMs are starting up - this may take a few minutes', 120 | 'Use get_range_status() to monitor VM status', 121 | 'Use get_connection_info() once VMs are fully running' 122 | ] : [ 123 | `VMs (${vmNames}) have been powered off`, 124 | `Use ludus_power({ action: "on", vmNames: "${vmNames}" }) to power them back on`, 125 | 'Or use get_range_status() to check current status' 126 | ], 127 | statusIcon: statusEmoji 128 | }; 129 | } else { 130 | throw new Error(result.message); 131 | } 132 | 133 | } catch (error: any) { 134 | logger.error('Power management command failed', { 135 | action, 136 | user, 137 | vmNames, 138 | error: error.message 139 | }); 140 | 141 | return { 142 | success: false, 143 | message: error.message, 144 | action, 145 | user: user || 'current user', 146 | troubleshooting: [ 147 | 'Verify the user has a deployed range', 148 | 'Check if you have admin permissions (if managing other users)', 149 | 'Ensure the range exists and is accessible', 150 | 'Try get_range_status() to check current range state', 151 | 'DOCUMENTATION SEARCH: If help menus don\'t provide sufficient information, use ludus_docs_search to access comprehensive official documentation with search capabilities.' 152 | ] 153 | }; 154 | } 155 | } -------------------------------------------------------------------------------- /base-configs/sccm-lab.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://docs.ludus.cloud/schemas/range-config.json 2 | # SCCM (System Center Configuration Manager) Lab 3 | # Enterprise SCCM infrastructure with DC, workstation, and SCCM servers 4 | # 5 | # IMPORTANT SCCM CONFIGURATION NOTES: 6 | # - Requires SCCM Ansible Collection installed by running 7 | # ludus ansible collection add synzack.ludus_sccm if not already installed 8 | # - Due to unknown issues with SCCM, .local domain suffixes will not work properly. 9 | # We recommend using something else such as .domain or .lab for your domain suffix 10 | # - If you wish to add client push to the DC, you will need to enable Remote Scheduled 11 | # Tasks Management firewall rules or use the disable_firewall role 12 | # - At this time, all 4 site server roles are needed to deploy SCCM, there is no 13 | # standalone option yet 14 | # - All SCCM VM hostnames MUST be 15 characters or less 15 | # When using hosts which you will join to sccm with client push either put the hosts before the sitesrv in the order within the config or use depends_on like 16 | #roles: 17 | # - name:synzack.ludus_sccm.ludus_sccm_siteserver 18 | # depends_on: 19 | # - vm_name: "{{ range_id }}-Workstation" 20 | # role: synzack.ludus_sccm.disable_firewall 21 | 22 | 23 | ludus: 24 | - vm_name: "{{ range_id }}-DC01" 25 | hostname: "DC01" 26 | template: win2022-server-x64-template 27 | vlan: 10 28 | ip_last_octet: 10 29 | ram_gb: 4 30 | ram_min_gb: 1 31 | cpus: 2 32 | windows: 33 | sysprep: true 34 | domain: 35 | fqdn: ludus.domain 36 | role: primary-dc 37 | roles: 38 | - synzack.ludus_sccm.install_adcs 39 | - synzack.ludus_sccm.disable_firewall 40 | 41 | - vm_name: "{{ range_id }}-Workstation" 42 | hostname: "Workstation" 43 | template: win11-22h2-x64-enterprise-template 44 | vlan: 10 45 | ip_last_octet: 11 46 | ram_gb: 4 47 | ram_min_gb: 1 48 | cpus: 2 49 | windows: 50 | sysprep: true 51 | domain: 52 | fqdn: ludus.domain 53 | role: member 54 | roles: 55 | - synzack.ludus_sccm.disable_firewall 56 | 57 | - vm_name: "{{ range_id }}-sccm-distro" 58 | hostname: "sccm-distro" 59 | template: win2022-server-x64-template 60 | vlan: 10 61 | ip_last_octet: 12 62 | ram_gb: 4 63 | ram_min_gb: 1 64 | cpus: 4 65 | windows: 66 | sysprep: true 67 | domain: 68 | fqdn: ludus.domain 69 | role: member 70 | roles: 71 | - synzack.ludus_sccm.ludus_sccm_distro 72 | role_vars: 73 | ludus_sccm_site_server_hostname: 'sccm-sitesrv' 74 | 75 | - vm_name: "{{ range_id }}-sccm-sql" 76 | hostname: "sccm-sql" 77 | template: win2022-server-x64-template 78 | vlan: 10 79 | ip_last_octet: 13 80 | ram_gb: 4 81 | ram_min_gb: 1 82 | cpus: 4 83 | windows: 84 | sysprep: true 85 | domain: 86 | fqdn: ludus.domain 87 | role: member 88 | roles: 89 | - synzack.ludus_sccm.ludus_sccm_sql 90 | role_vars: 91 | ludus_sccm_site_server_hostname: 'sccm-sitesrv' 92 | ludus_sccm_sql_server_hostname: 'sccm-sql' 93 | ludus_sccm_sql_svc_account_username: 'sqlsccmsvc' 94 | ludus_sccm_sql_svc_account_password: 'Password123' 95 | 96 | - vm_name: "{{ range_id }}-sccm-mgmt" 97 | hostname: "sccm-mgmt" 98 | template: win2022-server-x64-template 99 | vlan: 10 100 | ip_last_octet: 14 101 | ram_gb: 4 102 | ram_min_gb: 1 103 | cpus: 4 104 | windows: 105 | sysprep: true 106 | domain: 107 | fqdn: ludus.domain 108 | role: member 109 | roles: 110 | - synzack.ludus_sccm.ludus_sccm_mgmt 111 | role_vars: 112 | ludus_sccm_site_server_hostname: "sccm-sitesrv" 113 | 114 | - vm_name: "{{ range_id }}-sccm-sitesrv" 115 | hostname: "sccm-sitesrv" 116 | template: win2022-server-x64-template 117 | vlan: 10 118 | ip_last_octet: 15 119 | ram_gb: 4 120 | ram_min_gb: 1 121 | cpus: 4 122 | windows: 123 | sysprep: true 124 | autologon_user: domainadmin 125 | autologon_password: password 126 | domain: 127 | fqdn: ludus.domain 128 | role: member 129 | roles: 130 | - synzack.ludus_sccm.ludus_sccm_siteserver 131 | - synzack.ludus_sccm.enable_webdav 132 | role_vars: 133 | ludus_sccm_sitecode: 123 134 | ludus_sccm_sitename: Primary Site 135 | ludus_sccm_site_server_hostname: 'sccm-sitesrv' 136 | ludus_sccm_distro_server_hostname: 'sccm-distro' 137 | ludus_sccm_mgmt_server_hostname: 'sccm-mgmt' 138 | ludus_sccm_sql_server_hostname: 'sccm-sql' 139 | # --------------------------NAA Account------------------------------------------------- 140 | ludus_sccm_configure_naa: true 141 | ludus_sccm_naa_username: 'sccm_naa' 142 | ludus_sccm_naa_password: 'Password123' 143 | # --------------------------Client Push Account----------------------------------------- 144 | ludus_sccm_configure_client_push: true 145 | ludus_sccm_client_push_username: 'sccm_push' 146 | ludus_sccm_client_push_password: 'Password123' 147 | ludus_sccm_enable_automatic_client_push_installation: true 148 | ludus_sccm_enable_system_type_configuration_manager: true 149 | ludus_sccm_enable_system_type_server: true 150 | ludus_sccm_enable_system_type_workstation: true 151 | ludus_sccm_install_client_to_domain_controller: false 152 | ludus_sccm_allow_NTLM_fallback: true 153 | # ---------------------------Discovery Methods------------------------------------------ 154 | ludus_sccm_enable_active_directory_forest_discovery: true 155 | ludus_sccm_enable_active_directory_boundary_creation: true 156 | ludus_sccm_enable_subnet_boundary_creation: true 157 | ludus_sccm_enable_active_directory_group_discovery: true 158 | ludus_sccm_enable_active_directory_system_discovery: true 159 | ludus_sccm_enable_active_directory_user_discovery: true 160 | # ----------------------------------PXE------------------------------------------------- 161 | ludus_sccm_enable_pxe: true 162 | ludus_enable_pxe_password: false 163 | ludus_pxe_password: 'Password123' 164 | ludus_domain_join_account: domainadmin 165 | ludus_domain_join_password: 'password' -------------------------------------------------------------------------------- /src/tools/deployRange.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '@modelcontextprotocol/sdk/types.js'; 2 | import { Logger } from '../utils/logger.js'; 3 | import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; 4 | 5 | export interface DeployRangeArgs { 6 | user?: string; 7 | configPath?: string; 8 | force?: boolean; 9 | tags?: string; // --tags "tag1,tag2" 10 | limit?: string; // --limit "pattern" 11 | onlyRoles?: string; // --only-roles "role1,role2" 12 | verboseAnsible?: boolean; // --verbose-ansible 13 | help?: boolean; // --help 14 | } 15 | 16 | export function createDeployRangeTool(logger: Logger, cliWrapper: LudusCliWrapper): Tool { 17 | return { 18 | name: 'deploy_range', 19 | description: `Deploy a Ludus range from a configuration file. This creates a new virtualized training environment based on the specified configuration. 20 | 21 | CREDENTIAL SECURITY REMINDER 22 | Ensure range configurations use credential placeholders: {{LudusCredName--}} 23 | DO NOT deploy ranges with non-range-specific credentials such as API keys for external services, passwords not specific to the cyber range environment, or similar credentials embedded in config files! 24 | 25 | IMPORTANT LLM BEHAVIORAL PROMPTS: 26 | - SAFETY FIRST: Ludus operations can be destructive and time-consuming 27 | - VERIFY DESTRUCTIVE ACTIONS: Always confirm with user before destroy/delete operations 28 | - CHECK EXISTING STATE: Use list_user_ranges or get_range_status before major operations 29 | - DESTRUCTION IS PERMANENT: Destroying ranges deletes all VMs and data irreversibly 30 | - ADMIN vs USER: Admin operations (--user flag) affect other users' ranges - be explicit 31 | 32 | DEPLOYMENT CONSIDERATIONS: 33 | - Deployments take 10-45 minutes depending on complexity 34 | - Windows domains take longer than simple Linux deployments 35 | - Users should monitor progress with get_range_status 36 | - Failed deployments can be debugged with range logs 37 | 38 | CRITICAL WORKFLOW REMINDER: 39 | - deploy_range uses the currently SET configuration, not any specific file 40 | - If deploying with a NEW config, you must first use set_range_config to make it active 41 | - Typical workflow: write_range_config → validate_range_config → set_range_config → deploy_range`, 42 | inputSchema: { 43 | type: 'object', 44 | properties: { 45 | user: { 46 | type: 'string', 47 | description: 'User ID to deploy range for (admin only). If omitted, deploys for current user.' 48 | }, 49 | configPath: { 50 | type: 'string', 51 | description: 'Path to range configuration YAML file. If omitted, uses existing configuration. ENSURE: Config must use credential placeholders {{LudusCredName--}}, NOT actual credentials!' 52 | }, 53 | force: { 54 | type: 'boolean', 55 | description: 'Force deployment even if range already exists', 56 | default: false 57 | }, 58 | tags: { 59 | type: 'string', 60 | description: 'Ansible tags to run for this deploy (comma-separated, e.g. "dns,custom-groups"). Default: all tags' 61 | }, 62 | limit: { 63 | type: 'string', 64 | description: 'Limit deployment to VMs matching the specified pattern (must include localhost or no plays will run)' 65 | }, 66 | onlyRoles: { 67 | type: 'string', 68 | description: 'Limit user-defined roles to this comma-separated list (e.g. "role1,role2")' 69 | }, 70 | verboseAnsible: { 71 | type: 'boolean', 72 | description: 'Enable verbose output from Ansible during deployment', 73 | default: false 74 | }, 75 | help: { 76 | type: 'boolean', 77 | description: 'Show help information for the deploy_range command', 78 | default: false 79 | } 80 | }, 81 | required: [] 82 | } 83 | }; 84 | } 85 | 86 | export async function handleDeployRange( 87 | args: DeployRangeArgs, 88 | logger: Logger, 89 | cliWrapper: LudusCliWrapper 90 | ): Promise { 91 | const { 92 | user, 93 | configPath, 94 | force = false, 95 | tags, 96 | limit, 97 | onlyRoles, 98 | verboseAnsible = false, 99 | help = false 100 | } = args; 101 | 102 | // Handle help request 103 | if (help) { 104 | logger.info('Getting help for deploy_range command', { user }); 105 | const result = await cliWrapper.executeArbitraryCommand('range', ['deploy', '--help']); 106 | 107 | if (result.success) { 108 | return { 109 | success: true, 110 | message: 'Help information for deploy_range command', 111 | help: true, 112 | content: result.rawOutput || result.message 113 | }; 114 | } else { 115 | throw new Error(`Failed to get help: ${result.message}`); 116 | } 117 | } 118 | 119 | try { 120 | logger.info('Starting range deployment', { 121 | user, 122 | configPath, 123 | force, 124 | tags, 125 | limit, 126 | onlyRoles, 127 | verboseAnsible 128 | }); 129 | 130 | // Deploy the range with all options (deployRange handles config setting internally) 131 | logger.info('Deploying range'); 132 | 133 | // Build options object, filtering out undefined values 134 | const deployOptions: any = { force, verboseAnsible }; 135 | if (user !== undefined) deployOptions.user = user; 136 | if (configPath !== undefined) deployOptions.configPath = configPath; 137 | if (tags !== undefined) deployOptions.tags = tags; 138 | if (limit !== undefined) deployOptions.limit = limit; 139 | if (onlyRoles !== undefined) deployOptions.onlyRoles = onlyRoles; 140 | 141 | const deployResult = await cliWrapper.deployRange(deployOptions); 142 | 143 | if (!deployResult.success) { 144 | throw new Error(`Deployment failed: ${deployResult.message}`); 145 | } 146 | 147 | const successMessage = user 148 | ? `Range deployment initiated for user ${user}` 149 | : 'Range deployment initiated for current user'; 150 | 151 | return { 152 | success: true, 153 | message: successMessage, 154 | details: deployResult.data, 155 | user: user || 'current', 156 | configPath: configPath || 'existing configuration', 157 | rawOutput: deployResult.rawOutput 158 | }; 159 | 160 | } catch (error: any) { 161 | logger.error('Range deployment failed', { 162 | user, 163 | configPath, 164 | error: error.message 165 | }); 166 | 167 | return { 168 | success: false, 169 | message: error.message, 170 | user: user || 'current', 171 | configPath: configPath || 'existing configuration' 172 | }; 173 | } 174 | } -------------------------------------------------------------------------------- /schemas/5tuk0v.ludus_wsus.yaml: -------------------------------------------------------------------------------- 1 | name: 5tuk0v.ludus_wsus 2 | type: role 3 | version: 1.0.0 4 | description: Installs Windows Server Update Services (WSUS) on Windows Server and optionally configures products, classifications, and synchronization schedules 5 | repository: https://github.com/5tuk0v/ludus_wsus 6 | author: 5tuk0v (@5tuk0v) 7 | dependencies: [] 8 | installation_method: ludus ansible role add 5tuk0v.ludus_wsus 9 | note: Supports Windows Server 2012 R2, 2016, 2019, 2022, and 2025. Initial sync can take hours depending on selected products/classifications 10 | warning: With default settings (empty products/classifications lists), NO updates are synchronized. Configure products and classifications for your needs 11 | 12 | variables: 13 | # Storage Configuration 14 | ludus_wsus_content_folder: 15 | type: string 16 | required: false 17 | default: 'C:\WSUS\Content' 18 | description: Update content storage location on the WSUS server 19 | example: 20 | - 'C:\WSUS\Content' 21 | - 'D:\WSUS\UpdateFiles' 22 | 23 | ludus_wsus_log_folder: 24 | type: string 25 | required: false 26 | default: 'C:\WSUS\Logs' 27 | description: Log file storage location for WSUS operations 28 | example: 29 | - 'C:\WSUS\Logs' 30 | - 'D:\Logs\WSUS' 31 | 32 | # Update Source Configuration 33 | ludus_wsus_sync_from_mu: 34 | type: boolean 35 | required: false 36 | default: true 37 | description: Sync from Microsoft Update (true) or upstream WSUS server (false) 38 | 39 | ludus_wsus_upstream_server_name: 40 | type: string 41 | required: false 42 | default: '' 43 | description: Upstream WSUS server hostname/IP (used when sync_from_mu is false) 44 | example: 45 | - 'upstream-wsus.example.com' 46 | - '192.168.1.100' 47 | 48 | ludus_wsus_upstream_server_port: 49 | type: number 50 | required: false 51 | default: 8530 52 | description: Port for upstream WSUS server connection 53 | example: 54 | - 8530 55 | - 8531 56 | 57 | ludus_wsus_upstream_server_use_ssl: 58 | type: boolean 59 | required: false 60 | default: false 61 | description: Use SSL for upstream WSUS server connection 62 | 63 | ludus_wsus_upstream_server_replica: 64 | type: boolean 65 | required: false 66 | default: false 67 | description: Configure as replica of upstream WSUS server 68 | 69 | # Proxy Configuration 70 | ludus_wsus_proxy_name: 71 | type: string 72 | required: false 73 | default: '' 74 | description: Proxy server hostname/IP (empty = no proxy) 75 | example: 76 | - 'proxy.example.com' 77 | - '192.168.1.50' 78 | 79 | ludus_wsus_proxy_port: 80 | type: number 81 | required: false 82 | default: 80 83 | description: Proxy server port number 84 | example: 85 | - 80 86 | - 8080 87 | - 3128 88 | 89 | ludus_wsus_proxy_user_name: 90 | type: string 91 | required: false 92 | default: '' 93 | description: Username for proxy authentication 94 | 95 | ludus_wsus_proxy_password: 96 | type: string 97 | required: false 98 | default: '' 99 | description: Password for proxy authentication 100 | 101 | # Products and Classifications 102 | ludus_wsus_products_list: 103 | type: array 104 | required: false 105 | default: [] 106 | description: Products to synchronize updates for. Empty by default for fast deployment - configure manually in GUI or specify needed products 107 | example: 108 | - ['Windows Server 2016'] 109 | - ['Windows Server 2019', 'Microsoft Server operating system-21H2'] 110 | - ['Microsoft Server Operating System-24H2'] 111 | 112 | ludus_wsus_classifications_list: 113 | type: array 114 | required: false 115 | default: [] 116 | description: Update classifications to synchronize. Empty by default for fast deployment - configure manually in GUI or specify needed classifications 117 | example: 118 | - ['Critical Updates'] 119 | - ['Critical Updates', 'Security Updates'] 120 | - ['Critical Updates', 'Security Updates', 'Definition Updates'] 121 | 122 | ludus_wsus_update_languages: 123 | type: array 124 | required: false 125 | default: ['en'] 126 | description: Update languages to synchronize 127 | example: 128 | - ['en'] 129 | - ['en', 'es'] 130 | - ['en', 'fr', 'de'] 131 | 132 | # Client Configuration 133 | ludus_wsus_targeting_mode: 134 | type: string 135 | required: false 136 | default: 'Server' 137 | description: Computer targeting mode for group assignment 138 | valid_options: 139 | - 'Server' 140 | - 'Client' 141 | 142 | ludus_wsus_computer_target_group_list: 143 | type: array 144 | required: false 145 | default: ['Domain Controllers', 'Servers', 'Workstations'] 146 | description: Computer target groups to create in WSUS 147 | example: 148 | - ['Domain Controllers', 'Servers', 'Workstations'] 149 | - ['Production', 'Testing', 'Development'] 150 | - ['Critical Systems', 'Standard Systems'] 151 | 152 | # Synchronization Settings 153 | ludus_wsus_initial_sync: 154 | type: boolean 155 | required: false 156 | default: false 157 | description: Perform initial synchronization during deployment (can take hours) 158 | 159 | ludus_wsus_wait_for_sync: 160 | type: boolean 161 | required: false 162 | default: false 163 | description: Wait for synchronization to complete before continuing 164 | 165 | ludus_wsus_sync_timeout: 166 | type: number 167 | required: false 168 | default: 14400 169 | description: Maximum time in seconds to wait for synchronization (4 hours default) 170 | example: 171 | - 14400 172 | - 7200 173 | - 28800 174 | 175 | ludus_wsus_category_sync_timeout_minutes: 176 | type: number 177 | required: false 178 | default: 60 179 | description: Timeout in minutes for initial category synchronization 180 | example: 181 | - 60 182 | - 30 183 | - 120 184 | 185 | ludus_wsus_enable_auto_sync: 186 | type: boolean 187 | required: false 188 | default: false 189 | description: Enable automatic synchronization on a schedule 190 | 191 | ludus_wsus_sync_daily_time: 192 | type: object 193 | required: false 194 | default: {hour: 3, minute: 0} 195 | description: Local time for scheduled synchronization (automatically converted to UTC) 196 | example: 197 | - {hour: 3, minute: 0} 198 | - {hour: 22, minute: 30} 199 | - {hour: 1, minute: 15} 200 | 201 | ludus_wsus_syncs_per_day: 202 | type: number 203 | required: false 204 | default: 1 205 | description: Number of synchronizations per day (1-24) 206 | valid_options: [1, 2, 3, 4, 6, 8, 12, 24] 207 | example: 208 | - 1 209 | - 2 210 | - 4 211 | 212 | # Approval Settings 213 | ludus_wsus_enable_default_approval_rule: 214 | type: boolean 215 | required: false 216 | default: false 217 | description: Auto-approve updates using the default approval rule -------------------------------------------------------------------------------- /src/utils/downloadDocs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | import { execSync } from 'child_process'; 5 | import { Logger } from './logger.js'; 6 | 7 | export async function downloadLudusDocumentation(logger: Logger): Promise { 8 | const repoUrl = 'https://gitlab.com/badsectorlabs/ludus.git'; 9 | const targetPath = 'docs/docs'; 10 | const ludusConfigDir = path.join(os.homedir(), '.ludus-mcp'); 11 | const docsDir = path.join(ludusConfigDir, 'docs'); 12 | const tempDir = path.join(ludusConfigDir, 'temp-docs-download'); 13 | 14 | try { 15 | logger.info('Downloading fresh Ludus documentation...'); 16 | 17 | // Ensure .ludus-mcp directory exists 18 | await fs.mkdir(ludusConfigDir, { recursive: true }); 19 | 20 | // Clean up any existing temp and docs directories (Windows-safe) 21 | await removeDirectorySafe(tempDir); 22 | await removeDirectorySafe(docsDir); 23 | 24 | // Clone only the specific docs folder 25 | logger.info('Cloning documentation from GitLab repository...'); 26 | execSync(`git clone --filter=blob:none --sparse --depth 1 "${repoUrl}" "${tempDir}"`, { stdio: 'pipe' }); 27 | execSync(`git -C "${tempDir}" sparse-checkout set "${targetPath}"`, { stdio: 'pipe' }); 28 | 29 | // Move docs to final location (Windows-safe approach) 30 | const sourceDocsPath = path.join(tempDir, targetPath); 31 | await copyDirectoryRecursive(sourceDocsPath, docsDir); 32 | 33 | // Verify we got the important subdirectories 34 | const importantDirs = ['environment-guides', 'quick-start', 'troubleshooting']; 35 | const foundDirs: string[] = []; 36 | const missingDirs: string[] = []; 37 | 38 | for (const dirName of importantDirs) { 39 | const dirPath = path.join(docsDir, dirName); 40 | try { 41 | const stat = await fs.stat(dirPath); 42 | if (stat.isDirectory()) { 43 | const files = await findMarkdownFiles(dirPath); 44 | foundDirs.push(`${dirName} (${files.length} files)`); 45 | } else { 46 | missingDirs.push(dirName); 47 | } 48 | } catch { 49 | missingDirs.push(dirName); 50 | } 51 | } 52 | 53 | // Clean up temp directory 54 | await removeDirectorySafe(tempDir); 55 | 56 | // Get total file count and structure 57 | const allFiles = await findAllFiles(docsDir); 58 | const markdownFiles = await findMarkdownFiles(docsDir); 59 | 60 | logger.info(`Successfully downloaded Ludus documentation to ~/.ludus-mcp/docs/`, { 61 | totalFiles: allFiles.length, 62 | markdownFiles: markdownFiles.length, 63 | foundDirectories: foundDirs, 64 | missingDirectories: missingDirs.length > 0 ? missingDirs : undefined 65 | }); 66 | 67 | if (foundDirs.length > 0) { 68 | logger.info(`📂 Key documentation sections available: ${foundDirs.join(', ')}`); 69 | } 70 | 71 | if (missingDirs.length > 0) { 72 | logger.warn(` Some expected directories were not found: ${missingDirs.join(', ')}`); 73 | } 74 | 75 | } catch (error) { 76 | const errorMessage = error instanceof Error ? error.message : String(error); 77 | logger.error('Failed to download Ludus documentation', { error: errorMessage }); 78 | 79 | // Clean up on error 80 | await removeDirectorySafe(tempDir); 81 | throw new Error(`Documentation download failed: ${errorMessage}`); 82 | } 83 | } 84 | 85 | async function findAllFiles(dir: string): Promise { 86 | const files: string[] = []; 87 | 88 | try { 89 | const items = await fs.readdir(dir, { withFileTypes: true }); 90 | 91 | for (const item of items) { 92 | const fullPath = path.join(dir, item.name); 93 | 94 | if (item.isDirectory()) { 95 | const subFiles = await findAllFiles(fullPath); 96 | files.push(...subFiles); 97 | } else if (item.isFile()) { 98 | files.push(fullPath); 99 | } 100 | } 101 | } catch (error) { 102 | // Directory doesn't exist or can't be read 103 | } 104 | 105 | return files.sort(); 106 | } 107 | 108 | async function findMarkdownFiles(dir: string): Promise { 109 | const files: string[] = []; 110 | 111 | try { 112 | const items = await fs.readdir(dir, { withFileTypes: true }); 113 | 114 | for (const item of items) { 115 | const fullPath = path.join(dir, item.name); 116 | 117 | if (item.isDirectory()) { 118 | const subFiles = await findMarkdownFiles(fullPath); 119 | files.push(...subFiles); 120 | } else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.mdx'))) { 121 | files.push(fullPath); 122 | } 123 | } 124 | } catch (error) { 125 | // Directory doesn't exist or can't be read 126 | } 127 | 128 | return files.sort(); 129 | } 130 | 131 | /** 132 | * Get directory structure for documentation 133 | */ 134 | export async function getDocsStructure(logger: Logger): Promise { 135 | const ludusConfigDir = path.join(os.homedir(), '.ludus-mcp'); 136 | const docsDir = path.join(ludusConfigDir, 'docs'); 137 | 138 | try { 139 | return await buildDirectoryTree(docsDir, docsDir); 140 | } catch (error) { 141 | logger.error('Failed to get docs structure', { error }); 142 | return null; 143 | } 144 | } 145 | 146 | async function buildDirectoryTree(dirPath: string, basePath: string): Promise { 147 | const items = await fs.readdir(dirPath, { withFileTypes: true }); 148 | const tree: any = { 149 | directories: {}, 150 | files: [] 151 | }; 152 | 153 | for (const item of items) { 154 | const fullPath = path.join(dirPath, item.name); 155 | const relativePath = path.relative(basePath, fullPath); 156 | 157 | if (item.isDirectory()) { 158 | tree.directories[item.name] = await buildDirectoryTree(fullPath, basePath); 159 | } else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.mdx'))) { 160 | tree.files.push({ 161 | name: item.name, 162 | path: relativePath, 163 | fullPath: fullPath 164 | }); 165 | } 166 | } 167 | 168 | return tree; 169 | } 170 | 171 | /** 172 | * Windows-safe recursive directory copy 173 | */ 174 | async function copyDirectoryRecursive(source: string, destination: string): Promise { 175 | // Ensure destination directory exists 176 | await fs.mkdir(destination, { recursive: true }); 177 | 178 | const items = await fs.readdir(source, { withFileTypes: true }); 179 | 180 | for (const item of items) { 181 | const sourcePath = path.join(source, item.name); 182 | const destPath = path.join(destination, item.name); 183 | 184 | if (item.isDirectory()) { 185 | await copyDirectoryRecursive(sourcePath, destPath); 186 | } else if (item.isFile()) { 187 | await fs.copyFile(sourcePath, destPath); 188 | } 189 | } 190 | } 191 | 192 | /** 193 | * Windows-safe directory removal 194 | */ 195 | async function removeDirectorySafe(dirPath: string): Promise { 196 | try { 197 | await fs.access(dirPath); 198 | // Directory exists, try to remove it 199 | await fs.rm(dirPath, { recursive: true, force: true }); 200 | } catch (error) { 201 | // Directory doesn't exist or couldn't be removed - that's fine for cleanup 202 | } 203 | } -------------------------------------------------------------------------------- /src/utils/downloadBaseConfigs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | import { execSync } from 'child_process'; 5 | import { Logger } from './logger.js'; 6 | 7 | export async function downloadBaseConfigs(logger: Logger): Promise { 8 | const repoUrl = 'https://github.com/NocteDefensor/LudusMCP.git'; 9 | const targetPath = 'base-configs'; 10 | const ludusConfigDir = path.join(os.homedir(), '.ludus-mcp'); 11 | const rangeConfigTemplatesDir = path.join(ludusConfigDir, 'range-config-templates'); 12 | const baseConfigsDir = path.join(rangeConfigTemplatesDir, 'base-configs'); 13 | const tempDir = path.join(ludusConfigDir, 'temp-base-configs-download'); 14 | 15 | try { 16 | logger.info('Downloading base configurations from GitHub...'); 17 | 18 | // Ensure .ludus-mcp and range-config-templates directories exist 19 | await fs.mkdir(rangeConfigTemplatesDir, { recursive: true }); 20 | 21 | // Check if base-configs directory exists 22 | const dirExists = await fs.access(baseConfigsDir).then(() => true).catch(() => false); 23 | 24 | if (dirExists) { 25 | logger.info('base-configs directory exists, updating contents...'); 26 | await updateBaseConfigsContents(baseConfigsDir, tempDir, repoUrl, targetPath, logger); 27 | } else { 28 | logger.info('base-configs directory not found, performing full clone...'); 29 | await cloneBaseConfigs(baseConfigsDir, tempDir, repoUrl, targetPath, logger); 30 | } 31 | 32 | // Get file count and structure verification 33 | const configFiles = await findConfigFiles(baseConfigsDir); 34 | 35 | logger.info(`Successfully synchronized base configurations to ~/.ludus-mcp/range-config-templates/base-configs/`, { 36 | configFiles: configFiles.length, 37 | files: configFiles.map(f => path.basename(f)) 38 | }); 39 | 40 | if (configFiles.length > 0) { 41 | logger.info(`Configuration templates available: ${configFiles.map(f => path.basename(f)).join(', ')}`); 42 | } 43 | 44 | } catch (error) { 45 | const errorMessage = error instanceof Error ? error.message : String(error); 46 | logger.warn('Failed to download base configurations (continuing without update)', { error: errorMessage }); 47 | 48 | // Clean up on error 49 | await removeDirectorySafe(tempDir); 50 | // Don't throw - fail gracefully as requested 51 | } 52 | } 53 | 54 | /** 55 | * UPDATE MODE: Directory exists, update contents while preserving custom files 56 | */ 57 | async function updateBaseConfigsContents( 58 | baseConfigsDir: string, 59 | tempDir: string, 60 | repoUrl: string, 61 | targetPath: string, 62 | logger: Logger 63 | ): Promise { 64 | // Clean up any existing temp directory 65 | await removeDirectorySafe(tempDir); 66 | 67 | // Clone to temp location 68 | logger.info('Cloning base-configs from GitHub repository...'); 69 | execSync(`git clone --filter=blob:none --sparse --depth 1 "${repoUrl}" "${tempDir}"`, { stdio: 'pipe' }); 70 | execSync(`git -C "${tempDir}" sparse-checkout set "${targetPath}"`, { stdio: 'pipe' }); 71 | 72 | // Get source configs 73 | const sourceConfigsPath = path.join(tempDir, targetPath); 74 | const sourceFiles = await findConfigFiles(sourceConfigsPath); 75 | 76 | // Copy/overwrite GitHub config files, preserve user files 77 | let updatedCount = 0; 78 | let preservedCount = 0; 79 | 80 | // Get existing files for comparison 81 | const existingFiles = await findConfigFiles(baseConfigsDir); 82 | const existingFileNames = new Set(existingFiles.map(f => path.basename(f))); 83 | 84 | // Copy source files to destination (overwrite GitHub templates) 85 | for (const sourceFile of sourceFiles) { 86 | const fileName = path.basename(sourceFile); 87 | const destFile = path.join(baseConfigsDir, fileName); 88 | 89 | await fs.copyFile(sourceFile, destFile); 90 | updatedCount++; 91 | logger.debug('Updated config file', { fileName }); 92 | } 93 | 94 | // Count preserved user files (files that exist but aren't from GitHub) 95 | const sourceFileNames = new Set(sourceFiles.map(f => path.basename(f))); 96 | for (const existingFileName of existingFileNames) { 97 | if (!sourceFileNames.has(existingFileName)) { 98 | preservedCount++; 99 | logger.debug('Preserved user config file', { fileName: existingFileName }); 100 | } 101 | } 102 | 103 | // Clean up temp directory 104 | await removeDirectorySafe(tempDir); 105 | 106 | logger.info('Base configs update completed', { 107 | updated: updatedCount, 108 | preserved: preservedCount, 109 | total: updatedCount + preservedCount 110 | }); 111 | } 112 | 113 | /** 114 | * CLONE MODE: Directory doesn't exist, full clone 115 | */ 116 | async function cloneBaseConfigs( 117 | baseConfigsDir: string, 118 | tempDir: string, 119 | repoUrl: string, 120 | targetPath: string, 121 | logger: Logger 122 | ): Promise { 123 | // Clean up any existing temp directory 124 | await removeDirectorySafe(tempDir); 125 | 126 | // Clone only the specific base-configs folder 127 | logger.info('Cloning base-configs from GitHub repository...'); 128 | execSync(`git clone --filter=blob:none --sparse --depth 1 "${repoUrl}" "${tempDir}"`, { stdio: 'pipe' }); 129 | execSync(`git -C "${tempDir}" sparse-checkout set "${targetPath}"`, { stdio: 'pipe' }); 130 | 131 | // Move base-configs to final location (Windows-safe approach) 132 | const sourceConfigsPath = path.join(tempDir, targetPath); 133 | await copyDirectoryRecursive(sourceConfigsPath, baseConfigsDir); 134 | 135 | // Clean up temp directory 136 | await removeDirectorySafe(tempDir); 137 | 138 | const configFiles = await findConfigFiles(baseConfigsDir); 139 | logger.info('Base configs clone completed', { 140 | cloned: configFiles.length, 141 | files: configFiles.map(f => path.basename(f)) 142 | }); 143 | } 144 | 145 | /** 146 | * Find all configuration files (.yml, .yaml) in a directory 147 | */ 148 | async function findConfigFiles(dir: string): Promise { 149 | const files: string[] = []; 150 | 151 | try { 152 | const items = await fs.readdir(dir, { withFileTypes: true }); 153 | 154 | for (const item of items) { 155 | const fullPath = path.join(dir, item.name); 156 | 157 | if (item.isDirectory()) { 158 | const subFiles = await findConfigFiles(fullPath); 159 | files.push(...subFiles); 160 | } else if (item.isFile() && (item.name.endsWith('.yml') || item.name.endsWith('.yaml'))) { 161 | files.push(fullPath); 162 | } 163 | } 164 | } catch (error) { 165 | // Directory doesn't exist or can't be read 166 | } 167 | 168 | return files.sort(); 169 | } 170 | 171 | /** 172 | * Windows-safe recursive directory copy 173 | */ 174 | async function copyDirectoryRecursive(source: string, destination: string): Promise { 175 | // Ensure destination directory exists 176 | await fs.mkdir(destination, { recursive: true }); 177 | 178 | const items = await fs.readdir(source, { withFileTypes: true }); 179 | 180 | for (const item of items) { 181 | const sourcePath = path.join(source, item.name); 182 | const destPath = path.join(destination, item.name); 183 | 184 | if (item.isDirectory()) { 185 | await copyDirectoryRecursive(sourcePath, destPath); 186 | } else if (item.isFile()) { 187 | await fs.copyFile(sourcePath, destPath); 188 | } 189 | } 190 | } 191 | 192 | /** 193 | * Windows-safe directory removal 194 | */ 195 | async function removeDirectorySafe(dirPath: string): Promise { 196 | try { 197 | await fs.access(dirPath); 198 | // Directory exists, try to remove it 199 | await fs.rm(dirPath, { recursive: true, force: true }); 200 | } catch (error) { 201 | // Directory doesn't exist or couldn't be removed - that's fine for cleanup 202 | } 203 | } --------------------------------------------------------------------------------