feat(ai): migrate AI features from BYOK to XTM One agents (#149) #386
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: | |
| - main | |
| - develop | |
| pull_request: | |
| branches: | |
| - main | |
| - develop | |
| workflow_dispatch: | |
| jobs: | |
| # Lint and type check - run first and fast | |
| lint: | |
| name: Lint & Type Check | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run ESLint | |
| run: npm run lint | |
| - name: Run TypeScript type check | |
| run: npx tsc --noEmit | |
| # Unit tests - run after lint passes | |
| unit-tests: | |
| name: Unit Tests | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run unit tests with coverage | |
| run: npm run test:unit -- --coverage | |
| - name: Upload unit test coverage | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage-unit | |
| path: coverage/coverage-final.json | |
| retention-days: 1 | |
| # OpenCTI integration tests | |
| integration-opencti: | |
| name: OpenCTI Integration Tests | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 30 | |
| services: | |
| redis: | |
| image: redis:8.6.1 | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| elasticsearch: | |
| image: docker.elastic.co/elasticsearch/elasticsearch:8.19.16 | |
| env: | |
| discovery.type: single-node | |
| xpack.ml.enabled: 'false' | |
| xpack.security.enabled: 'false' | |
| thread_pool.search.queue_size: '5000' | |
| ES_JAVA_OPTS: -Xms2g -Xmx2g | |
| ports: | |
| - 9200:9200 | |
| options: >- | |
| --health-cmd "curl -sf http://localhost:9200 || exit 1" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 30 | |
| minio: | |
| image: lazybit/minio | |
| ports: | |
| - 9000:9000 | |
| - 9001:9001 | |
| env: | |
| MINIO_ROOT_USER: minioadmin | |
| MINIO_ROOT_PASSWORD: minioadmin | |
| options: >- | |
| --health-cmd "curl -sf http://localhost:9000/minio/health/live || exit 1" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| rabbitmq: | |
| image: rabbitmq:4.2-management | |
| ports: | |
| - 5672:5672 | |
| - 15672:15672 | |
| env: | |
| RABBITMQ_DEFAULT_USER: guest | |
| RABBITMQ_DEFAULT_PASS: guest | |
| options: >- | |
| --health-cmd "rabbitmq-diagnostics -q ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Wait for Elasticsearch to be ready | |
| run: | | |
| echo "Waiting for Elasticsearch..." | |
| for i in {1..60}; do | |
| if curl -sf http://localhost:9200 >/dev/null 2>&1; then | |
| echo "Elasticsearch is ready!" | |
| break | |
| fi | |
| echo "Attempt $i: Elasticsearch not ready yet..." | |
| sleep 5 | |
| done | |
| - name: Wait for MinIO to be ready | |
| run: | | |
| echo "Waiting for MinIO..." | |
| for i in {1..30}; do | |
| if curl -sf http://localhost:9000/minio/health/live >/dev/null 2>&1; then | |
| echo "MinIO is ready!" | |
| break | |
| fi | |
| echo "Attempt $i: MinIO not ready yet..." | |
| sleep 2 | |
| done | |
| - name: Pull and run OpenCTI | |
| run: | | |
| docker run -d --name opencti \ | |
| --network host \ | |
| -e NODE_OPTIONS=--max-old-space-size=8096 \ | |
| -e APP__PORT=8080 \ | |
| -e APP__BASE_URL=http://localhost:8080 \ | |
| -e APP__ADMIN__EMAIL=admin@opencti.io \ | |
| -e APP__ADMIN__PASSWORD=admin \ | |
| -e APP__ADMIN__TOKEN=bfa014e0-e02e-4aa6-a42b-603b19dcf159 \ | |
| -e APP__ENCRYPTION_KEY=wbQnxC2ZIoohYzkXW19LiYU2V3lfoCVFkBadFd/dFAY= \ | |
| -e APP__HEALTH_ACCESS_KEY=healthcheck \ | |
| -e REDIS__HOSTNAME=localhost \ | |
| -e REDIS__PORT=6379 \ | |
| -e ELASTICSEARCH__URL=http://localhost:9200 \ | |
| -e ELASTICSEARCH__NUMBER_OF_REPLICAS=0 \ | |
| -e MINIO__ENDPOINT=localhost \ | |
| -e MINIO__PORT=9000 \ | |
| -e MINIO__USE_SSL=false \ | |
| -e MINIO__ACCESS_KEY=minioadmin \ | |
| -e MINIO__SECRET_KEY=minioadmin \ | |
| -e RABBITMQ__HOSTNAME=localhost \ | |
| -e RABBITMQ__PORT=5672 \ | |
| -e RABBITMQ__PORT_MANAGEMENT=15672 \ | |
| -e RABBITMQ__MANAGEMENT_SSL=false \ | |
| -e RABBITMQ__USERNAME=guest \ | |
| -e RABBITMQ__PASSWORD=guest \ | |
| -e SMTP__HOSTNAME=localhost \ | |
| -e SMTP__PORT=25 \ | |
| -e PROVIDERS__LOCAL__STRATEGY=LocalStrategy \ | |
| opencti/platform:latest | |
| - name: Wait for OpenCTI to be ready | |
| run: | | |
| echo "Waiting for OpenCTI to be ready..." | |
| for i in {1..120}; do | |
| if curl -s -H "Authorization: Bearer bfa014e0-e02e-4aa6-a42b-603b19dcf159" http://localhost:8080/graphql -d '{"query":"{ about { version } }"}' -H "Content-Type: application/json" | grep -q "version"; then | |
| echo "OpenCTI is ready!" | |
| curl -s -H "Authorization: Bearer bfa014e0-e02e-4aa6-a42b-603b19dcf159" http://localhost:8080/graphql -d '{"query":"{ about { version } }"}' -H "Content-Type: application/json" | |
| break | |
| fi | |
| echo "Attempt $i: OpenCTI not ready yet..." | |
| docker logs opencti --tail 20 2>/dev/null || true | |
| sleep 10 | |
| done | |
| - name: Seed test data in OpenCTI | |
| run: | | |
| echo "Creating test data in OpenCTI..." | |
| API_URL="http://localhost:8080/graphql" | |
| AUTH_HEADER="Authorization: Bearer bfa014e0-e02e-4aa6-a42b-603b19dcf159" | |
| # Create Threat Actor Groups (actual organizations) | |
| echo "Creating Threat Actor Group GRU..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { threatActorGroupAdd(input: { name: \"GRU\", aliases: [\"Main Intelligence Directorate\", \"Unit 26165\", \"Unit 74455\"], description: \"Russian military intelligence agency\" }) { id name } }"}' || true | |
| echo "Creating Threat Actor Group Lazarus Group..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { threatActorGroupAdd(input: { name: \"Lazarus Group\", aliases: [\"HIDDEN COBRA\", \"Guardians of Peace\"], description: \"North Korean state-sponsored threat actor\" }) { id name } }"}' || true | |
| # Create Intrusion Sets (adversary campaigns/operations) | |
| echo "Creating Intrusion Set APT29..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { intrusionSetAdd(input: { name: \"APT29\", aliases: [\"Cozy Bear\", \"The Dukes\", \"IRON HEMLOCK\"], description: \"Russian cyber espionage group attributed to SVR\" }) { id name } }"}' || true | |
| echo "Creating Intrusion Set APT28..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { intrusionSetAdd(input: { name: \"APT28\", aliases: [\"Fancy Bear\", \"Sofacy\", \"IRON TWILIGHT\"], description: \"Russian cyber espionage group attributed to GRU\" }) { id name } }"}' || true | |
| # Create Malware | |
| echo "Creating Malware Emotet..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { malwareAdd(input: { name: \"Emotet\", aliases: [\"Heodo\", \"Geodo\"], description: \"Banking trojan turned malware distribution service\", malware_types: [\"trojan\"], is_family: true }) { id name } }"}' || true | |
| echo "Creating Malware Ryuk..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { malwareAdd(input: { name: \"Ryuk\", aliases: [\"WIZARD SPIDER\"], description: \"Ransomware targeting enterprises\", malware_types: [\"ransomware\"], is_family: true }) { id name } }"}' || true | |
| # Create Attack Patterns (MITRE ATT&CK) | |
| echo "Creating Attack Pattern T1566 Phishing..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { attackPatternAdd(input: { name: \"Phishing\", x_mitre_id: \"T1566\", description: \"Adversaries may send phishing messages to gain access to victim systems\" }) { id name } }"}' || true | |
| echo "Creating Attack Pattern T1059 Command and Scripting Interpreter..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { attackPatternAdd(input: { name: \"Command and Scripting Interpreter\", x_mitre_id: \"T1059\", description: \"Adversaries may abuse command and script interpreters to execute commands\" }) { id name } }"}' || true | |
| # Create an Indicator | |
| echo "Creating test Indicator..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { indicatorAdd(input: { name: \"Malicious Domain\", pattern: \"[domain-name:value = '\''evil-domain.com'\'']\", pattern_type: \"stix\", valid_from: \"2024-01-01T00:00:00.000Z\" }) { id name } }"}' || true | |
| # Create a Campaign | |
| echo "Creating Campaign SolarWinds..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { campaignAdd(input: { name: \"SolarWinds Campaign\", aliases: [\"SUNBURST\", \"Solorigate\"], description: \"Supply chain attack discovered in December 2020\" }) { id name } }"}' || true | |
| # Create Vulnerabilities (CVEs) | |
| echo "Creating Vulnerability CVE-2021-44228..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { vulnerabilityAdd(input: { name: \"CVE-2021-44228\", description: \"Log4Shell - Remote code execution in Apache Log4j\" }) { id name } }"}' || true | |
| echo "Creating Vulnerability CVE-2023-23397..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { vulnerabilityAdd(input: { name: \"CVE-2023-23397\", description: \"Microsoft Outlook privilege escalation vulnerability\" }) { id name } }"}' || true | |
| # Create Tools | |
| echo "Creating Tool Cobalt Strike..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { toolAdd(input: { name: \"Cobalt Strike\", aliases: [\"CS\", \"Beacon\"], description: \"Commercial adversary simulation and red team operations tool\" }) { id name } }"}' || true | |
| echo "Creating Tool Mimikatz..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { toolAdd(input: { name: \"Mimikatz\", description: \"Credential dumping tool for Windows\" }) { id name } }"}' || true | |
| # Create Labels | |
| echo "Creating Labels..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { labelAdd(input: { value: \"apt\", color: \"#ff0000\" }) { id value } }"}' || true | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { labelAdd(input: { value: \"ransomware\", color: \"#ff6600\" }) { id value } }"}' || true | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"mutation { labelAdd(input: { value: \"state-sponsored\", color: \"#9900ff\" }) { id value } }"}' || true | |
| echo "Test data seeding completed!" | |
| - name: Wait for Elasticsearch indexing | |
| run: | | |
| echo "Waiting for Elasticsearch to index seeded data..." | |
| sleep 15 | |
| - name: Verify test data | |
| run: | | |
| echo "Verifying test data..." | |
| API_URL="http://localhost:8080/graphql" | |
| AUTH_HEADER="Authorization: Bearer bfa014e0-e02e-4aa6-a42b-603b19dcf159" | |
| echo "Querying Threat Actor Groups..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"{ threatActorsGroup(first: 5) { edges { node { id name aliases } } } }"}' | |
| echo "" | |
| echo "Querying Intrusion Sets..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"{ intrusionSets(first: 5) { edges { node { id name aliases } } } }"}' | |
| echo "" | |
| echo "Querying Malware..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"{ malwares(first: 5) { edges { node { id name aliases } } } }"}' | |
| echo "" | |
| echo "Querying Attack Patterns..." | |
| curl -s -X POST "$API_URL" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"{ attackPatterns(first: 5) { edges { node { id name x_mitre_id } } } }"}' | |
| - name: Run OpenCTI integration tests with coverage | |
| env: | |
| OPENCTI_URL: http://localhost:8080 | |
| OPENCTI_TOKEN: bfa014e0-e02e-4aa6-a42b-603b19dcf159 | |
| run: npm run test:opencti -- --coverage | |
| - name: Upload OpenCTI integration test coverage | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: coverage-opencti | |
| path: coverage/coverage-final.json | |
| retention-days: 1 | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| docker stop opencti || true | |
| docker rm opencti || true | |
| # OpenAEV integration tests | |
| integration-openaev: | |
| name: OpenAEV Integration Tests | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 30 | |
| services: | |
| postgres: | |
| image: postgres:18-alpine | |
| env: | |
| POSTGRES_DB: openaev | |
| POSTGRES_USER: openaev | |
| POSTGRES_PASSWORD: openaev | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U openaev -d openaev" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| elasticsearch: | |
| image: docker.elastic.co/elasticsearch/elasticsearch:8.19.16 | |
| env: | |
| discovery.type: single-node | |
| xpack.ml.enabled: 'false' | |
| xpack.security.enabled: 'false' | |
| thread_pool.search.queue_size: '5000' | |
| ES_JAVA_OPTS: -Xms2g -Xmx2g | |
| ports: | |
| - 9200:9200 | |
| options: >- | |
| --health-cmd "curl -sf http://localhost:9200 || exit 1" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 30 | |
| minio: | |
| image: lazybit/minio | |
| ports: | |
| - 9000:9000 | |
| - 9001:9001 | |
| env: | |
| MINIO_ROOT_USER: minioadmin | |
| MINIO_ROOT_PASSWORD: minioadmin | |
| options: >- | |
| --health-cmd "curl -sf http://localhost:9000/minio/health/live || exit 1" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| rabbitmq: | |
| image: rabbitmq:4.2-management | |
| ports: | |
| - 5672:5672 | |
| - 15672:15672 | |
| env: | |
| RABBITMQ_DEFAULT_USER: guest | |
| RABBITMQ_DEFAULT_PASS: guest | |
| options: >- | |
| --health-cmd "rabbitmq-diagnostics -q ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Wait for Elasticsearch to be ready | |
| run: | | |
| echo "Waiting for Elasticsearch..." | |
| for i in {1..60}; do | |
| if curl -sf http://localhost:9200 >/dev/null 2>&1; then | |
| echo "Elasticsearch is ready!" | |
| break | |
| fi | |
| echo "Attempt $i: Elasticsearch not ready yet..." | |
| sleep 5 | |
| done | |
| - name: Wait for MinIO to be ready | |
| run: | | |
| echo "Waiting for MinIO..." | |
| for i in {1..30}; do | |
| if curl -sf http://localhost:9000/minio/health/live >/dev/null 2>&1; then | |
| echo "MinIO is ready!" | |
| break | |
| fi | |
| echo "Attempt $i: MinIO not ready yet..." | |
| sleep 2 | |
| done | |
| - name: Pull and run OpenAEV | |
| run: | | |
| docker run -d --name openaev \ | |
| --network host \ | |
| -e OPENAEV_BASE-URL=http://localhost:8080 \ | |
| -e OPENAEV_AUTH-LOCAL-ENABLE=true \ | |
| -e OPENAEV_ADMIN_EMAIL=admin@openaev.io \ | |
| -e OPENAEV_ADMIN_PASSWORD=admin \ | |
| -e OPENAEV_ADMIN_TOKEN=bfa014e0-e02e-4aa6-a42b-603b19dcf159 \ | |
| -e OPENAEV_ADMIN_ENCRYPTION_KEY=ChangeMe \ | |
| -e OPENAEV_ADMIN_ENCRYPTION_SALT=ChangeMeSalt \ | |
| -e OPENAEV_HEALTHCHECK_KEY=healthcheck \ | |
| -e SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/openaev \ | |
| -e SPRING_DATASOURCE_USERNAME=openaev \ | |
| -e SPRING_DATASOURCE_PASSWORD=openaev \ | |
| -e MINIO_ENDPOINT=localhost \ | |
| -e MINIO_ACCESS-KEY=minioadmin \ | |
| -e MINIO_ACCESS-SECRET=minioadmin \ | |
| -e OPENAEV_RABBITMQ_HOSTNAME=localhost \ | |
| -e OPENAEV_RABBITMQ_USER=guest \ | |
| -e OPENAEV_RABBITMQ_PASS=guest \ | |
| -e ENGINE_URL=http://localhost:9200 \ | |
| openaev/platform:latest || echo "OpenAEV image may not be available, running mock tests only" | |
| - name: Wait for OpenAEV to be ready | |
| run: | | |
| echo "Waiting for OpenAEV to be ready..." | |
| for i in {1..90}; do | |
| if curl -s -H "Authorization: Bearer bfa014e0-e02e-4aa6-a42b-603b19dcf159" http://localhost:8080/api/settings 2>/dev/null | grep -q "platform"; then | |
| echo "OpenAEV is ready!" | |
| curl -s -H "Authorization: Bearer bfa014e0-e02e-4aa6-a42b-603b19dcf159" http://localhost:8080/api/settings | |
| break | |
| fi | |
| if [ $i -eq 90 ]; then | |
| echo "OpenAEV did not start in time, running mock tests only" | |
| fi | |
| echo "Attempt $i: OpenAEV not ready yet..." | |
| docker logs openaev --tail 20 2>/dev/null || true | |
| sleep 5 | |
| done | |
| - name: Seed test data in OpenAEV | |
| run: | | |
| echo "Creating test data in OpenAEV..." | |
| API_URL="http://localhost:8080/api" | |
| AUTH_HEADER="Authorization: Bearer bfa014e0-e02e-4aa6-a42b-603b19dcf159" | |
| # Check if OpenAEV is available | |
| if ! curl -s -H "$AUTH_HEADER" "$API_URL/settings" 2>/dev/null | grep -q "platform"; then | |
| echo "OpenAEV not available, skipping data seeding" | |
| exit 0 | |
| fi | |
| # Create test Endpoints (agentless) | |
| echo "Creating test Endpoints..." | |
| curl -s -X POST "$API_URL/endpoints/agentless" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "asset_name": "Production Web Server", | |
| "asset_description": "Main production web server", | |
| "endpoint_platform": "Linux", | |
| "endpoint_arch": "x86_64" | |
| }' || true | |
| curl -s -X POST "$API_URL/endpoints/agentless" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "asset_name": "Database Server", | |
| "asset_description": "PostgreSQL database server", | |
| "endpoint_platform": "Linux", | |
| "endpoint_arch": "x86_64", | |
| "endpoint_hostname": "db-server-01", | |
| "endpoint_ips": ["192.168.1.100", "10.0.0.50"] | |
| }' || true | |
| curl -s -X POST "$API_URL/endpoints/agentless" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "asset_name": "Workstation Alpha", | |
| "asset_description": "Developer workstation", | |
| "endpoint_platform": "Windows", | |
| "endpoint_arch": "x86_64", | |
| "endpoint_hostname": "ws-alpha", | |
| "endpoint_mac_addresses": ["00:1A:2B:3C:4D:5E"] | |
| }' || true | |
| # Create test Asset Groups | |
| echo "Creating test Asset Groups..." | |
| curl -s -X POST "$API_URL/asset_groups" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "asset_group_name": "Production Servers", | |
| "asset_group_description": "All production environment servers" | |
| }' || true | |
| curl -s -X POST "$API_URL/asset_groups" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "asset_group_name": "Developer Workstations", | |
| "asset_group_description": "Development team workstations" | |
| }' || true | |
| # Create test Teams | |
| echo "Creating test Teams..." | |
| curl -s -X POST "$API_URL/teams" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "team_name": "Red Team Alpha", | |
| "team_description": "Offensive security team" | |
| }' || true | |
| curl -s -X POST "$API_URL/teams" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "team_name": "Blue Team", | |
| "team_description": "Defensive security team" | |
| }' || true | |
| # Create test Attack Patterns | |
| echo "Creating test Attack Patterns..." | |
| curl -s -X POST "$API_URL/attack_patterns" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "attack_pattern_name": "Phishing", | |
| "attack_pattern_external_id": "T1566", | |
| "attack_pattern_stix_id": "attack-pattern--a62a8db3-f23a-4d8f-afd6-9dbc77e7813b", | |
| "attack_pattern_description": "Phishing attack technique" | |
| }' || true | |
| curl -s -X POST "$API_URL/attack_patterns" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "attack_pattern_name": "Command and Scripting Interpreter", | |
| "attack_pattern_external_id": "T1059", | |
| "attack_pattern_stix_id": "attack-pattern--d1fcf083-a721-4223-aedf-bf8960798d62", | |
| "attack_pattern_description": "Execution via command interpreter" | |
| }' || true | |
| curl -s -X POST "$API_URL/attack_patterns" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "attack_pattern_name": "PowerShell", | |
| "attack_pattern_external_id": "T1059.001", | |
| "attack_pattern_stix_id": "attack-pattern--970a3432-3237-47ad-bcca-7d8cbb217736", | |
| "attack_pattern_description": "Execution via PowerShell" | |
| }' || true | |
| # Create test Scenarios | |
| echo "Creating test Scenarios..." | |
| curl -s -X POST "$API_URL/scenarios" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "scenario_name": "Ransomware Simulation", | |
| "scenario_description": "Simulated ransomware attack scenario" | |
| }' || true | |
| curl -s -X POST "$API_URL/scenarios" \ | |
| -H "$AUTH_HEADER" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "scenario_name": "Lateral Movement Exercise", | |
| "scenario_description": "Testing lateral movement detection" | |
| }' || true | |
| echo "Test data seeding completed!" | |
| - name: Verify test data | |
| run: | | |
| echo "Verifying test data..." | |
| API_URL="http://localhost:8080/api" | |
| AUTH_HEADER="Authorization: Bearer bfa014e0-e02e-4aa6-a42b-603b19dcf159" | |
| # Check if OpenAEV is available | |
| if ! curl -s -H "$AUTH_HEADER" "$API_URL/settings" 2>/dev/null | grep -q "platform"; then | |
| echo "OpenAEV not available, skipping verification" | |
| exit 0 | |
| fi | |
| echo "Querying Endpoints..." | |
| curl -s -H "$AUTH_HEADER" "$API_URL/endpoints" || true | |
| echo "" | |
| echo "Querying Asset Groups..." | |
| curl -s -H "$AUTH_HEADER" "$API_URL/asset_groups" || true | |
| echo "" | |
| echo "Querying Teams..." | |
| curl -s -H "$AUTH_HEADER" "$API_URL/teams" || true | |
| echo "" | |
| echo "Querying Attack Patterns..." | |
| curl -s -H "$AUTH_HEADER" "$API_URL/attack_patterns" || true | |
| - name: Run OpenAEV integration tests with coverage | |
| env: | |
| OPENAEV_URL: http://localhost:8080 | |
| OPENAEV_TOKEN: bfa014e0-e02e-4aa6-a42b-603b19dcf159 | |
| run: npm run test:openaev -- --coverage | |
| - name: Upload OpenAEV integration test coverage | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: coverage-openaev | |
| path: coverage/coverage-final.json | |
| retention-days: 1 | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| docker stop openaev || true | |
| docker rm openaev || true | |
| # E2E tests with Playwright (with coverage collection) | |
| e2e-tests: | |
| name: E2E Tests (Playwright + Coverage) | |
| runs-on: ubuntu-latest | |
| needs: lint | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Cache Playwright browsers | |
| id: playwright-cache | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} | |
| - name: Install Playwright browser and system dependencies | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| run: npx playwright install --with-deps chromium | |
| - name: Install Playwright system dependencies (cache hit) | |
| if: steps.playwright-cache.outputs.cache-hit == 'true' | |
| run: npx playwright install-deps chromium | |
| - name: Install xvfb | |
| run: | | |
| echo "Installing xvfb for headed Chrome..." | |
| sudo apt-get update && sudo apt-get install -y xvfb | |
| echo "xvfb install complete." | |
| - name: Build extension with coverage instrumentation | |
| run: npm run build:coverage | |
| - name: Run Playwright E2E tests | |
| # Use xvfb-run to provide a virtual display for headed Chrome (required for extensions) | |
| run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" npm run test:e2e | |
| - name: Upload Playwright report | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: playwright-report | |
| path: playwright-report/ | |
| retention-days: 7 | |
| - name: Upload Playwright results | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: playwright-results | |
| path: playwright-results.json | |
| retention-days: 7 | |
| - name: Upload E2E coverage | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: coverage-e2e | |
| path: coverage/e2e/ | |
| retention-days: 1 | |
| # Upload combined coverage to Codecov | |
| coverage: | |
| name: Merge & Upload Coverage | |
| runs-on: ubuntu-latest | |
| needs: [lint, unit-tests, integration-opencti, integration-openaev, e2e-tests] | |
| if: always() | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Download unit test coverage | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: coverage-unit | |
| path: coverage/unit | |
| continue-on-error: true | |
| - name: Download OpenCTI coverage | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: coverage-opencti | |
| path: coverage/opencti | |
| continue-on-error: true | |
| - name: Download OpenAEV coverage | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: coverage-openaev | |
| path: coverage/openaev | |
| continue-on-error: true | |
| - name: Download E2E coverage | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: coverage-e2e | |
| path: coverage/e2e | |
| continue-on-error: true | |
| - name: List coverage files | |
| run: | | |
| echo "Coverage files downloaded:" | |
| find coverage -name "*.json" -type f 2>/dev/null || echo "No coverage files found" | |
| - name: Merge coverage reports | |
| run: npm run coverage:merge | |
| continue-on-error: true | |
| - name: Upload coverage reports to Codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| directory: coverage/ | |
| flags: unittests,integration-opencti,integration-openaev,e2e | |
| fail_ci_if_error: false | |
| verbose: true | |