diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 21d793897..1ad368ea0 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,72 +1,280 @@ -# ๐Ÿ› ๏ธ Multi-Environment Workflows โ€“ UTMStack +# ๐Ÿ› ๏ธ GitHub Actions Workflows โ€“ UTMStack -> This repository uses a structured, version-based deployment system (`v10`, `v11`) across multiple environments: `dev`, `qa`, `rc`, and `prod`. -> Each environment is triggered automatically based on branch patterns and workflow logic. +> This repository uses streamlined CI/CD workflows for building and deploying UTMStack v10 and v11 across different environments. -![alt text](workflow.png) +## ๐Ÿ“‹ Table of Contents -## ๐ŸŒฟ Branches Involved - -- `v10`, `v11` โ†’ Main version branches -- `release/v10.x.x`, `release/v11.x.x` โ†’ Feature/bugfix integration branches -- `hotfix/v10.x.x`, `hotfix/v11.x.x` โ†’ For urgent production fixes -- `feature/...` โ†’ Optional; features are typically integrated into `release/*` branches +- [Workflows Overview](#workflows-overview) +- [V10 Deployment Pipeline](#v10-deployment-pipeline) +- [V11 Deployment Pipeline](#v11-deployment-pipeline) +- [Installer Release](#installer-release) +- [Required Secrets and Variables](#required-secrets-and-variables) --- -## โœจ Feature Flow (per version) +## ๐Ÿ”„ Workflows Overview + +### 1. **installer-release.yml** +Automatically builds and publishes installers when a GitHub release is created. + +**Trigger:** Release created (types: `released`) + +**Behavior:** +- Detects version (v10 or v11) from release tag +- Builds installer for the detected version +- Uploads installer binary to the GitHub release -> Best used for planned feature development. +### 2. **v10-deployment-pipeline.yml** +Automated CI/CD pipeline for v10 builds and deployments. -1. Developers work on shared integration branches: - `release/v10.x.x` or `release/v11.x.x` +**Triggers:** +- Push to `v10` branch โ†’ Deploys to **v10-rc** +- Push to `release/v10**` branches โ†’ Deploys to **v10-dev** +- Tags `v10.*` โ†’ Production build -2. On **push**, the `dev` workflow is triggered and deployed to: - `v10-dev` or `v11-dev` +**Environments:** +- `v10-dev` - Development environment (from release branches) +- `v10-rc` - Release candidate environment (from v10 branch) +- Production (from tags) -3. Once stable, a **pull request is opened to `v10` or `v11`**, the `qa` workflow is triggered and deployed to: - `v10-qa` or `v11-qa` +### 3. **v11-deployment-pipeline.yml** +Manual deployment pipeline for v11 with version control. -5. After QA validation, the PR is **merged** into the base branch (`v10` or `v11`). +**Trigger:** Manual (`workflow_dispatch`) -6. This triggers deployment to the **RC environment**: - `v10-rc` or `v11-rc` +**Required Inputs:** +- `version_tag`: Version to deploy (e.g., `v11.0.0-dev.1` or `v11.1.0`) +- `event_processor_tag`: Event processor version (e.g., `1.0.0-beta`) -7. Once RC validation is complete, a **release tag (`v10.x.x` or `v11.x.x`)** is created to deploy to production. +**Version Formats:** +- **Dev:** `v11.x.x-dev.N` (e.g., `v11.0.0-dev.1`) +- **Production:** `v11.x.x` (e.g., `v11.1.0`) --- -## ๐Ÿ”ฅ Hotfix Flow (urgent patches) +## ๐Ÿš€ V10 Deployment Pipeline -> Used for emergency fixes in production. +### Flow -1. Create a branch: - `hotfix/v10.x.x` or `hotfix/v11.x.x` from `v10` or `v11` +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Push to Branch โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”œโ”€โ”€โ”€ release/v10** โ”€โ”€โ†’ Build & Deploy to v10-dev + โ”œโ”€โ”€โ”€ v10 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ Build & Deploy to v10-rc + โ””โ”€โ”€โ”€ tag v10.* โ”€โ”€โ”€โ”€โ†’ Build for Production +``` -2. After development, open a **PR to `v10` or `v11`** +### Jobs -3. On merge, the `rc` workflow is triggered: - `v10-rc` or `v11-rc` +1. **setup_deployment** - Determines environment based on trigger +2. **validations** - Validates user permissions +3. **build_agent** - Builds and signs Windows/Linux agents +4. **build_agent_manager** - Builds agent-manager Docker image +5. **build_*** - Builds all microservices (aws, backend, correlation, frontend, etc.) +6. **all_builds_complete** - Checkpoint for all builds +7. **deploy_dev / deploy_rc** - Deploys to respective environments -4. If the patch is valid, create a **release tag** to deploy to production. +### Permissions + +- **Dev deployments**: `integration-developers` or `core-developers` teams +- **RC/Prod deployments**: Same as dev --- -## โš™๏ธ GitHub Actions Triggers +## ๐ŸŽฏ V11 Deployment Pipeline + +### Flow + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Manual Workflow Dispatch โ”‚ +โ”‚ with version_tag input โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”œโ”€โ”€โ”€ v11.x.x-dev.N โ”€โ”€โ†’ DEV Environment + โ””โ”€โ”€โ”€ v11.x.x โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ PROD Environment +``` + +### Jobs + +1. **validations** - Validates user permissions and version format +2. **build_agent** - Builds and signs Windows/Linux agents +3. **build_utmstack_collector** - Builds UTMStack Collector +4. **build_agent_manager** - Builds agent-manager Docker image +5. **build_event_processor** - Builds event processor with plugins +6. **build_backend** - Builds backend microservice (Java 17) +7. **build_frontend** - Builds frontend microservice +8. **build_user_auditor** - Builds user-auditor microservice +9. **build_web_pdf** - Builds web-pdf microservice +10. **all_builds_complete** - Checkpoint for all builds +11. **publish_new_version** - Publishes version to Customer Manager +12. **schedule** - Schedules release to configured instances + +### Permissions + +- **Dev versions** (`v11.x.x-dev.N`): + - Must run from `release/` or `feature/` branches + - Requires: `administrators`, `integration-developers`, or `core-developers` team membership + +- **Production versions** (`v11.x.x`): + - Requires: `administrators` team membership only + +### Environment Detection + +The pipeline automatically detects the environment based on version format: + +| Version Format | Environment | CM Auth Secret | CM URL | Schedule Instances Var | Schedule Token Secret | +|----------------|-------------|----------------|--------|------------------------|----------------------| +| `v11.x.x-dev.N` | dev | `CM_AUTH_DEV` | `https://cm.dev.utmstack.com` | `SCHEDULE_INSTANCES_DEV` | `CM_SCHEDULE_TOKEN_DEV` | +| `v11.x.x` | prod | `CM_AUTH` | `https://cm.utmstack.com` | `SCHEDULE_INSTANCES_PROD` | `CM_SCHEDULE_TOKEN_PROD` | + +--- + +## ๐Ÿ“ฆ Installer Release + +### Flow + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ GitHub Release โ”‚ +โ”‚ Created & Publishedโ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”œโ”€โ”€โ”€ Tag v10.x.x โ”€โ”€โ†’ Build v10 Installer + โ””โ”€โ”€โ”€ Tag v11.x.x โ”€โ”€โ†’ Build v11 Installer +``` + +### Behavior + +- Validates release tag format +- Builds installer with correct configuration: + - **V10:** Basic build + - **V11:** Build with ldflags (version, branch, encryption keys) +- Uploads installer to GitHub release assets + +--- + +## ๐Ÿ” Required Secrets and Variables + +### Secrets + +| Secret Name | Used In | Description | +|-------------|---------|-------------| +| `API_SECRET` | All | GitHub API token for team membership validation | +| `AGENT_SECRET_PREFIX` | v10, v11 | Agent encryption key | +| `SIGN_CERT` | v10, v11 | Code signing certificate path (var) | +| `SIGN_KEY` | v10, v11 | Code signing key | +| `SIGN_CONTAINER` | v10, v11 | Code signing container name | +| `CM_AUTH` | v11 | Customer Manager auth credentials (prod) | +| `CM_AUTH_DEV` | v11 | Customer Manager auth credentials (dev) | +| `CM_ENCRYPT_SALT` | installer | Encryption salt for installer | +| `CM_SIGN_PUBLIC_KEY` | installer | Public key for installer verification | +| `CM_SCHEDULE_TOKEN_PROD` | v11 | Auth token for cm-version-publisher (prod) | +| `CM_SCHEDULE_TOKEN_DEV` | v11 | Auth token for cm-version-publisher (dev) | +| `GITHUB_TOKEN` | All | Auto-provided by GitHub Actions | -| Environment | Trigger Condition | -|-------------|-------------------| -| `dev` | Push to `release/v10**` or `release/v11**` | -| `qa` | Pull request to `v10` or `v11` from `release/v10**` or `release/v11**`, and approved | -| `rc` | Push to `v10` or `v11` from `hotfix/*` or `release/*` | -| `prod` | Push of a tag starting with `v10.` or `v11.` | +### Variables + +| Variable Name | Used In | Description | Format | +|---------------|---------|-------------|--------| +| `SCHEDULE_INSTANCES_PROD` | v11 | Instance IDs for prod scheduling | Comma-separated UUIDs | +| `SCHEDULE_INSTANCES_DEV` | v11 | Instance IDs for dev scheduling | Comma-separated UUIDs | + +**Example Variable Values:** +``` +SCHEDULE_INSTANCES_PROD=uuid1,uuid2,uuid3 +SCHEDULE_INSTANCES_DEV=uuid-dev1 +``` --- -## ๐Ÿš€ Releasing to Production +## ๐ŸŽฎ How to Deploy -A production deployment is triggered only by pushing a version tag: +### V10 Deployment + +**Dev Environment:** +```bash +git checkout release/v10.x.x +git push origin release/v10.x.x +# Automatically deploys to v10-dev +``` +**RC Environment:** +```bash +git checkout v10 +git merge release/v10.x.x +git push origin v10 +# Automatically deploys to v10-rc +``` + +**Production Release:** ```bash git tag v10.5.0 -git push origin v10.5.0 \ No newline at end of file +git push origin v10.5.0 +# Builds production artifacts +``` + +### V11 Deployment + +**Dev Environment:** +1. Navigate to Actions tab +2. Select "v11 - Build & Deploy Pipeline" +3. Click "Run workflow" +4. Fill in: + - **version_tag:** `v11.0.0-dev.1` + - **event_processor_tag:** `1.0.0-beta` +5. Click "Run workflow" + +**Production Release:** +1. Navigate to Actions tab +2. Select "v11 - Build & Deploy Pipeline" +3. Click "Run workflow" +4. Fill in: + - **version_tag:** `v11.1.0` + - **event_processor_tag:** `1.0.0` +5. Click "Run workflow" + +--- + +## ๐Ÿ—๏ธ Reusable Workflows + +The following reusable workflows are called by the main pipelines: + +- `reusable-basic.yml` - Basic Docker builds +- `reusable-golang.yml` - Golang microservice builds +- `reusable-java.yml` - Java microservice builds +- `reusable-node.yml` - Node.js/Frontend builds + +--- + +## ๐Ÿ“ Notes + +- All Docker images are pushed to `ghcr.io/utmstack/utmstack/*` +- V11 uses `-community` suffix for all image tags +- Agent signing requires `utmstack-signer` runner +- Artifacts (agents, collector) have 1-day retention +- Failed deployments will stop the pipeline and report errors + +--- + +## ๐Ÿ†˜ Troubleshooting + +**Permission Denied:** +- Verify you're a member of the required team +- For v11 prod: Must be in `administrators` team +- For v11 dev: Can be in `administrators`, `integration-developers`, or `core-developers` + +**Build Failures:** +- Check that all required secrets are configured +- Verify runner availability (especially `utmstack-signer` for agent builds) +- Review build logs for specific errors + +**Version Format Errors:** +- Dev: Must match `v11.x.x-dev.N` (e.g., `v11.0.0-dev.1`) +- Prod: Must match `v11.x.x` (e.g., `v11.1.0`) + +--- + +**For questions or issues, please contact the DevOps team.** diff --git a/.github/workflows/installer-release.yml b/.github/workflows/installer-release.yml new file mode 100644 index 000000000..8101cb808 --- /dev/null +++ b/.github/workflows/installer-release.yml @@ -0,0 +1,114 @@ +name: Installer Release + +on: + release: + types: ['released'] + +jobs: + setup_deployment: + name: Validate & Setup + runs-on: ubuntu-24.04 + outputs: + version_major: ${{ steps.validate.outputs.version_major }} + version: ${{ steps.validate.outputs.version }} + steps: + - name: Validate Release Version + id: validate + run: | + VERSION="${{ github.event.release.tag_name }}" + echo "Validating release: $VERSION" + + if [[ "$VERSION" =~ ^v10\.[0-9]+\.[0-9]+ ]]; then + echo "โœ… V10 release detected: $VERSION" + echo "version_major=v10" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + + elif [[ "$VERSION" =~ ^v11\.[0-9]+\.[0-9]+ ]]; then + echo "โœ… V11 release detected: $VERSION" + echo "version_major=v11" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + + else + echo "โญ๏ธ Skipping: Not a v10 or v11 release (got $VERSION)" + fi + + build_v10: + name: Build V10 Installer + runs-on: ubuntu-24.04 + needs: setup_deployment + if: needs.setup_deployment.outputs.version_major == 'v10' + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ^1.20 + id: go + + - name: Build + working-directory: ./installer + env: + GOOS: linux + GOARCH: amd64 + run: go build -o installer -v . + + - name: Upload Release Assets + uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + body_path: ./CHANGELOG.md + draft: false + prerelease: false + files: | + ./installer/installer + + build_v11: + name: Build V11 Installer + runs-on: ubuntu-24.04 + needs: setup_deployment + if: needs.setup_deployment.outputs.version_major == 'v11' + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ^1.20 + id: go + + - name: Configure git for private modules + run: | + git config --global url."https://${{ secrets.API_SECRET }}:x-oauth-basic@github.com/".insteadOf "https://github.com/" + echo "GOPRIVATE=github.com/utmstack" >> $GITHUB_ENV + echo "GONOPROXY=github.com/utmstack" >> $GITHUB_ENV + echo "GONOSUMDB=github.com/utmstack" >> $GITHUB_ENV + + - name: Build + working-directory: ./installer + env: + GOOS: linux + GOARCH: amd64 + GOPRIVATE: github.com/utmstack + GONOPROXY: github.com/utmstack + GONOSUMDB: github.com/utmstack + run: | + echo "Building V11 Installer for production release" + go build -o installer -v -ldflags "\ + -X 'github.com/utmstack/UTMStack/installer/config.DEFAULT_BRANCH=prod' \ + -X 'github.com/utmstack/UTMStack/installer/config.INSTALLER_VERSION=${{ needs.setup_deployment.outputs.version }}' \ + -X 'github.com/utmstack/UTMStack/installer/config.REPLACE=${{ secrets.CM_ENCRYPT_SALT }}' \ + -X 'github.com/utmstack/UTMStack/installer/config.PUBLIC_KEY=${{ secrets.CM_SIGN_PUBLIC_KEY }}'" . + + - name: Upload Release Assets + uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + body_path: ./CHANGELOG.md + draft: false + files: | + ./installer/installer diff --git a/.github/workflows/v10-used-docker-basic.yml b/.github/workflows/reusable-basic.yml similarity index 91% rename from .github/workflows/v10-used-docker-basic.yml rename to .github/workflows/reusable-basic.yml index 0bc45de74..a1efc6a51 100644 --- a/.github/workflows/v10-used-docker-basic.yml +++ b/.github/workflows/reusable-basic.yml @@ -1,11 +1,11 @@ -name: Docker Image Basic +name: Build & Push Basic Docker Image on: workflow_call: inputs: image_name: required: true type: string - environment: + tag: required: true type: string @@ -28,4 +28,4 @@ jobs: with: context: ./${{inputs.image_name}} push: true - tags: ghcr.io/utmstack/utmstack/${{inputs.image_name}}:${{inputs.environment}} \ No newline at end of file + tags: ghcr.io/utmstack/utmstack/${{inputs.image_name}}:${{inputs.tag}} \ No newline at end of file diff --git a/.github/workflows/v11-used-docker-golang.yml b/.github/workflows/reusable-golang.yml similarity index 97% rename from .github/workflows/v11-used-docker-golang.yml rename to .github/workflows/reusable-golang.yml index f5e632a3e..51986706b 100644 --- a/.github/workflows/v11-used-docker-golang.yml +++ b/.github/workflows/reusable-golang.yml @@ -1,4 +1,4 @@ -name: Docker Image Golang +name: Build & Push Golang Docker Image on: workflow_call: diff --git a/.github/workflows/reusable-java.yml b/.github/workflows/reusable-java.yml new file mode 100644 index 000000000..34b49a1fd --- /dev/null +++ b/.github/workflows/reusable-java.yml @@ -0,0 +1,107 @@ +name: "Build & Push Java Docker Image" + +on: + workflow_call: + inputs: + image_name: + required: true + type: string + description: "Name of the Docker image to build" + tag: + required: true + type: string + description: "Docker image tag" + java_version: + required: false + type: string + default: "11" + description: "Java version (11, 17, 21, etc.)" + use_version_file: + required: false + type: boolean + default: false + description: "Read version from version.yml file" + use_tag_as_version: + required: false + type: boolean + default: false + description: "Use tag as Maven version (-Drevision)" + maven_profile: + required: false + type: string + default: "" + description: "Maven profile to use (e.g., 'prod')" + maven_goals: + required: false + type: string + default: "clean install" + description: "Maven goals to execute" + +jobs: + build: + name: Build & Push Java ${{ inputs.java_version }} Image + runs-on: ubuntu-24.04 + steps: + - name: Check out code into the right branch + uses: actions/checkout@v4 + + - name: Set up JDK ${{ inputs.java_version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java_version }} + distribution: "temurin" + + - name: Read version from config + if: ${{ inputs.use_version_file }} + id: read_version + uses: CumulusDS/get-yaml-paths-action@v1.0.2 + with: + file: version.yml + version: version + + - name: Build with Maven + working-directory: ./${{ inputs.image_name }} + run: | + echo "Building with maven" + + # Build Maven command dynamically + MVN_CMD="mvn -B" + + # Add revision based on version source + if [[ "${{ inputs.use_version_file }}" == "true" ]]; then + echo "The configured version is: ${{ steps.read_version.outputs.version }}" + MVN_CMD="$MVN_CMD -Drevision=${{ steps.read_version.outputs.version }}" + elif [[ "${{ inputs.use_tag_as_version }}" == "true" ]]; then + echo "The configured version is: ${{ inputs.tag }}" + MVN_CMD="$MVN_CMD -Drevision=${{ inputs.tag }}" + fi + + # Add profile if specified + if [[ -n "${{ inputs.maven_profile }}" ]]; then + MVN_CMD="$MVN_CMD -P${{ inputs.maven_profile }}" + fi + + # Add goals + MVN_CMD="$MVN_CMD ${{ inputs.maven_goals }}" + + # Add settings file + MVN_CMD="$MVN_CMD -s settings.xml" + + echo "Executing: $MVN_CMD" + $MVN_CMD + env: + MAVEN_TK: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: utmstack + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push the Image + uses: docker/build-push-action@v6 + with: + context: ./${{ inputs.image_name }} + push: true + tags: ghcr.io/utmstack/utmstack/${{ inputs.image_name }}:${{ inputs.tag }} diff --git a/.github/workflows/v10-v11-used-docker-frontend.yml b/.github/workflows/reusable-node.yml similarity index 81% rename from .github/workflows/v10-v11-used-docker-frontend.yml rename to .github/workflows/reusable-node.yml index e4cd815dc..2d02bf952 100644 --- a/.github/workflows/v10-v11-used-docker-frontend.yml +++ b/.github/workflows/reusable-node.yml @@ -1,4 +1,4 @@ -name: Create and Publish Frontend Docker Image +name: Build & Push Node Docker Image on: workflow_call: @@ -6,7 +6,7 @@ on: image_name: required: true type: string - environment: + tag: required: true type: string @@ -17,11 +17,11 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 #this installs node and npm for us + - uses: actions/setup-node@v4 with: node-version: '14.16.1' - - uses: actions/cache@v4 # this allows for re-using node_modules caching, making builds a bit faster. + - uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -50,4 +50,4 @@ jobs: with: context: /home/runner/work/UTMStack/UTMStack/${{inputs.image_name}}/ push: true - tags: ghcr.io/utmstack/utmstack/${{inputs.image_name}}:${{inputs.environment}} \ No newline at end of file + tags: ghcr.io/utmstack/utmstack/${{inputs.image_name}}:${{inputs.tag}} \ No newline at end of file diff --git a/.github/workflows/v10-deployment-pipeline.yml b/.github/workflows/v10-deployment-pipeline.yml new file mode 100644 index 000000000..3340d6429 --- /dev/null +++ b/.github/workflows/v10-deployment-pipeline.yml @@ -0,0 +1,349 @@ +name: V10 - Build & Deploy Pipeline + +on: + push: + branches: [ 'v10', 'release/v10**' ] + tags: [ 'v10.*' ] + pull_request: + branches: [ 'v10' ] + +jobs: + setup_deployment: + name: Setup Deployment + runs-on: ubuntu-24.04 + outputs: + tag: ${{ steps.set-env.outputs.tag }} + steps: + - name: Determine Build Environment + id: set-env + run: | + if ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/v10') }}; then + echo "DEV environment" + echo "tag=v10-dev" >> $GITHUB_OUTPUT + elif ${{ github.event_name == 'push' && github.ref == 'refs/heads/v10' }}; then + echo "RC environment" + echo "tag=v10-rc" >> $GITHUB_OUTPUT + elif ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v10.') }}; then + echo "RELEASE environment" + echo "tag=v10" >> $GITHUB_OUTPUT + fi + + validations: + name: Validate permissions + runs-on: ubuntu-24.04 + needs: setup_deployment + if: ${{ needs.setup_deployment.outputs.tag != '' }} + steps: + - name: Check permissions + run: | + echo "Validating user permissions..." + + RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ + -H "Accept: application/vnd.github.json" \ + "https://api.github.com/orgs/utmstack/teams/integration-developers/memberships/${{ github.actor }}") + + if echo "$RESPONSE" | grep -q '"state": "active"'; then + echo "โœ… User ${{ github.actor }} is a member of the integration-developers team." + else + RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ + -H "Accept: application/vnd.github.json" \ + "https://api.github.com/orgs/utmstack/teams/core-developers/memberships/${{ github.actor }}") + + if echo "$RESPONSE" | grep -q '"state": "active"'; then + echo "โœ… User ${{ github.actor }} is a member of the core-developers team." + else + echo "โ›” ERROR: User ${{ github.actor }} is not a member of the core-developers or integration-developers team." + echo $RESPONSE + exit 1 + fi + fi + + build_agent: + name: Build and Sign Agent + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + runs-on: utmstack-signer + steps: + - name: Check out code into the right branch + uses: actions/checkout@v4 + + - name: Build Linux Agent + env: + GOOS: linux + GOARCH: amd64 + run: | + cd ${{ github.workspace }}/agent + go build -o utmstack_agent_service -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . + + - name: Build Windows Agent (amd64) + env: + GOOS: windows + GOARCH: amd64 + run: | + cd ${{ github.workspace }}/agent + go build -o utmstack_agent_service.exe -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . + + - name: Build Windows Agent (arm64) + env: + GOOS: windows + GOARCH: arm64 + run: | + cd ${{ github.workspace }}/agent + go build -o utmstack_agent_service_arm64.exe -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . + + - name: Sign Windows Agents + run: | + cd ${{ github.workspace }}/agent + signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /f "${{ vars.SIGN_CERT }}" /csp "eToken Base Cryptographic Provider" /k "[{{${{ secrets.SIGN_KEY }}}}]=${{ secrets.SIGN_CONTAINER }}" "utmstack_agent_service.exe" + signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /f "${{ vars.SIGN_CERT }}" /csp "eToken Base Cryptographic Provider" /k "[{{${{ secrets.SIGN_KEY }}}}]=${{ secrets.SIGN_CONTAINER }}" "utmstack_agent_service_arm64.exe" + + - name: Upload signed binaries as artifacts + uses: actions/upload-artifact@v4 + with: + name: signed-agents + path: | + ${{ github.workspace }}/agent/utmstack_agent_service + ${{ github.workspace }}/agent/utmstack_agent_service.exe + ${{ github.workspace }}/agent/utmstack_agent_service_arm64.exe + retention-days: 1 + + build_agent_manager: + name: Build Agent-Manager Image + needs: [build_agent,validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + runs-on: ubuntu-22.04 + steps: + - name: Check out code into the right branch + uses: actions/checkout@v4 + + - name: Download signed binaries from artifacts + uses: actions/download-artifact@v4 + with: + name: signed-agents + path: ${{ github.workspace }}/agent + + - name: Prepare dependencies for Agent Manager Image + run: | + cd ${{ github.workspace }}/agent-manager + GOOS=linux GOARCH=amd64 go build -o agent-manager -v . + + mkdir -p ./dependencies/collector + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/collector/linux-as400-collector.zip" -o ./dependencies/collector/linux-as400-collector.zip + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/collector/windows-as400-collector.zip" -o ./dependencies/collector/windows-as400-collector.zip + + mkdir -p ./dependencies/agent/ + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/agent/utmstack_agent_dependencies_linux.zip" -o ./dependencies/agent/utmstack_agent_dependencies_linux.zip + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/agent/utmstack_agent_dependencies_windows.zip" -o ./dependencies/agent/utmstack_agent_dependencies_windows.zip + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/agent/utmstack_agent_dependencies_windows_arm64.zip" -o ./dependencies/agent/utmstack_agent_dependencies_windows_arm64.zip + + cp "${{ github.workspace }}/agent/utmstack_agent_service" ./dependencies/agent/ + cp "${{ github.workspace }}/agent/utmstack_agent_service.exe" ./dependencies/agent/ + cp "${{ github.workspace }}/agent/utmstack_agent_service_arm64.exe" ./dependencies/agent/ + cp "${{ github.workspace }}/agent/version.json" ./dependencies/agent/ + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: utmstack + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push the Agent Manager Image + uses: docker/build-push-action@v6 + with: + context: ./agent-manager + push: true + tags: ghcr.io/utmstack/utmstack/agent-manager:${{ needs.setup_deployment.outputs.tag }} + + build_aws: + name: Build AWS Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-golang.yml + with: + image_name: aws + tag: ${{ needs.setup_deployment.outputs.tag }} + + build_backend: + name: Build Backend Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-java.yml + with: + image_name: backend + tag: ${{ needs.setup_deployment.outputs.tag }} + java_version: '11' + use_version_file: true + maven_profile: 'prod' + maven_goals: 'clean package' + + build_correlation: + name: Build Correlation Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-golang.yml + with: + image_name: correlation + tag: ${{ needs.setup_deployment.outputs.tag }} + + build_frontend: + name: Build Frontend Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-node.yml + with: + image_name: frontend + tag: ${{ needs.setup_deployment.outputs.tag }} + + build_bitdefender: + name: Build Bitdefender Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-golang.yml + with: + image_name: bitdefender + tag: ${{ needs.setup_deployment.outputs.tag }} + + build_mutate: + name: Build Mutate Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-basic.yml + with: + image_name: mutate + tag: ${{ needs.setup_deployment.outputs.tag }} + + build_office365: + name: Build Office365 Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-golang.yml + with: + image_name: office365 + tag: ${{ needs.setup_deployment.outputs.tag }} + + build_log_auth_proxy: + name: Build Log-Auth-Proxy Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-golang.yml + with: + image_name: log-auth-proxy + tag: ${{ needs.setup_deployment.outputs.tag }} + + build_soc_ai: + name: Build Soc-AI Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-golang.yml + with: + image_name: soc-ai + tag: ${{ needs.setup_deployment.outputs.tag }} + + build_sophos: + name: Build Sophos Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-golang.yml + with: + image_name: sophos + tag: ${{ needs.setup_deployment.outputs.tag }} + + build_user_auditor: + name: Build User-Auditor Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-java.yml + with: + image_name: user-auditor + tag: ${{ needs.setup_deployment.outputs.tag }} + java_version: '11' + use_version_file: false + maven_goals: 'clean install -U' + + build_web_pdf: + name: Build Web-PDF Microservice + needs: [validations,setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + uses: ./.github/workflows/reusable-java.yml + with: + image_name: web-pdf + tag: ${{ needs.setup_deployment.outputs.tag }} + java_version: '11' + use_version_file: false + maven_goals: 'clean install -U' + + all_builds_complete: + name: All Builds Complete + needs: [ + setup_deployment, + build_agent_manager, + build_aws, build_backend, build_correlation, build_frontend, + build_bitdefender, build_mutate, build_office365, + build_log_auth_proxy, build_soc_ai, build_sophos, + build_user_auditor, build_web_pdf + ] + if: ${{ needs.setup_deployment.outputs.tag != '' }} + runs-on: ubuntu-24.04 + steps: + - run: echo "โœ… All builds completed successfully" + + deploy_dev: + name: Deploy to v10-dev environment + needs: [all_builds_complete, setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag == 'v10-dev' }} + runs-on: utmstack-v10-dev + steps: + - name: Check out code into the right branch + uses: actions/checkout@v4 + + - name: Set up Go 1.x + uses: actions/setup-go@v5 + with: + go-version: ^1.20 + id: go + + - name: Build + working-directory: ./installer + env: + GOOS: linux + GOARCH: amd64 + run: | + go build -o installer -v . + mv installer /home/utmstack/installer + chmod +x /home/utmstack/installer + + - name: Run + working-directory: /home/utmstack + run: | + sudo ./installer + + deploy_rc: + name: Deploy to v10-rc environment + needs: [all_builds_complete, setup_deployment] + if: ${{ needs.setup_deployment.outputs.tag == 'v10-rc' }} + runs-on: utmstack-v10-rc + steps: + - name: Check out code into the right branch + uses: actions/checkout@v4 + + - name: Set up Go 1.x + uses: actions/setup-go@v5 + with: + go-version: ^1.20 + id: go + + - name: Build + working-directory: ./installer + env: + GOOS: linux + GOARCH: amd64 + run: | + go build -o installer -v . + mv installer /home/utmstack/installer + chmod +x /home/utmstack/installer + + - name: Run + working-directory: /home/utmstack + run: | + sudo ./installer diff --git a/.github/workflows/v10-principal-installer-release.yml b/.github/workflows/v10-principal-installer-release.yml deleted file mode 100644 index c8e0001e0..000000000 --- a/.github/workflows/v10-principal-installer-release.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Installer Release v10 - -on: - release: - types: [ 'released' ] - -jobs: - build: - name: Build - runs-on: ubuntu-24.04 - steps: - - name: Check out code into the right branch - uses: actions/checkout@v4 - - - name: Set up Go 1.x - uses: actions/setup-go@v5 - with: - go-version: ^1.20 - id: go - - - name: Get dependencies - working-directory: ./installer - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - - - name: Test - working-directory: ./installer - run: go test -v . - - - name: Build - working-directory: ./installer - env: - GOOS: linux - GOARCH: amd64 - run: go build -o installer -v . - - - name: Create Release - id: create_release - uses: softprops/action-gh-release@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - body_path: ./CHANGELOG.md - draft: false - prerelease: false - files: | - ./installer/installer diff --git a/.github/workflows/v10-principal-multi-env.yml b/.github/workflows/v10-principal-multi-env.yml deleted file mode 100644 index 506095ff6..000000000 --- a/.github/workflows/v10-principal-multi-env.yml +++ /dev/null @@ -1,240 +0,0 @@ -name: Multi Environment Build - -on: - push: - branches: [ 'v10', 'release/v10**' ] - tags: [ 'v10.*' ] - pull_request: - branches: [ 'v10' ] - -jobs: - setup_deployment: - name: Setup Deployment - runs-on: ubuntu-24.04 - outputs: - tag: ${{ steps.set-env.outputs.tag }} - steps: - - name: Determine Build Environment - id: set-env - run: | - if ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release/v10') }}; then - echo "DEV environment" - echo "tag=v10-dev" >> $GITHUB_OUTPUT - elif ${{ github.event_name == 'push' && github.ref == 'refs/heads/v10' }}; then - echo "RC environment" - echo "tag=v10-rc" >> $GITHUB_OUTPUT - elif ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v10.') }}; then - echo "RELEASE environment" - echo "tag=v10" >> $GITHUB_OUTPUT - fi - - validations: - name: Validate permissions - runs-on: ubuntu-24.04 - needs: setup_deployment - if: ${{ needs.setup_deployment.outputs.tag != '' }} - steps: - - name: Check permissions - run: | - echo "Validating user permissions..." - - RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ - -H "Accept: application/vnd.github.json" \ - "https://api.github.com/orgs/utmstack/teams/integration-developers/memberships/${{ github.actor }}") - - if echo "$RESPONSE" | grep -q '"state": "active"'; then - echo "โœ… User ${{ github.actor }} is a member of the integration-developers team." - else - RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ - -H "Accept: application/vnd.github.json" \ - "https://api.github.com/orgs/utmstack/teams/core-developers/memberships/${{ github.actor }}") - - if echo "$RESPONSE" | grep -q '"state": "active"'; then - echo "โœ… User ${{ github.actor }} is a member of the core-developers team." - else - echo "โ›” ERROR: User ${{ github.actor }} is not a member of the core-developers or integration-developers team." - echo $RESPONSE - exit 1 - fi - fi - - build_agent: - name: Build Agent-Manager Image & Agent & Dependencies - needs: [validations,setup_deployment] - if: ${{ needs.setup_deployment.outputs.tag != '' }} - runs-on: ubuntu-22.04 - steps: - - name: Check out code into the right branch - uses: actions/checkout@v4 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: utmstack - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and Sign Agent - run: | - echo "Building Agent..." - cd ${{ github.workspace }}/agent - - GOOS=linux GOARCH=amd64 go build -o utmstack_agent_service -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . - GOOS=windows GOARCH=amd64 go build -o utmstack_agent_service.exe -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . - GOOS=windows GOARCH=arm64 go build -o utmstack_agent_service_arm64.exe -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . - - if [[ {{ needs.setup_deployment.outputs.tag }} != "v10-dev" ]]; then - echo "Signing Windows Agent..." - FILES_TO_SIGN=("utmstack_agent_service.exe" "utmstack_agent_service_arm64.exe") - for file in "${FILES_TO_SIGN[@]}"; do - echo "Uploading $file for signing..." - RESPONSE=$(curl -sS -f -X POST http://customermanager.utmstack.com:8081/api/v1/upload \ - -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - -F "file=@$file") - - FILE_ID=$(echo "$RESPONSE" | jq -r '.file_id') - - if [[ -z "$FILE_ID" || "$FILE_ID" == "null" ]]; then - echo "โŒ Failed to upload $file for signing." - exit 1 - fi - - echo "Uploaded $file with file_id: $FILE_ID" - echo "Waiting for signing to complete..." - - while true; do - STATUS=$(curl -sS -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - http://customermanager.utmstack.com:8081/api/v1/status/$FILE_ID | jq -r '.status') - - if [[ "$STATUS" == "ready" ]]; then - echo "โœ… $file has been signed." - break - elif [[ "$STATUS" == "signing" ]]; then - echo "โณ Still signing $file... waiting 5s" - sleep 5 - else - echo "โŒ Unexpected status: $STATUS" - exit 1 - fi - done - - echo "Downloading signed $file..." - curl -sS -f -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - -o "$file" \ - http://customermanager.utmstack.com:8081/api/v1/download/$FILE_ID - - echo "Marking $file as finished..." - curl -sS -X POST -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - http://customermanager.utmstack.com:8081/api/v1/finish/$FILE_ID > /dev/null || true - done - - echo "โœ… All agents signed successfully." - else - echo "Skipping signing for Dev environment." - fi - - - name: Prepare dependencies for Agent Manager Image - run: | - cd ${{ github.workspace }}/agent-manager - GOOS=linux GOARCH=amd64 go build -o agent-manager -v . - - mkdir -p ./dependencies/collector - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/${{ needs.setup_deployment.outputs.tag }}/collector/linux-as400-collector.zip" -o ./dependencies/collector/linux-as400-collector.zip - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/${{ needs.setup_deployment.outputs.tag }}/collector/windows-as400-collector.zip" -o ./dependencies/collector/windows-as400-collector.zip - - mkdir -p ./dependencies/agent/ - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/${{ needs.setup_deployment.outputs.tag }}/agent/utmstack_agent_dependencies_linux.zip" -o ./dependencies/agent/utmstack_agent_dependencies_linux.zip - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/${{ needs.setup_deployment.outputs.tag }}/agent/utmstack_agent_dependencies_windows.zip" -o ./dependencies/agent/utmstack_agent_dependencies_windows.zip - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/${{ needs.setup_deployment.outputs.tag }}/agent/utmstack_agent_dependencies_windows_arm64.zip" -o ./dependencies/agent/utmstack_agent_dependencies_windows_arm64.zip - - cp "${{ github.workspace }}/agent/utmstack_agent_service" ./dependencies/agent/ - # cp "${{ github.workspace }}/agent/utmstack_agent_service_arm64" ./dependencies/agent/ - cp "${{ github.workspace }}/agent/utmstack_agent_service.exe" ./dependencies/agent/ - cp "${{ github.workspace }}/agent/utmstack_agent_service_arm64.exe" ./dependencies/agent/ - cp "${{ github.workspace }}/agent/version.json" ./dependencies/agent/ - - - name: Build and Push the Agent Manager Image - uses: docker/build-push-action@v6 - with: - context: ./agent-manager - push: true - tags: ghcr.io/utmstack/utmstack/agent-manager:${{ needs.setup_deployment.outputs.tag }} - - runner_release: - name: Images deployment - needs: [validations,setup_deployment] - if: ${{ needs.setup_deployment.outputs.tag != '' }} - strategy: - fail-fast: false - matrix: - service: ['aws', 'backend', 'correlation', 'frontend', 'bitdefender', 'mutate', 'office365', 'log-auth-proxy', 'soc-ai', 'sophos', 'user-auditor', 'web-pdf'] - uses: ./.github/workflows/v10-used-runner.yml - with: - microservice: ${{ matrix.service }} - environment: ${{ needs.setup_deployment.outputs.tag }} - secrets: inherit - - deploy_dev: - name: Deploy to v10-dev environment - needs: [build_agent, runner_release, setup_deployment] - if: ${{ needs.setup_deployment.outputs.tag == 'v10-dev' }} - runs-on: utmstack-v10-dev - steps: - - name: Check out code into the right branch - uses: actions/checkout@v4 - - - name: Set up Go 1.x - uses: actions/setup-go@v5 - with: - go-version: ^1.20 - id: go - - - name: Build - working-directory: ./installer - env: - GOOS: linux - GOARCH: amd64 - run: | - go build -o installer -v . - mv installer /home/utmstack/installer - chmod +x /home/utmstack/installer - - - name: Run - working-directory: /home/utmstack - run: | - sudo ./installer - - deploy_rc: - name: Deploy to v10-rc environment - needs: [build_agent, runner_release, setup_deployment] - if: ${{ needs.setup_deployment.outputs.tag == 'v10-rc' }} - runs-on: utmstack-v10-rc - steps: - - name: Check out code into the right branch - uses: actions/checkout@v4 - - - name: Set up Go 1.x - uses: actions/setup-go@v5 - with: - go-version: ^1.20 - id: go - - - name: Build - working-directory: ./installer - env: - GOOS: linux - GOARCH: amd64 - run: | - go build -o installer -v . - mv installer /home/utmstack/installer - chmod +x /home/utmstack/installer - - - name: Run - working-directory: /home/utmstack - run: | - sudo ./installer diff --git a/.github/workflows/v10-used-docker-golang.yml b/.github/workflows/v10-used-docker-golang.yml deleted file mode 100644 index 33ec599a6..000000000 --- a/.github/workflows/v10-used-docker-golang.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Docker Image Golang - -on: - workflow_call: - inputs: - image_name: - required: true - type: string - environment: - required: true - type: string - workflow_dispatch: - inputs: - image_name: - required: true - type: string - environment: - required: true - type: string -jobs: - build: - name: Build - runs-on: ubuntu-24.04 - steps: - - name: Check out code into the right branch - uses: actions/checkout@v4 - - - name: Set up Go 1.x - uses: actions/setup-go@v5 - with: - go-version: ^1.20 - id: go - - - name: Running Tests - working-directory: ./${{inputs.image_name}} - run: go test -v ./... - - - name: Build Binary - working-directory: ./${{inputs.image_name}} - run: go build -o ${{inputs.image_name}} -v . - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: utmstack - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and Push the Image - uses: docker/build-push-action@v6 - with: - context: ./${{inputs.image_name}} - push: true - tags: ghcr.io/utmstack/utmstack/${{inputs.image_name}}:${{inputs.environment}} diff --git a/.github/workflows/v10-used-docker-java-11.yml b/.github/workflows/v10-used-docker-java-11.yml deleted file mode 100644 index ec8357142..000000000 --- a/.github/workflows/v10-used-docker-java-11.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Docker Image Java 11 - -on: - workflow_call: - inputs: - image_name: - required: true - type: string - environment: - required: true - type: string -jobs: - build: - name: Build - runs-on: ubuntu-24.04 - steps: - - name: Check out code into the right branch - uses: actions/checkout@v4 - - - name: Set up JDK 11 - uses: actions/setup-java@v4 - with: - java-version: "11" - distribution: "temurin" - - - name: Read version from config - id: read_version - uses: CumulusDS/get-yaml-paths-action@v1.0.2 - with: - file: version.yml - version: version - - - name: Build with Maven - working-directory: ./${{inputs.image_name}} - run: | - echo "Building with maven" - echo "The configured version is: ${{steps.read_version.outputs.version}}" - mvn -B -Drevision=${{steps.read_version.outputs.version}} -Pprod clean package -s settings.xml - env: - MAVEN_TK: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: utmstack - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and Push the Image - uses: docker/build-push-action@v6 - with: - context: /home/runner/work/UTMStack/UTMStack/${{inputs.image_name}}/ - push: true - tags: ghcr.io/utmstack/utmstack/${{inputs.image_name}}:${{inputs.environment}} \ No newline at end of file diff --git a/.github/workflows/v10-used-runner.yml b/.github/workflows/v10-used-runner.yml deleted file mode 100644 index f9c28d0ea..000000000 --- a/.github/workflows/v10-used-runner.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Runner -on: - workflow_call: - inputs: - environment: - required: true - type: string - microservice: - required: true - type: string - -jobs: - prepare_deployment: - name: Prepare deployment - ${{inputs.microservice}} - runs-on: ubuntu-24.04 - outputs: - tech: ${{ steps.get_tech.outputs.tech }} - steps: - - uses: actions/checkout@v4 - - - name: Determine Tech - id: get_tech - run: | - folder_changed="${{inputs.microservice}}" - if [[ "$folder_changed" == "aws" || "$folder_changed" == "correlation" || "$folder_changed" == "bitdefender" || "$folder_changed" == "office365" || "$folder_changed" == "soc-ai" || "$folder_changed" == "sophos" || "$folder_changed" == "log-auth-proxy" ]]; then - tech="golang" - elif [[ "$folder_changed" == "backend" ]]; then - tech="java-11" - elif [[ "$folder_changed" == "frontend" ]]; then - tech="frontend" - elif [[ "$folder_changed" == "mutate" ]]; then - tech="basic" - elif [[ "$folder_changed" == "user-auditor" || "web-pdf" ]]; then - tech="java" - else - tech="unknown" - fi - echo $tech - echo "::set-output name=tech::$tech" - shell: bash - - basic_deployment: - name: Basic deployment - needs: prepare_deployment - if: ${{ needs.prepare_deployment.outputs.tech == 'basic' }} - uses: ./.github/workflows/v10-used-docker-basic.yml - with: - image_name: ${{ inputs.microservice }} - environment: ${{inputs.environment}} - - frontend_deployment: - name: Frontend deployment - needs: prepare_deployment - if: ${{ needs.prepare_deployment.outputs.tech == 'frontend' }} - uses: ./.github/workflows/v10-v11-used-docker-frontend.yml - with: - image_name: ${{ inputs.microservice }} - environment: ${{inputs.environment}} - - golang_deployment: - name: Golang deployment - needs: prepare_deployment - if: ${{ needs.prepare_deployment.outputs.tech == 'golang' }} - uses: ./.github/workflows/v10-used-docker-golang.yml - with: - image_name: ${{ inputs.microservice }} - environment: ${{inputs.environment}} - - java_11_deployment: - name: Java 11 deployment - needs: prepare_deployment - if: ${{ needs.prepare_deployment.outputs.tech == 'java-11' }} - uses: ./.github/workflows/v10-used-docker-java-11.yml - with: - image_name: ${{ inputs.microservice }} - environment: ${{inputs.environment}} - - java_deployment: - name: Java deployment - needs: prepare_deployment - if: ${{ needs.prepare_deployment.outputs.tech == 'java' }} - uses: ./.github/workflows/v10-v11-used-docker-java.yml - with: - image_name: ${{ inputs.microservice }} - environment: ${{inputs.environment}} diff --git a/.github/workflows/v10-v11-used-docker-java.yml b/.github/workflows/v10-v11-used-docker-java.yml deleted file mode 100644 index d9d47cdc5..000000000 --- a/.github/workflows/v10-v11-used-docker-java.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Docker Image Java -on: - workflow_call: - inputs: - image_name: - required: true - type: string - environment: - required: true - type: string -jobs: - build: - runs-on: ubuntu-24.04 - steps: - - name: Check out code into the right branch - uses: actions/checkout@v4 - - - name: Set up JDK 11 - uses: actions/setup-java@v4 - with: - java-version: "11" - distribution: "temurin" - - - name: Build with Maven - working-directory: ./${{inputs.image_name}} - run: | - echo "Building with maven" - mvn clean install -U -s settings.xml - env: - MAVEN_TK: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: utmstack - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and Push the Image - uses: docker/build-push-action@v6 - with: - context: /home/runner/work/UTMStack/UTMStack/${{inputs.image_name}}/ - push: true - tags: ghcr.io/utmstack/utmstack/${{inputs.image_name}}:${{inputs.environment}} \ No newline at end of file diff --git a/.github/workflows/v11-deployment-pipeline.yml b/.github/workflows/v11-deployment-pipeline.yml new file mode 100644 index 000000000..b5d4fdc36 --- /dev/null +++ b/.github/workflows/v11-deployment-pipeline.yml @@ -0,0 +1,420 @@ +name: "v11 - Build & Deploy Pipeline" + +on: + workflow_dispatch: + inputs: + version_tag: + description: "Version to deploy.(e.g., v11.0.0-dev.1, v11.1.0)" + required: true + event_processor_tag: + description: "Event processor version to use for this deployment.(e.g., 1.0.0-beta)" + required: true + default: "1.0.0-beta" + +jobs: + validations: + name: Validate permissions + runs-on: ubuntu-24.04 + steps: + - name: Check permissions + run: | + echo "Checking permissions..." + + # Check if version is production format (v11.x.x) or dev format (v11.x.x-dev.x, etc.) + if [[ "${{ github.event.inputs.version_tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "๐Ÿ” Detected PRODUCTION version format: ${{ github.event.inputs.version_tag }}" + + # For production versions, only administrators can deploy + echo "Validating user permissions against administrators team..." + RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ + -H "Accept: application/vnd.github.json" \ + "https://api.github.com/orgs/utmstack/teams/administrators/memberships/${{ github.actor }}") + + if echo "$RESPONSE" | grep -q '"state": "active"'; then + echo "โœ… User ${{ github.actor }} is a member of the administrators team." + else + echo "โ›” ERROR: User ${{ github.actor }} is not a member of the administrators team." + echo "Production deployments require administrator permissions." + echo $RESPONSE + exit 1 + fi + + elif [[ "${{ github.event.inputs.version_tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-dev\.[0-9]+$ ]]; then + echo "๐Ÿ” Detected DEV version format: ${{ github.event.inputs.version_tag }}" + + if [[ "${{ github.ref }}" =~ ^refs/heads/(release/|feature/) ]]; then + echo "โœ… Base branch ${{ github.ref }} is valid." + else + echo "โ›” ERROR: Base branch ${{ github.ref }} is not valid. It should be release/ or feature/." + exit 1 + fi + + # For dev versions, check administrators first, then integration-developers, then core-developers + echo "Validating user permissions..." + RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ + -H "Accept: application/vnd.github.json" \ + "https://api.github.com/orgs/utmstack/teams/administrators/memberships/${{ github.actor }}") + + if echo "$RESPONSE" | grep -q '"state": "active"'; then + echo "โœ… User ${{ github.actor }} is a member of the administrators team." + else + RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ + -H "Accept: application/vnd.github.json" \ + "https://api.github.com/orgs/utmstack/teams/integration-developers/memberships/${{ github.actor }}") + + if echo "$RESPONSE" | grep -q '"state": "active"'; then + echo "โœ… User ${{ github.actor }} is a member of the integration-developers team." + else + RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ + -H "Accept: application/vnd.github.json" \ + "https://api.github.com/orgs/utmstack/teams/core-developers/memberships/${{ github.actor }}") + + if echo "$RESPONSE" | grep -q '"state": "active"'; then + echo "โœ… User ${{ github.actor }} is a member of the core-developers team." + else + echo "โ›” ERROR: User ${{ github.actor }} is not a member of administrators, integration-developers, or core-developers teams." + echo $RESPONSE + exit 1 + fi + fi + fi + + else + echo "โ›” Version tag format is incorrect." + echo "Expected formats:" + echo " - Production: vX.Y.Z (e.g., v11.0.0)" + echo " - Development: vX.Y.Z-dev.N (e.g., v11.0.0-dev.1)" + exit 1 + fi + + build_agent: + name: Build and Sign Agent + needs: [validations] + runs-on: utmstack-signer + steps: + - name: Check out code into the right branch + uses: actions/checkout@v4 + + - name: Build Linux Agent + env: + GOOS: linux + GOARCH: amd64 + run: | + cd ${{ github.workspace }}/agent + go build -o utmstack_agent_service -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . + + - name: Build Windows Agent (amd64) + env: + GOOS: windows + GOARCH: amd64 + run: | + cd ${{ github.workspace }}/agent + go build -o utmstack_agent_service.exe -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . + + - name: Build Windows Agent (arm64) + env: + GOOS: windows + GOARCH: arm64 + run: | + cd ${{ github.workspace }}/agent + go build -o utmstack_agent_service_arm64.exe -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . + + - name: Sign Windows Agents + run: | + cd ${{ github.workspace }}/agent + signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /f "${{ vars.SIGN_CERT }}" /csp "eToken Base Cryptographic Provider" /k "[{{${{ secrets.SIGN_KEY }}}}]=${{ secrets.SIGN_CONTAINER }}" "utmstack_agent_service.exe" + signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /f "${{ vars.SIGN_CERT }}" /csp "eToken Base Cryptographic Provider" /k "[{{${{ secrets.SIGN_KEY }}}}]=${{ secrets.SIGN_CONTAINER }}" "utmstack_agent_service_arm64.exe" + + - name: Upload signed binaries as artifacts + uses: actions/upload-artifact@v4 + with: + name: signed-agents + path: | + ${{ github.workspace }}/agent/utmstack_agent_service + ${{ github.workspace }}/agent/utmstack_agent_service.exe + ${{ github.workspace }}/agent/utmstack_agent_service_arm64.exe + retention-days: 1 + + build_utmstack_collector: + name: Build UTMStack Collector + needs: [validations] + runs-on: ubuntu-24.04 + steps: + - name: Check out code into the right branch + uses: actions/checkout@v4 + + - name: Build UTMStack Collector + run: | + echo "Building UTMStack Collector..." + cd ${{ github.workspace }}/utmstack-collector + + GOOS=linux GOARCH=amd64 go build -o utmstack_collector -v -ldflags "-X 'github.com/utmstack/UTMStack/utmstack-collector/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . + + - name: Upload collector binary as artifact + uses: actions/upload-artifact@v4 + with: + name: utmstack-collector + path: ${{ github.workspace }}/utmstack-collector/utmstack_collector + retention-days: 1 + + build_agent_manager: + name: Build Agent Manager Microservice + needs: [validations, build_agent, build_utmstack_collector] + runs-on: ubuntu-24.04 + steps: + - name: Check out code into the right branch + uses: actions/checkout@v4 + + - name: Download signed agents from artifacts + uses: actions/download-artifact@v4 + with: + name: signed-agents + path: ${{ github.workspace }}/agent + + - name: Download UTMStack Collector from artifacts + uses: actions/download-artifact@v4 + with: + name: utmstack-collector + path: ${{ github.workspace }}/utmstack-collector + + - name: Prepare dependencies for Agent Manager Image + run: | + cd ${{ github.workspace }}/agent-manager + GOOS=linux GOARCH=amd64 go build -o agent-manager -v . + + mkdir -p ./dependencies/collector + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/collector/linux-as400-collector.zip" -o ./dependencies/collector/linux-as400-collector.zip + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/collector/windows-as400-collector.zip" -o ./dependencies/collector/windows-as400-collector.zip + + cp "${{ github.workspace }}/utmstack-collector/utmstack_collector" ./dependencies/collector/ + cp "${{ github.workspace }}/utmstack-collector/version.json" ./dependencies/collector/ + + mkdir -p ./dependencies/agent/ + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/agent/utmstack_agent_dependencies_linux.zip" -o ./dependencies/agent/utmstack_agent_dependencies_linux.zip + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/agent/utmstack_agent_dependencies_windows.zip" -o ./dependencies/agent/utmstack_agent_dependencies_windows.zip + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/agent/utmstack_agent_dependencies_windows_arm64.zip" -o ./dependencies/agent/utmstack_agent_dependencies_windows_arm64.zip + + cp "${{ github.workspace }}/agent/utmstack_agent_service" ./dependencies/agent/ + cp "${{ github.workspace }}/agent/utmstack_agent_service.exe" ./dependencies/agent/ + cp "${{ github.workspace }}/agent/utmstack_agent_service_arm64.exe" ./dependencies/agent/ + cp "${{ github.workspace }}/agent/version.json" ./dependencies/agent/ + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: utmstack + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push the Agent Manager Image + uses: docker/build-push-action@v6 + with: + context: ./agent-manager + push: true + tags: ghcr.io/utmstack/utmstack/agent-manager:${{ inputs.version_tag }}-community + + build_event_processor: + name: Build Event Processor Microservice + needs: [validations] + runs-on: ubuntu-24.04 + steps: + - name: Check out code into the right branch + uses: actions/checkout@v4 + + - name: Build Plugins + env: + GOOS: linux + GOARCH: amd64 + run: | + cd ${{ github.workspace }}/plugins/alerts; go build -o com.utmstack.alerts.plugin -v . + cd ${{ github.workspace }}/plugins/aws; go build -o com.utmstack.aws.plugin -v . + cd ${{ github.workspace }}/plugins/azure; go build -o com.utmstack.azure.plugin -v . + cd ${{ github.workspace }}/plugins/bitdefender; go build -o com.utmstack.bitdefender.plugin -v . + cd ${{ github.workspace }}/plugins/config; go build -o com.utmstack.config.plugin -v . + cd ${{ github.workspace }}/plugins/events; go build -o com.utmstack.events.plugin -v . + cd ${{ github.workspace }}/plugins/gcp; go build -o com.utmstack.gcp.plugin -v . + cd ${{ github.workspace }}/plugins/geolocation; go build -o com.utmstack.geolocation.plugin -v . + cd ${{ github.workspace }}/plugins/inputs; go build -o com.utmstack.inputs.plugin -v . + cd ${{ github.workspace }}/plugins/o365; go build -o com.utmstack.o365.plugin -v . + cd ${{ github.workspace }}/plugins/sophos; go build -o com.utmstack.sophos.plugin -v . + cd ${{ github.workspace }}/plugins/stats; go build -o com.utmstack.stats.plugin -v . + cd ${{ github.workspace }}/plugins/soc-ai; go build -o com.utmstack.soc-ai.plugin -v . + cd ${{ github.workspace }}/plugins/modules-config; go build -o com.utmstack.modules-config.plugin -v . + + - name: Prepare Dependencies for Event Processor Image + run: | + mkdir -p ./geolocation + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/geolocation/asn-blocks-v4.csv" -o ./geolocation/asn-blocks-v4.csv + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/geolocation/asn-blocks-v6.csv" -o ./geolocation/asn-blocks-v6.csv + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/geolocation/blocks-v4.csv" -o ./geolocation/blocks-v4.csv + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/geolocation/blocks-v6.csv" -o ./geolocation/blocks-v6.csv + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/geolocation/locations-en.csv" -o ./geolocation/locations-en.csv + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: utmstack + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push the Event Processor Image + uses: docker/build-push-action@v6 + with: + context: . + file: ./event_processor.Dockerfile + push: true + tags: ghcr.io/utmstack/utmstack/eventprocessor:${{ inputs.version_tag }}-community + build-args: | + BASE_IMAGE=ghcr.io/threatwinds/eventprocessor/base:${{ inputs.event_processor_tag }} + + build_backend: + name: Build Backend Microservice + needs: [validations] + uses: ./.github/workflows/reusable-java.yml + with: + image_name: backend + tag: ${{ inputs.version_tag }}-community + java_version: '17' + use_tag_as_version: true + maven_profile: 'prod' + maven_goals: 'clean package' + + build_frontend: + name: Build Frontend Microservice + needs: [validations] + uses: ./.github/workflows/reusable-node.yml + with: + image_name: frontend + tag: ${{ inputs.version_tag }}-community + + build_user_auditor: + name: Build User-Auditor Microservice + needs: [validations] + uses: ./.github/workflows/reusable-java.yml + with: + image_name: user-auditor + tag: ${{ inputs.version_tag }}-community + java_version: '11' + use_version_file: false + maven_goals: 'clean install -U' + + build_web_pdf: + name: Build Web-PDF Microservice + needs: [validations] + uses: ./.github/workflows/reusable-java.yml + with: + image_name: web-pdf + tag: ${{ inputs.version_tag }}-community + java_version: '11' + use_version_file: false + maven_goals: 'clean install -U' + + all_builds_complete: + name: All Builds Complete + needs: [ + build_agent_manager, + build_event_processor, + build_backend, + build_frontend, + build_user_auditor, + build_web_pdf + ] + runs-on: ubuntu-24.04 + steps: + - run: echo "โœ… All builds completed successfully." + + publish_new_version: + name: Publish New Version to Customer Manager + needs: all_builds_complete + runs-on: ubuntu-24.04 + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Publish version + run: | + changelog=$(cat CHANGELOG.md) + + # Determine environment and CM_AUTH based on version format + if [[ "${{ inputs.version_tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "๐Ÿ” Detected PRODUCTION version" + cmAuth=$(echo '${{ secrets.CM_AUTH }}' | jq -r '.') + cm_url="https://cm.utmstack.com/api/v1/versions/register" + elif [[ "${{ inputs.version_tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-dev\.[0-9]+$ ]]; then + echo "๐Ÿ” Detected DEV version" + cmAuth=$(echo '${{ secrets.CM_AUTH_DEV }}' | jq -r '.') + cm_url="https://cm.dev.utmstack.com/api/v1/versions/register" + else + echo "โ›” Version format not recognized" + exit 1 + fi + + id=$(echo "$cmAuth" | jq -r '.id') + key=$(echo "$cmAuth" | jq -r '.key') + + body=$(jq -n \ + --arg version "${{ inputs.version_tag }}" \ + --arg changelog "$changelog" \ + '{version: $version, changelog: $changelog}' + ) + + response=$(curl -s -X POST "$cm_url" \ + -H "Content-Type: application/json" \ + -H "id: $id" \ + -H "key: $key" \ + -d "$body") + + echo "Response: $response" + + schedule: + name: Schedule release to our instances + needs: publish_new_version + runs-on: ubuntu-24.04 + steps: + - name: Run publisher + run: | + # Determine environment, instance IDs, and auth token based on version format + if [[ "${{ inputs.version_tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "๐Ÿ” Detected PRODUCTION version" + environment="prod" + instance_ids="${{ vars.SCHEDULE_INSTANCES_PROD }}" + auth_token="${{ secrets.CM_SCHEDULE_TOKEN_PROD }}" + elif [[ "${{ inputs.version_tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-dev\.[0-9]+$ ]]; then + echo "๐Ÿ” Detected DEV version" + environment="dev" + instance_ids="${{ vars.SCHEDULE_INSTANCES_DEV }}" + auth_token="${{ secrets.CM_SCHEDULE_TOKEN_DEV }}" + else + echo "โ›” Version format not recognized" + exit 1 + fi + + # Download cm-version-publisher + curl -sSL "https://storage.googleapis.com/utmstack-updates/dependencies/cm-version-publisher" -o ./cm-version-publisher + chmod +x ./cm-version-publisher + + # Parse IDs (handle single ID or comma-separated IDs) + IFS=',' read -ra ID_ARRAY <<< "$instance_ids" + + # Iterate over each instance ID + for instance_id in "${ID_ARRAY[@]}"; do + # Trim whitespace + instance_id=$(echo "$instance_id" | xargs) + + echo "๐Ÿ“… Scheduling release for instance: $instance_id" + ./cm-version-publisher \ + --env "$environment" \ + --instance-id "$instance_id" \ + --version "${{ inputs.version_tag }}" \ + --auth-token "$auth_token" + + if [ $? -eq 0 ]; then + echo "โœ… Successfully scheduled for instance: $instance_id" + else + echo "โŒ Failed to schedule for instance: $instance_id" + exit 1 + fi + done + + echo "โœ… Scheduled release for all instances with version ${{ inputs.version_tag }}" + \ No newline at end of file diff --git a/.github/workflows/v11-principal-alpha-deployment.yml b/.github/workflows/v11-principal-alpha-deployment.yml deleted file mode 100644 index b5fe1afbe..000000000 --- a/.github/workflows/v11-principal-alpha-deployment.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: "Alpha Deployment" - -on: - workflow_dispatch: - inputs: - version_tag: - description: "Version to deploy.(e.g., v11.0.0-alpha.1)" - required: true - event_processor_tag: - description: "Event processor version to use for this deployment.(e.g., 1.0.0-beta)" - required: true - -jobs: - validations: - name: Validate permissions - runs-on: ubuntu-24.04 - steps: - - name: Check permissions - run: | - echo "Checking permissions..." - - if [[ "${{ github.event.inputs.version_tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then - echo "โœ… Version tag format is correct." - - if [[ "${{ github.ref }}" =~ ^refs/heads/(release/|feature/) ]]; then - echo "โœ… Base branch ${{ github.ref }} is valid." - else - echo "โ›” ERROR: Base branch ${{ github.ref }} is not valid. It should be release/ or feature/." - exit 1 - fi - - echo "Validating user permissions..." - RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ - -H "Accept: application/vnd.github.json" \ - "https://api.github.com/orgs/utmstack/teams/integration-developers/memberships/${{ github.actor }}") - - if echo "$RESPONSE" | grep -q '"state": "active"'; then - echo "โœ… User ${{ github.actor }} is a member of the integration-developers team." - else - RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ - -H "Accept: application/vnd.github.json" \ - "https://api.github.com/orgs/utmstack/teams/core-developers/memberships/${{ github.actor }}") - - if echo "$RESPONSE" | grep -q '"state": "active"'; then - echo "โœ… User ${{ github.actor }} is a member of the core-developers team." - else - echo "โ›” ERROR: User ${{ github.actor }} is not a member of the core-developers or integration-developers team." - echo $RESPONSE - exit 1 - fi - fi - - else - echo "โ›” Version tag format is incorrect. It should be in the format vX.Y.Z-alpha.N." - exit 1 - fi - - deploy: - name: Deploy to Alpha - needs: validations - uses: ./.github/workflows/v11-used-build.yml - with: - version_tag: ${{ github.event.inputs.version_tag }} - event_processor_tag: ${{ github.event.inputs.event_processor_tag }} - environment: alpha - secrets: - AGENT_SECRET_PREFIX: ${{ secrets.AGENT_SECRET_PREFIX }} - SIGNER_TOKEN: ${{ secrets.SIGNER_TOKEN }} - CM_AUTH: ${{ secrets.CM_AUTH_ALPHA }} - - schedule: - name: Schedule release to alpha - needs: deploy - runs-on: ubuntu-24.04 - steps: - - name: Run publisher - run: | - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/v11-alpha/version-publisher" -o ./version-publisher - - chmod +x ./version-publisher - ./version-publisher '43cb25b3-1426-4c77-8bbe-b2e0b491ce08' '${{ github.event.inputs.version_tag }}' - echo "Scheduled release to alpha for version ${{ github.event.inputs.version_tag }}." - \ No newline at end of file diff --git a/.github/workflows/v11-principal-beta-deployment.yml b/.github/workflows/v11-principal-beta-deployment.yml deleted file mode 100644 index 349b8390a..000000000 --- a/.github/workflows/v11-principal-beta-deployment.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: "Beta Deployment" - -on: - workflow_dispatch: - inputs: - version_tag: - description: "Version to deploy.(e.g., v11.0.0-beta.1)" - required: true - event_processor_tag: - description: "Event processor version to use for this deployment.(e.g., 1.0.0-beta)" - required: true - -jobs: - validations: - name: Validate permissions - runs-on: ubuntu-24.04 - steps: - - name: Check permissions - run: | - echo "Checking permissions..." - - if [[ "${{ github.event.inputs.version_tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$ ]]; then - echo "โœ… Version tag format is correct." - - if [[ "${{ github.ref }}" =~ ^refs/heads/(release/|feature/) ]]; then - echo "โœ… Base branch ${{ github.ref }} is valid." - else - echo "โ›” ERROR: Base branch ${{ github.ref }} is not valid. It should be release/ or feature/." - exit 1 - fi - - echo "Validating user permissions..." - RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ - -H "Accept: application/vnd.github.json" \ - "https://api.github.com/orgs/utmstack/teams/core-developers/memberships/${{ github.actor }}") - - if echo "$RESPONSE" | grep -q '"state": "active"'; then - echo "โœ… User ${{ github.actor }} is a member of the core-developers team." - else - echo "โ›” ERROR: User ${{ github.actor }} is not a member of the core-developers team." - exit 1 - fi - - else - echo "โ›” Version tag format is incorrect. It should be in the format vX.Y.Z-beta.N." - exit 1 - fi - - deploy: - name: Deploy to Beta - needs: validations - uses: ./.github/workflows/v11-used-build.yml - with: - version_tag: ${{ github.event.inputs.version_tag }} - event_processor_tag: ${{ github.event.inputs.event_processor_tag }} - environment: beta - secrets: - AGENT_SECRET_PREFIX: ${{ secrets.AGENT_SECRET_PREFIX }} - SIGNER_TOKEN: ${{ secrets.SIGNER_TOKEN }} - CM_AUTH: ${{ secrets.CM_AUTH_BETA }} - \ No newline at end of file diff --git a/.github/workflows/v11-principal-installer-release.yml b/.github/workflows/v11-principal-installer-release.yml deleted file mode 100644 index 06544578a..000000000 --- a/.github/workflows/v11-principal-installer-release.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: Installer Release V11 - -on: - release: - types: ['released', 'prereleased'] - -jobs: - setup_deployment: - name: Setup Deployment - runs-on: ubuntu-24.04 - outputs: - environment: ${{ steps.set-env.outputs.environment }} - version: ${{ steps.set-env.outputs.version }} - steps: - - name: Validate and Determine Build Environment - id: set-env - run: | - VERSION="${{ github.event.release.tag_name }}" - - # Verify tag starts with v11. - if [[ ! "$VERSION" =~ ^v11\. ]]; then - echo "Skipping: This workflow only processes v11.* releases" - echo "Received tag: $VERSION" - exit 0 - fi - - # Validate version format and determine environment - if [[ "$VERSION" =~ ^v11\.[0-9]+\.[0-9]+$ ]]; then - echo "Production environment detected" - echo "environment=prod" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - - elif [[ "$VERSION" =~ ^v11\.[0-9]+\.[0-9]+-beta\.[0-9]+$ ]]; then - echo "Beta environment detected" - echo "environment=beta" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - - elif [[ "$VERSION" =~ ^v11\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then - echo "RC environment detected" - echo "environment=rc" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - - else - echo "ERROR: Invalid version format. Version must match one of:" - echo " - v11.x.x (production)" - echo " - v11.x.x-beta.x (beta)" - echo " - v11.x.x-rc.x (release candidate)" - exit 1 - fi - - build: - name: Build - runs-on: ubuntu-24.04 - needs: setup_deployment - if: needs.setup_deployment.outputs.environment != '' - steps: - - name: Check out code into the right branch - uses: actions/checkout@v4 - - - name: Set up Go 1.x - uses: actions/setup-go@v5 - with: - go-version: ^1.20 - id: go - - - name: Configure git for private modules - run: | - git config --global url."https://${{ secrets.API_SECRET }}:x-oauth-basic@github.com/".insteadOf "https://github.com/" - echo "GOPRIVATE=github.com/utmstack" >> $GITHUB_ENV - echo "GONOPROXY=github.com/utmstack" >> $GITHUB_ENV - echo "GONOSUMDB=github.com/utmstack" >> $GITHUB_ENV - - - name: Build - working-directory: ./installer - env: - GOOS: linux - GOARCH: amd64 - GOPRIVATE: github.com/utmstack - GONOPROXY: github.com/utmstack - GONOSUMDB: github.com/utmstack - run: | - echo "Building Installer..." - go build -o installer -v -ldflags "\ - -X 'github.com/utmstack/UTMStack/installer/config.DEFAULT_BRANCH=${{ needs.setup_deployment.outputs.environment }}' \ - -X 'github.com/utmstack/UTMStack/installer/config.INSTALLER_VERSION=${{ needs.setup_deployment.outputs.version }}' \ - -X 'github.com/utmstack/UTMStack/installer/config.REPLACE=${{ secrets.CM_ENCRYPT_SALT }}' \ - -X 'github.com/utmstack/UTMStack/installer/config.PUBLIC_KEY=${{ secrets.CM_SIGN_PUBLIC_KEY }}'" . - - - name: Upload Release Assets - uses: softprops/action-gh-release@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - body_path: ./CHANGELOG.md - draft: false - files: | - ./installer/installer diff --git a/.github/workflows/v11-principal-production-deployment.yml b/.github/workflows/v11-principal-production-deployment.yml deleted file mode 100644 index 4404a8ff4..000000000 --- a/.github/workflows/v11-principal-production-deployment.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: "Production Deployment" - -on: - workflow_dispatch: - inputs: - version_tag: - description: "Version to deploy.(e.g., v11.0.0)" - required: true - event_processor_tag: - description: "Event processor version to use for this deployment.(e.g., 1.0.0)" - required: true - -jobs: - validations: - name: Validate permissions - runs-on: ubuntu-24.04 - steps: - - name: Check permissions - run: | - echo "Checking permissions..." - - if [[ "${{ github.event.inputs.version_tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "โœ… Version tag format is correct." - - if [[ "${{ github.ref }}" =~ ^refs/heads/main) ]]; then - echo "โœ… Base branch ${{ github.ref }} is valid." - else - echo "โ›” ERROR: Base branch ${{ github.ref }} is not valid. It should be main." - exit 1 - fi - - echo "Validating user permissions..." - RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ - -H "Accept: application/vnd.github.json" \ - "https://api.github.com/orgs/utmstack/teams/core-developers/memberships/${{ github.actor }}") - - if echo "$RESPONSE" | grep -q '"state": "active"'; then - echo "โœ… User ${{ github.actor }} is a member of the core-developers team." - else - echo "โ›” ERROR: User ${{ github.actor }} is not a member of the core-developers team." - exit 1 - fi - - else - echo "โ›” Version tag format is incorrect. It should be in the format vX.Y.Z." - exit 1 - fi - - deploy: - name: Deploy to Production - needs: validations - uses: ./.github/workflows/v11-used-build.yml - with: - version_tag: ${{ github.event.inputs.version_tag }} - event_processor_tag: ${{ github.event.inputs.event_processor_tag }} - environment: prod - secrets: - AGENT_SECRET_PREFIX: ${{ secrets.AGENT_SECRET_PREFIX }} - SIGNER_TOKEN: ${{ secrets.SIGNER_TOKEN }} - CM_AUTH: ${{ secrets.CM_AUTH_PROD }} - \ No newline at end of file diff --git a/.github/workflows/v11-principal-rc-deployment.yml b/.github/workflows/v11-principal-rc-deployment.yml deleted file mode 100644 index 3eea0a34d..000000000 --- a/.github/workflows/v11-principal-rc-deployment.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: "RC Deployment" - -on: - workflow_dispatch: - inputs: - version_tag: - description: "Version to deploy.(e.g., v11.0.0-rc.1)" - required: true - event_processor_tag: - description: "Event processor version to use for this deployment.(e.g., 1.0.0-beta)" - required: true - -jobs: - validations: - name: Validate permissions - runs-on: ubuntu-24.04 - steps: - - name: Check permissions - run: | - echo "Checking permissions..." - - if [[ "${{ github.event.inputs.version_tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then - echo "โœ… Version tag format is correct." - - if [[ "${{ github.ref }}" =~ ^refs/heads/main) ]]; then - echo "โœ… Base branch ${{ github.ref }} is valid." - else - echo "โ›” ERROR: Base branch ${{ github.ref }} is not valid. It should be main." - exit 1 - fi - - echo "Validating user permissions..." - RESPONSE=$(curl -s -H "Authorization: Bearer ${{ secrets.API_SECRET }}" \ - -H "Accept: application/vnd.github.json" \ - "https://api.github.com/orgs/utmstack/teams/core-developers/memberships/${{ github.actor }}") - - if echo "$RESPONSE" | grep -q '"state": "active"'; then - echo "โœ… User ${{ github.actor }} is a member of the core-developers team." - else - echo "โ›” ERROR: User ${{ github.actor }} is not a member of the core-developers team." - exit 1 - fi - - else - echo "โ›” Version tag format is incorrect. It should be in the format vX.Y.Z-rc.N." - exit 1 - fi - - deploy: - name: Deploy to RC - needs: validations - uses: ./.github/workflows/v11-used-build.yml - with: - version_tag: ${{ github.event.inputs.version_tag }} - event_processor_tag: ${{ github.event.inputs.event_processor_tag }} - environment: rc - secrets: - AGENT_SECRET_PREFIX: ${{ secrets.AGENT_SECRET_PREFIX }} - SIGNER_TOKEN: ${{ secrets.SIGNER_TOKEN }} - CM_AUTH: ${{ secrets.CM_AUTH_RC }} - \ No newline at end of file diff --git a/.github/workflows/v11-used-build.yml b/.github/workflows/v11-used-build.yml deleted file mode 100644 index 71a3708fc..000000000 --- a/.github/workflows/v11-used-build.yml +++ /dev/null @@ -1,201 +0,0 @@ -name: Build & Push Images - -on: - workflow_call: - inputs: - version_tag: - required: true - type: string - event_processor_tag: - required: true - type: string - environment: - required: true - type: string - secrets: - AGENT_SECRET_PREFIX: - required: true - SIGNER_TOKEN: - required: true - CM_AUTH: - required: true - - -jobs: - build_images: - name: Build Docker Images without dependencies - strategy: - fail-fast: false - matrix: - service: ['backend', 'frontend', 'user-auditor', 'web-pdf'] - uses: ./.github/workflows/v11-used-images-without-dependencies.yml - with: - microservice: ${{ matrix.service }} - tag: ${{ inputs.version_tag }} - secrets: inherit - - build_images_with_dependencies: - name: Build & Push Images with dependencies - needs: - - build_images - runs-on: ubuntu-22.04 - steps: - - name: Check out code into the right branch - uses: actions/checkout@v4 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: utmstack - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Download base images - run: | - docker pull ghcr.io/threatwinds/eventprocessor/base:${{ inputs.event_processor_tag }} - echo "Downloaded base images" - - - name: Build and Sign Agent - run: | - echo "Building Agent..." - cd ${{ github.workspace }}/agent - - GOOS=linux GOARCH=amd64 go build -o utmstack_agent_service -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . - GOOS=windows GOARCH=amd64 go build -o utmstack_agent_service.exe -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . - GOOS=windows GOARCH=arm64 go build -o utmstack_agent_service_arm64.exe -v -ldflags "-X 'github.com/utmstack/UTMStack/agent/config.REPLACE_KEY=${{ secrets.AGENT_SECRET_PREFIX }}'" . - - if [[ ${{ inputs.environment }} != "alpha" ]]; then - echo "Signing Windows Agent..." - FILES_TO_SIGN=("utmstack_agent_service.exe" "utmstack_agent_service_arm64.exe") - for file in "${FILES_TO_SIGN[@]}"; do - echo "Uploading $file for signing..." - RESPONSE=$(curl -sS -f -X POST http://customermanager.utmstack.com:8081/api/v1/upload \ - -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - -F "file=@$file") - - FILE_ID=$(echo "$RESPONSE" | jq -r '.file_id') - - if [[ -z "$FILE_ID" || "$FILE_ID" == "null" ]]; then - echo "โŒ Failed to upload $file for signing." - exit 1 - fi - - echo "Uploaded $file with file_id: $FILE_ID" - echo "Waiting for signing to complete..." - - while true; do - STATUS=$(curl -sS -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - http://customermanager.utmstack.com:8081/api/v1/status/$FILE_ID | jq -r '.status') - - if [[ "$STATUS" == "ready" ]]; then - echo "โœ… $file has been signed." - break - elif [[ "$STATUS" == "signing" ]]; then - echo "โณ Still signing $file... waiting 5s" - sleep 5 - else - echo "โŒ Unexpected status: $STATUS" - exit 1 - fi - done - - echo "Downloading signed $file..." - curl -sS -f -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - -o "$file" \ - http://customermanager.utmstack.com:8081/api/v1/download/$FILE_ID - - echo "Marking $file as finished..." - curl -sS -X POST -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - http://customermanager.utmstack.com:8081/api/v1/finish/$FILE_ID > /dev/null || true - done - - echo "โœ… All agents signed successfully." - else - echo "Skipping signing for Alpha environment." - fi - - - name: Build Plugins - env: - GOOS: linux - GOARCH: amd64 - run: | - cd ${{ github.workspace }}/plugins/alerts; go build -o com.utmstack.alerts.plugin -v . - cd ${{ github.workspace }}/plugins/aws; go build -o com.utmstack.aws.plugin -v . - cd ${{ github.workspace }}/plugins/azure; go build -o com.utmstack.azure.plugin -v . - cd ${{ github.workspace }}/plugins/bitdefender; go build -o com.utmstack.bitdefender.plugin -v . - cd ${{ github.workspace }}/plugins/config; go build -o com.utmstack.config.plugin -v . - cd ${{ github.workspace }}/plugins/events; go build -o com.utmstack.events.plugin -v . - cd ${{ github.workspace }}/plugins/gcp; go build -o com.utmstack.gcp.plugin -v . - cd ${{ github.workspace }}/plugins/geolocation; go build -o com.utmstack.geolocation.plugin -v . - cd ${{ github.workspace }}/plugins/inputs; go build -o com.utmstack.inputs.plugin -v . - cd ${{ github.workspace }}/plugins/o365; go build -o com.utmstack.o365.plugin -v . - cd ${{ github.workspace }}/plugins/sophos; go build -o com.utmstack.sophos.plugin -v . - cd ${{ github.workspace }}/plugins/stats; go build -o com.utmstack.stats.plugin -v . - cd ${{ github.workspace }}/plugins/soc-ai; go build -o com.utmstack.soc-ai.plugin -v . - cd ${{ github.workspace }}/plugins/modules-config; go build -o com.utmstack.modules-config.plugin -v . - - - name: Prepare Dependencies for Event Processor Image - run: | - mkdir -p ./geolocation - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/v11-${{ inputs.environment }}/geolocation/asn-blocks-v4.csv" \ - -o ./geolocation/asn-blocks-v4.csv - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/v11-${{ inputs.environment }}/geolocation/asn-blocks-v6.csv" \ - -o ./geolocation/asn-blocks-v6.csv - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/v11-${{ inputs.environment }}/geolocation/blocks-v4.csv" -o ./geolocation/blocks-v4.csv - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/v11-${{ inputs.environment }}/geolocation/blocks-v6.csv" -o ./geolocation/blocks-v6.csv - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/v11-${{ inputs.environment }}/geolocation/locations-en.csv" -o ./geolocation/locations-en.csv - - docker build -t ghcr.io/utmstack/utmstack/eventprocessor:${{ inputs.version_tag }}-community --build-arg BASE_IMAGE=ghcr.io/threatwinds/eventprocessor/base:${{ inputs.event_processor_tag }} -f ./event_processor.Dockerfile . - docker push ghcr.io/utmstack/utmstack/eventprocessor:${{ inputs.version_tag }}-community - echo "Event Processor image built and pushed" - - - name: Build & Push Agent Manager Image - run: | - cd ${{ github.workspace }}/agent-manager - GOOS=linux GOARCH=amd64 go build -o agent-manager -v . - - mkdir -p ./dependencies/agent/ - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/v11-${{ inputs.environment }}/agent/utmstack_agent_dependencies_linux.zip" -o ./dependencies/agent/utmstack_agent_dependencies_linux.zip - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/v11-${{ inputs.environment }}/agent/utmstack_agent_dependencies_windows.zip" -o ./dependencies/agent/utmstack_agent_dependencies_windows.zip - curl -sSL -H "Authorization: Bearer ${{ secrets.SIGNER_TOKEN }}" \ - "http://customermanager.utmstack.com:8081/api/v1/fs/v11-${{ inputs.environment }}/agent/utmstack_agent_dependencies_windows_arm64.zip" -o ./dependencies/agent/utmstack_agent_dependencies_windows_arm64.zip - - cp "${{ github.workspace }}/agent/utmstack_agent_service" ./dependencies/agent/ - # cp "${{ github.workspace }}/agent/utmstack_agent_service_arm64" ./dependencies/agent/ - cp "${{ github.workspace }}/agent/utmstack_agent_service.exe" ./dependencies/agent/ - cp "${{ github.workspace }}/agent/utmstack_agent_service_arm64.exe" ./dependencies/agent/ - cp "${{ github.workspace }}/agent/version.json" ./dependencies/agent/ - - docker build -t ghcr.io/utmstack/utmstack/agent-manager:${{ inputs.version_tag }}-community . - docker push ghcr.io/utmstack/utmstack/agent-manager:${{ inputs.version_tag }}-community - echo "Agent Manager image built and pushed" - - - name: Push new release - run: | - echo "Pushing new release..." - changelog=$(cat CHANGELOG.md) - - cmAuth=$(echo '${{ secrets.CM_AUTH }}' | jq -r '.') - id=$(echo "$cmAuth" | jq -r '.id') - key=$(echo "$cmAuth" | jq -r '.key') - - body=$(jq -n \ - --arg version "${{ inputs.version_tag }}" \ - --arg changelog "$changelog" \ - '{version: $version, changelog: $changelog}' - ) - - response=$(curl -s -X POST "https://customermanager.utmstack.com/${{ inputs.environment }}/api/v1/versions/register" \ - -H "Content-Type: application/json" \ - -H "id: $id" \ - -H "key: $key" \ - -d "$body") - - echo "Response: $response" \ No newline at end of file diff --git a/.github/workflows/v11-used-docker-java-11.yml b/.github/workflows/v11-used-docker-java-11.yml deleted file mode 100644 index d1b002747..000000000 --- a/.github/workflows/v11-used-docker-java-11.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Docker Image Java 11 - -on: - workflow_call: - inputs: - image_name: - required: true - type: string - tag: - required: true - type: string -jobs: - build: - name: Build - runs-on: ubuntu-24.04 - steps: - - name: Check out code into the right branch - uses: actions/checkout@v4 - - - name: Set up JDK 11 - uses: actions/setup-java@v4 - with: - java-version: "11" - distribution: "temurin" - - - name: Build with Maven - working-directory: ./${{inputs.image_name}} - run: | - echo "Building with maven" - echo "The configured version is: ${{ inputs.tag }}" - mvn -B -Drevision=${{ inputs.tag }} -Pprod clean package -s settings.xml - env: - MAVEN_TK: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: utmstack - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and Push the Image - uses: docker/build-push-action@v6 - with: - context: /home/runner/work/UTMStack/UTMStack/${{inputs.image_name}}/ - push: true - tags: ghcr.io/utmstack/utmstack/${{inputs.image_name}}:${{inputs.tag}} \ No newline at end of file diff --git a/.github/workflows/v11-used-images-without-dependencies.yml b/.github/workflows/v11-used-images-without-dependencies.yml deleted file mode 100644 index 18a301567..000000000 --- a/.github/workflows/v11-used-images-without-dependencies.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Runner -on: - workflow_call: - inputs: - tag: - required: true - type: string - microservice: - required: true - type: string - flags: - required: false - type: string - -jobs: - prepare_deployment: - name: Prepare deployment - ${{inputs.microservice}} - runs-on: ubuntu-24.04 - outputs: - tech: ${{ steps.get_tech.outputs.tech }} - steps: - - uses: actions/checkout@v4 - - - name: Determine Tech - id: get_tech - run: | - service="${{inputs.microservice}}" - if [[ "$service" == "backend" ]]; then - tech="java-11" - elif [[ "$service" == "frontend" ]]; then - tech="frontend" - elif [[ "$service" == "user-auditor" || "web-pdf" ]]; then - tech="java" - else - tech="unknown" - fi - echo $tech - echo "::set-output name=tech::$tech" - shell: bash - - frontend_deployment: - name: Frontend deployment - needs: prepare_deployment - if: ${{ needs.prepare_deployment.outputs.tech == 'frontend' }} - uses: ./.github/workflows/v10-v11-used-docker-frontend.yml - with: - image_name: ${{ inputs.microservice }} - environment: ${{inputs.tag}}-community - - golang_deployment: - name: Golang deployment - needs: prepare_deployment - if: ${{ needs.prepare_deployment.outputs.tech == 'golang' }} - uses: ./.github/workflows/v11-used-docker-golang.yml - with: - image_name: ${{ inputs.microservice }} - tag: ${{inputs.tag}}-community - flags: ${{inputs.flags}} - - java_11_deployment: - name: Java 11 deployment - needs: prepare_deployment - if: ${{ needs.prepare_deployment.outputs.tech == 'java-11' }} - uses: ./.github/workflows/v11-used-docker-java-11.yml - with: - image_name: ${{ inputs.microservice }} - tag: ${{inputs.tag}}-community - - java_deployment: - name: Java deployment - needs: prepare_deployment - if: ${{ needs.prepare_deployment.outputs.tech == 'java' }} - uses: ./.github/workflows/v10-v11-used-docker-java.yml - with: - image_name: ${{ inputs.microservice }} - environment: ${{inputs.tag}}-community \ No newline at end of file diff --git a/.github/workflows/workflow.png b/.github/workflows/workflow.png deleted file mode 100644 index 8e65f5f41..000000000 Binary files a/.github/workflows/workflow.png and /dev/null differ diff --git a/CHANGELOG.md b/CHANGELOG.md index bd964314e..69eb6385d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,18 @@ -# UTMStack 11.0.0-beta.1 Release Notes +# UTMStack 11.0.0 Release Notes This is the release notes for **UTMStack v11**, a major update from v10. This version introduces significant improvements and new features aimed at enhancing performance, scalability, and security. +## โš ๏ธ BREAKING CHANGE - Migration Required + +**IMPORTANT:** UTMStack v11 introduces fundamental architectural changes that make it **incompatible with v10**. + +- **Direct upgrades from v10 to v11 are NOT supported** +- A **complete migration** is required to move from v10 to v11 +- We are currently developing a **migration tool** to facilitate this process +- **Do not attempt to upgrade** your v10 installation to v11 until the migration tool is available + +Please contact our support team for guidance on migration planning and timeline. + ## Key Highlights ### Performance and Resource Optimization diff --git a/agent-manager/Dockerfile b/agent-manager/Dockerfile index e2f78b69f..41e405726 100644 --- a/agent-manager/Dockerfile +++ b/agent-manager/Dockerfile @@ -2,6 +2,7 @@ FROM ubuntu:24.04 COPY agent-manager /app/ COPY ./dependencies/agent/ /dependencies/agent/ +COPY ./dependencies/collector/ /dependencies/collector/ # Install jq RUN apt-get update && \ diff --git a/agent-manager/agent/agent_imp.go b/agent-manager/agent/agent_imp.go index 47f5728a1..b1ecd3774 100644 --- a/agent-manager/agent/agent_imp.go +++ b/agent-manager/agent/agent_imp.go @@ -9,6 +9,7 @@ import ( "time" "github.com/google/uuid" + "github.com/threatwinds/go-sdk/catcher" "github.com/utmstack/UTMStack/agent-manager/database" "github.com/utmstack/UTMStack/agent-manager/models" "github.com/utmstack/UTMStack/agent-manager/utils" @@ -82,7 +83,7 @@ func (s *AgentService) RegisterAgent(ctx context.Context, req *AgentRequest) (*A Key: oldAgent.AgentKey, }, nil } else { - utils.ALogger.ErrorF("agent with hostname %s already exists", agent.Hostname) + catcher.Error("agent already exists", err, map[string]any{"hostname": agent.Hostname}) return nil, status.Errorf(codes.AlreadyExists, "hostname has already been registered") } } @@ -91,7 +92,7 @@ func (s *AgentService) RegisterAgent(ctx context.Context, req *AgentRequest) (*A agent.AgentKey = key err = s.DBConnection.Create(agent) if err != nil { - utils.ALogger.ErrorF("failed to create agent: %v", err) + catcher.Error("failed to create agent", err, nil) return nil, status.Error(codes.Internal, fmt.Sprintf("failed to create agent: %v", err)) } @@ -105,7 +106,7 @@ func (s *AgentService) RegisterAgent(ctx context.Context, req *AgentRequest) (*A LastPing: time.Now(), } - utils.ALogger.Info("Agent %s with id %d registered correctly", agent.Hostname, agent.ID) + catcher.Info("Agent registered correctly", map[string]any{"hostname": agent.Hostname, "id": agent.ID}) return &AuthResponse{ Id: uint32(agent.ID), Key: key, @@ -125,7 +126,7 @@ func (s *AgentService) UpdateAgent(ctx context.Context, req *AgentRequest) (*Aut agent := &models.Agent{} err = s.DBConnection.GetFirst(agent, "id = ?", idInt) if err != nil { - utils.ALogger.ErrorF("failed to fetch agent: %v", err) + catcher.Error("failed to fetch agent", err, nil) return nil, status.Errorf(codes.NotFound, "agent not found") } @@ -156,7 +157,7 @@ func (s *AgentService) UpdateAgent(ctx context.Context, req *AgentRequest) (*Aut err = s.DBConnection.Upsert(&agent, "id = ?", nil, idInt) if err != nil { - utils.ALogger.ErrorF("failed to update agent: %v", err) + catcher.Error("failed to update agent", err, nil) return nil, status.Errorf(codes.Internal, "failed to update agent: %v", err) } @@ -180,18 +181,18 @@ func (s *AgentService) DeleteAgent(ctx context.Context, req *DeleteRequest) (*Au err = s.DBConnection.Upsert(&models.Agent{}, "id = ?", map[string]interface{}{"deleted_by": req.DeletedBy}, id) if err != nil { - utils.ALogger.ErrorF("unable to update delete_by field in agent: %v", err) + catcher.Error("unable to update delete_by field in agent", err, nil) } err = s.DBConnection.Delete(&models.AgentCommand{}, "agent_id = ?", false, uint(idInt)) if err != nil { - utils.ALogger.ErrorF("unable to delete agent commands: %v", err) + catcher.Error("unable to delete agent commands", err, nil) return &AuthResponse{}, status.Error(codes.Internal, fmt.Sprintf("unable to delete agent commands: %v", err.Error())) } err = s.DBConnection.Delete(&models.Agent{}, "id = ?", false, id) if err != nil { - utils.ALogger.ErrorF("unable to delete agent: %v", err) + catcher.Error("unable to delete agent", err, nil) return &AuthResponse{}, status.Error(codes.Internal, fmt.Sprintf("unable to delete agent: %v", err.Error())) } @@ -203,7 +204,7 @@ func (s *AgentService) DeleteAgent(ctx context.Context, req *DeleteRequest) (*Au delete(s.AgentStreamMap, uint(idInt)) s.AgentStreamMutex.Unlock() - utils.ALogger.Info("Agent with key %s deleted by %s", key, req.DeletedBy) + catcher.Info("Agent deleted", map[string]any{"key": key, "deleted_by": req.DeletedBy}) return &AuthResponse{ Id: uint32(idInt), @@ -218,7 +219,7 @@ func (s *AgentService) ListAgents(ctx context.Context, req *ListRequest) (*ListA agents := []models.Agent{} total, err := s.DBConnection.GetByPagination(&agents, page, filter, "", false) if err != nil { - utils.ALogger.ErrorF("failed to fetch agents: %v", err) + catcher.Error("failed to fetch agents", err, nil) return nil, status.Errorf(codes.Internal, "failed to fetch agents: %v", err) } @@ -266,7 +267,7 @@ func (s *AgentService) AgentStream(stream AgentService_AgentStreamServer) error switch msg := in.StreamMessage.(type) { case *BidirectionalStream_Result: - utils.ALogger.Info("Received command result from agent %s: %s", msg.Result.AgentId, msg.Result.Result) + catcher.Info("Received command result from agent", map[string]any{"agent_id": msg.Result.AgentId, "result": msg.Result.Result}) cmdID := msg.Result.GetCmdId() s.CommandResultChannelM.Lock() @@ -278,7 +279,7 @@ func (s *AgentService) AgentStream(stream AgentService_AgentStreamServer) error ExecutedAt: msg.Result.ExecutedAt, } } else { - utils.ALogger.ErrorF("failed to find result channel for CmdID: %s", cmdID) + catcher.Error("failed to find result channel for CmdID", nil, map[string]any{"cmdID": cmdID}) } s.CommandResultChannelM.Unlock() } @@ -324,7 +325,7 @@ func (s *AgentService) ProcessCommand(stream PanelService_ProcessCommandServer) histCommand := createHistoryCommand(cmd, cmdID, uint(streamId)) err = s.DBConnection.Create(&histCommand) if err != nil { - utils.ALogger.ErrorF("unable to create a new command history") + catcher.Error("unable to create a new command history", err, nil) } err = agentStream.Send(&BidirectionalStream{ @@ -348,7 +349,7 @@ func (s *AgentService) ProcessCommand(stream PanelService_ProcessCommandServer) cmd.AgentId, cmdID, ) if err != nil { - utils.ALogger.ErrorF("failed to update command status: %v", err) + catcher.Error("failed to update command status", err, nil) } err = stream.Send(result) @@ -369,7 +370,7 @@ func (s *AgentService) ListAgentCommands(ctx context.Context, req *ListRequest) commands := []models.AgentCommand{} total, err := s.DBConnection.GetByPagination(&commands, page, filter, "", false) if err != nil { - utils.ALogger.ErrorF("failed to fetch agent commands: %v", err) + catcher.Error("failed to fetch agent commands", err, nil) return nil, status.Errorf(codes.Internal, "failed to fetch agent commands: %v", err) } diff --git a/agent-manager/agent/collector.pb.go b/agent-manager/agent/collector.pb.go index 02c598a6f..bcf8d63b7 100644 --- a/agent-manager/agent/collector.pb.go +++ b/agent-manager/agent/collector.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 +// protoc-gen-go v1.36.9 // protoc v3.21.12 // source: collector.proto @@ -11,6 +11,7 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -23,16 +24,19 @@ const ( type CollectorModule int32 const ( - CollectorModule_AS_400 CollectorModule = 0 + CollectorModule_AS_400 CollectorModule = 0 + CollectorModule_UTMSTACK CollectorModule = 1 ) // Enum value maps for CollectorModule. var ( CollectorModule_name = map[int32]string{ 0: "AS_400", + 1: "UTMSTACK", } CollectorModule_value = map[string]int32{ - "AS_400": 0, + "AS_400": 0, + "UTMSTACK": 1, } ) @@ -64,23 +68,20 @@ func (CollectorModule) EnumDescriptor() ([]byte, []int) { } type RegisterRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Collector CollectorModule `protobuf:"varint,4,opt,name=collector,proto3,enum=agent.CollectorModule" json:"collector,omitempty"` unknownFields protoimpl.UnknownFields - - Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` - Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` - Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` - Collector CollectorModule `protobuf:"varint,4,opt,name=collector,proto3,enum=agent.CollectorModule" json:"collector,omitempty"` + sizeCache protoimpl.SizeCache } func (x *RegisterRequest) Reset() { *x = RegisterRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_collector_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_collector_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RegisterRequest) String() string { @@ -91,7 +92,7 @@ func (*RegisterRequest) ProtoMessage() {} func (x *RegisterRequest) ProtoReflect() protoreflect.Message { mi := &file_collector_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -135,21 +136,18 @@ func (x *RegisterRequest) GetCollector() CollectorModule { } type ListCollectorResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Rows []*Collector `protobuf:"bytes,1,rep,name=rows,proto3" json:"rows,omitempty"` + Total int32 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` unknownFields protoimpl.UnknownFields - - Rows []*Collector `protobuf:"bytes,1,rep,name=rows,proto3" json:"rows,omitempty"` - Total int32 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ListCollectorResponse) Reset() { *x = ListCollectorResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_collector_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_collector_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ListCollectorResponse) String() string { @@ -160,7 +158,7 @@ func (*ListCollectorResponse) ProtoMessage() {} func (x *ListCollectorResponse) ProtoReflect() protoreflect.Message { mi := &file_collector_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -190,27 +188,24 @@ func (x *ListCollectorResponse) GetTotal() int32 { } type Collector struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Status Status `protobuf:"varint,2,opt,name=status,proto3,enum=agent.Status" json:"status,omitempty"` + CollectorKey string `protobuf:"bytes,3,opt,name=collector_key,json=collectorKey,proto3" json:"collector_key,omitempty"` + Ip string `protobuf:"bytes,4,opt,name=ip,proto3" json:"ip,omitempty"` + Hostname string `protobuf:"bytes,5,opt,name=hostname,proto3" json:"hostname,omitempty"` + Version string `protobuf:"bytes,6,opt,name=version,proto3" json:"version,omitempty"` + Module CollectorModule `protobuf:"varint,7,opt,name=module,proto3,enum=agent.CollectorModule" json:"module,omitempty"` + LastSeen string `protobuf:"bytes,8,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"` unknownFields protoimpl.UnknownFields - - Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - Status Status `protobuf:"varint,2,opt,name=status,proto3,enum=agent.Status" json:"status,omitempty"` - CollectorKey string `protobuf:"bytes,3,opt,name=collector_key,json=collectorKey,proto3" json:"collector_key,omitempty"` - Ip string `protobuf:"bytes,4,opt,name=ip,proto3" json:"ip,omitempty"` - Hostname string `protobuf:"bytes,5,opt,name=hostname,proto3" json:"hostname,omitempty"` - Version string `protobuf:"bytes,6,opt,name=version,proto3" json:"version,omitempty"` - Module CollectorModule `protobuf:"varint,7,opt,name=module,proto3,enum=agent.CollectorModule" json:"module,omitempty"` - LastSeen string `protobuf:"bytes,8,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Collector) Reset() { *x = Collector{} - if protoimpl.UnsafeEnabled { - mi := &file_collector_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_collector_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Collector) String() string { @@ -221,7 +216,7 @@ func (*Collector) ProtoMessage() {} func (x *Collector) ProtoReflect() protoreflect.Message { mi := &file_collector_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -293,24 +288,21 @@ func (x *Collector) GetLastSeen() string { } type CollectorMessages struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Types that are assignable to StreamMessage: + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to StreamMessage: // // *CollectorMessages_Config // *CollectorMessages_Result StreamMessage isCollectorMessages_StreamMessage `protobuf_oneof:"stream_message"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CollectorMessages) Reset() { *x = CollectorMessages{} - if protoimpl.UnsafeEnabled { - mi := &file_collector_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_collector_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CollectorMessages) String() string { @@ -321,7 +313,7 @@ func (*CollectorMessages) ProtoMessage() {} func (x *CollectorMessages) ProtoReflect() protoreflect.Message { mi := &file_collector_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -336,23 +328,27 @@ func (*CollectorMessages) Descriptor() ([]byte, []int) { return file_collector_proto_rawDescGZIP(), []int{3} } -func (m *CollectorMessages) GetStreamMessage() isCollectorMessages_StreamMessage { - if m != nil { - return m.StreamMessage +func (x *CollectorMessages) GetStreamMessage() isCollectorMessages_StreamMessage { + if x != nil { + return x.StreamMessage } return nil } func (x *CollectorMessages) GetConfig() *CollectorConfig { - if x, ok := x.GetStreamMessage().(*CollectorMessages_Config); ok { - return x.Config + if x != nil { + if x, ok := x.StreamMessage.(*CollectorMessages_Config); ok { + return x.Config + } } return nil } func (x *CollectorMessages) GetResult() *ConfigKnowledge { - if x, ok := x.GetStreamMessage().(*CollectorMessages_Result); ok { - return x.Result + if x != nil { + if x, ok := x.StreamMessage.(*CollectorMessages_Result); ok { + return x.Result + } } return nil } @@ -374,22 +370,19 @@ func (*CollectorMessages_Config) isCollectorMessages_StreamMessage() {} func (*CollectorMessages_Result) isCollectorMessages_StreamMessage() {} type CollectorConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + CollectorId string `protobuf:"bytes,1,opt,name=collector_id,json=collectorId,proto3" json:"collector_id,omitempty"` + Groups []*CollectorConfigGroup `protobuf:"bytes,2,rep,name=groups,proto3" json:"groups,omitempty"` + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` unknownFields protoimpl.UnknownFields - - CollectorId string `protobuf:"bytes,1,opt,name=collector_id,json=collectorId,proto3" json:"collector_id,omitempty"` - Groups []*CollectorConfigGroup `protobuf:"bytes,2,rep,name=groups,proto3" json:"groups,omitempty"` - RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *CollectorConfig) Reset() { *x = CollectorConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_collector_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_collector_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CollectorConfig) String() string { @@ -400,7 +393,7 @@ func (*CollectorConfig) ProtoMessage() {} func (x *CollectorConfig) ProtoReflect() protoreflect.Message { mi := &file_collector_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -437,24 +430,21 @@ func (x *CollectorConfig) GetRequestId() string { } type CollectorConfigGroup struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` GroupName string `protobuf:"bytes,2,opt,name=group_name,json=groupName,proto3" json:"group_name,omitempty"` GroupDescription string `protobuf:"bytes,3,opt,name=group_description,json=groupDescription,proto3" json:"group_description,omitempty"` Configurations []*CollectorGroupConfigurations `protobuf:"bytes,4,rep,name=configurations,proto3" json:"configurations,omitempty"` CollectorId int32 `protobuf:"varint,5,opt,name=collector_id,json=collectorId,proto3" json:"collector_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CollectorConfigGroup) Reset() { *x = CollectorConfigGroup{} - if protoimpl.UnsafeEnabled { - mi := &file_collector_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_collector_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CollectorConfigGroup) String() string { @@ -465,7 +455,7 @@ func (*CollectorConfigGroup) ProtoMessage() {} func (x *CollectorConfigGroup) ProtoReflect() protoreflect.Message { mi := &file_collector_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -516,27 +506,24 @@ func (x *CollectorConfigGroup) GetCollectorId() int32 { } type CollectorGroupConfigurations struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - GroupId int32 `protobuf:"varint,2,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` - ConfKey string `protobuf:"bytes,3,opt,name=conf_key,json=confKey,proto3" json:"conf_key,omitempty"` - ConfValue string `protobuf:"bytes,4,opt,name=conf_value,json=confValue,proto3" json:"conf_value,omitempty"` - ConfName string `protobuf:"bytes,5,opt,name=conf_name,json=confName,proto3" json:"conf_name,omitempty"` - ConfDescription string `protobuf:"bytes,6,opt,name=conf_description,json=confDescription,proto3" json:"conf_description,omitempty"` - ConfDataType string `protobuf:"bytes,7,opt,name=conf_data_type,json=confDataType,proto3" json:"conf_data_type,omitempty"` - ConfRequired bool `protobuf:"varint,8,opt,name=conf_required,json=confRequired,proto3" json:"conf_required,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + GroupId int32 `protobuf:"varint,2,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + ConfKey string `protobuf:"bytes,3,opt,name=conf_key,json=confKey,proto3" json:"conf_key,omitempty"` + ConfValue string `protobuf:"bytes,4,opt,name=conf_value,json=confValue,proto3" json:"conf_value,omitempty"` + ConfName string `protobuf:"bytes,5,opt,name=conf_name,json=confName,proto3" json:"conf_name,omitempty"` + ConfDescription string `protobuf:"bytes,6,opt,name=conf_description,json=confDescription,proto3" json:"conf_description,omitempty"` + ConfDataType string `protobuf:"bytes,7,opt,name=conf_data_type,json=confDataType,proto3" json:"conf_data_type,omitempty"` + ConfRequired bool `protobuf:"varint,8,opt,name=conf_required,json=confRequired,proto3" json:"conf_required,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CollectorGroupConfigurations) Reset() { *x = CollectorGroupConfigurations{} - if protoimpl.UnsafeEnabled { - mi := &file_collector_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_collector_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *CollectorGroupConfigurations) String() string { @@ -547,7 +534,7 @@ func (*CollectorGroupConfigurations) ProtoMessage() {} func (x *CollectorGroupConfigurations) ProtoReflect() protoreflect.Message { mi := &file_collector_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -619,21 +606,18 @@ func (x *CollectorGroupConfigurations) GetConfRequired() bool { } type ConfigKnowledge struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Accepted string `protobuf:"bytes,1,opt,name=accepted,proto3" json:"accepted,omitempty"` + RequestId string `protobuf:"bytes,2,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` unknownFields protoimpl.UnknownFields - - Accepted string `protobuf:"bytes,1,opt,name=accepted,proto3" json:"accepted,omitempty"` - RequestId string `protobuf:"bytes,2,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ConfigKnowledge) Reset() { *x = ConfigKnowledge{} - if protoimpl.UnsafeEnabled { - mi := &file_collector_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_collector_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ConfigKnowledge) String() string { @@ -644,7 +628,7 @@ func (*ConfigKnowledge) ProtoMessage() {} func (x *ConfigKnowledge) ProtoReflect() protoreflect.Message { mi := &file_collector_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -674,20 +658,17 @@ func (x *ConfigKnowledge) GetRequestId() string { } type ConfigRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Module CollectorModule `protobuf:"varint,1,opt,name=module,proto3,enum=agent.CollectorModule" json:"module,omitempty"` unknownFields protoimpl.UnknownFields - - Module CollectorModule `protobuf:"varint,1,opt,name=module,proto3,enum=agent.CollectorModule" json:"module,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ConfigRequest) Reset() { *x = ConfigRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_collector_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_collector_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ConfigRequest) String() string { @@ -698,7 +679,7 @@ func (*ConfigRequest) ProtoMessage() {} func (x *ConfigRequest) ProtoReflect() protoreflect.Message { mi := &file_collector_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -722,150 +703,86 @@ func (x *ConfigRequest) GetModule() CollectorModule { var File_collector_proto protoreflect.FileDescriptor -var file_collector_proto_rawDesc = []byte{ - 0x0a, 0x0f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x05, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x1a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8d, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, - 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, - 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x12, 0x34, 0x0a, 0x09, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x52, 0x09, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x53, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x24, 0x0a, 0x04, 0x72, 0x6f, 0x77, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, - 0x04, 0x72, 0x6f, 0x77, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0xfa, 0x01, 0x0a, 0x09, - 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0d, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x6b, 0x65, - 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x6d, - 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x4d, 0x6f, 0x64, - 0x75, 0x6c, 0x65, 0x52, 0x06, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6c, - 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x22, 0x89, 0x01, 0x0a, 0x11, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x12, 0x30, - 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x30, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x16, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4b, - 0x6e, 0x6f, 0x77, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x42, 0x10, 0x0a, 0x0e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x6d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x22, 0x88, 0x01, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x33, 0x0a, 0x06, 0x67, - 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x06, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, - 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x22, - 0xe2, 0x01, 0x0a, 0x14, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x72, - 0x6f, 0x75, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x67, 0x72, 0x6f, 0x75, 0x70, - 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x10, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4b, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x47, 0x72, - 0x6f, 0x75, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x52, 0x0e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x69, - 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x49, 0x64, 0x22, 0x96, 0x02, 0x0a, 0x1c, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, - 0x12, 0x19, 0x0a, 0x08, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x66, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, - 0x6f, 0x6e, 0x66, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, - 0x6e, 0x66, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, - 0x6f, 0x6e, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x5f, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x5f, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x66, - 0x44, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x66, - 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x22, 0x4c, 0x0a, - 0x0f, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4b, 0x6e, 0x6f, 0x77, 0x6c, 0x65, 0x64, 0x67, 0x65, - 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x22, 0x3f, 0x0a, 0x0d, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x06, - 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x4d, 0x6f, - 0x64, 0x75, 0x6c, 0x65, 0x52, 0x06, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x2a, 0x1d, 0x0a, 0x0f, - 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x12, - 0x0a, 0x0a, 0x06, 0x41, 0x53, 0x5f, 0x34, 0x30, 0x30, 0x10, 0x00, 0x32, 0xee, 0x02, 0x0a, 0x10, - 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x42, 0x0a, 0x11, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x16, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x0f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x14, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x43, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x12, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0f, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x18, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x1a, 0x18, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, - 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x44, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x14, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x00, 0x32, 0x64, 0x0a, 0x15, - 0x50, 0x61, 0x6e, 0x65, 0x6c, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4b, 0x0a, 0x17, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, - 0x72, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x16, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x16, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4b, 0x6e, 0x6f, 0x77, 0x6c, 0x65, 0x64, 0x67, 0x65, - 0x22, 0x00, 0x42, 0x32, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x75, 0x74, 0x6d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x55, 0x54, 0x4d, 0x53, 0x74, 0x61, - 0x63, 0x6b, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, - 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +const file_collector_proto_rawDesc = "" + + "\n" + + "\x0fcollector.proto\x12\x05agent\x1a\fcommon.proto\"\x8d\x01\n" + + "\x0fRegisterRequest\x12\x0e\n" + + "\x02ip\x18\x01 \x01(\tR\x02ip\x12\x1a\n" + + "\bhostname\x18\x02 \x01(\tR\bhostname\x12\x18\n" + + "\aversion\x18\x03 \x01(\tR\aversion\x124\n" + + "\tcollector\x18\x04 \x01(\x0e2\x16.agent.CollectorModuleR\tcollector\"S\n" + + "\x15ListCollectorResponse\x12$\n" + + "\x04rows\x18\x01 \x03(\v2\x10.agent.CollectorR\x04rows\x12\x14\n" + + "\x05total\x18\x02 \x01(\x05R\x05total\"\xfa\x01\n" + + "\tCollector\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x05R\x02id\x12%\n" + + "\x06status\x18\x02 \x01(\x0e2\r.agent.StatusR\x06status\x12#\n" + + "\rcollector_key\x18\x03 \x01(\tR\fcollectorKey\x12\x0e\n" + + "\x02ip\x18\x04 \x01(\tR\x02ip\x12\x1a\n" + + "\bhostname\x18\x05 \x01(\tR\bhostname\x12\x18\n" + + "\aversion\x18\x06 \x01(\tR\aversion\x12.\n" + + "\x06module\x18\a \x01(\x0e2\x16.agent.CollectorModuleR\x06module\x12\x1b\n" + + "\tlast_seen\x18\b \x01(\tR\blastSeen\"\x89\x01\n" + + "\x11CollectorMessages\x120\n" + + "\x06config\x18\x01 \x01(\v2\x16.agent.CollectorConfigH\x00R\x06config\x120\n" + + "\x06result\x18\x02 \x01(\v2\x16.agent.ConfigKnowledgeH\x00R\x06resultB\x10\n" + + "\x0estream_message\"\x88\x01\n" + + "\x0fCollectorConfig\x12!\n" + + "\fcollector_id\x18\x01 \x01(\tR\vcollectorId\x123\n" + + "\x06groups\x18\x02 \x03(\v2\x1b.agent.CollectorConfigGroupR\x06groups\x12\x1d\n" + + "\n" + + "request_id\x18\x03 \x01(\tR\trequestId\"\xe2\x01\n" + + "\x14CollectorConfigGroup\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x1d\n" + + "\n" + + "group_name\x18\x02 \x01(\tR\tgroupName\x12+\n" + + "\x11group_description\x18\x03 \x01(\tR\x10groupDescription\x12K\n" + + "\x0econfigurations\x18\x04 \x03(\v2#.agent.CollectorGroupConfigurationsR\x0econfigurations\x12!\n" + + "\fcollector_id\x18\x05 \x01(\x05R\vcollectorId\"\x96\x02\n" + + "\x1cCollectorGroupConfigurations\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x19\n" + + "\bgroup_id\x18\x02 \x01(\x05R\agroupId\x12\x19\n" + + "\bconf_key\x18\x03 \x01(\tR\aconfKey\x12\x1d\n" + + "\n" + + "conf_value\x18\x04 \x01(\tR\tconfValue\x12\x1b\n" + + "\tconf_name\x18\x05 \x01(\tR\bconfName\x12)\n" + + "\x10conf_description\x18\x06 \x01(\tR\x0fconfDescription\x12$\n" + + "\x0econf_data_type\x18\a \x01(\tR\fconfDataType\x12#\n" + + "\rconf_required\x18\b \x01(\bR\fconfRequired\"L\n" + + "\x0fConfigKnowledge\x12\x1a\n" + + "\baccepted\x18\x01 \x01(\tR\baccepted\x12\x1d\n" + + "\n" + + "request_id\x18\x02 \x01(\tR\trequestId\"?\n" + + "\rConfigRequest\x12.\n" + + "\x06module\x18\x01 \x01(\x0e2\x16.agent.CollectorModuleR\x06module*+\n" + + "\x0fCollectorModule\x12\n" + + "\n" + + "\x06AS_400\x10\x00\x12\f\n" + + "\bUTMSTACK\x10\x012\xee\x02\n" + + "\x10CollectorService\x12B\n" + + "\x11RegisterCollector\x12\x16.agent.RegisterRequest\x1a\x13.agent.AuthResponse\"\x00\x12>\n" + + "\x0fDeleteCollector\x12\x14.agent.DeleteRequest\x1a\x13.agent.AuthResponse\"\x00\x12C\n" + + "\rListCollector\x12\x12.agent.ListRequest\x1a\x1c.agent.ListCollectorResponse\"\x00\x12K\n" + + "\x0fCollectorStream\x12\x18.agent.CollectorMessages\x1a\x18.agent.CollectorMessages\"\x00(\x010\x01\x12D\n" + + "\x12GetCollectorConfig\x12\x14.agent.ConfigRequest\x1a\x16.agent.CollectorConfig\"\x002d\n" + + "\x15PanelCollectorService\x12K\n" + + "\x17RegisterCollectorConfig\x12\x16.agent.CollectorConfig\x1a\x16.agent.ConfigKnowledge\"\x00B5Z3github.com/utmstack/UTMStack/docker-collector/agentb\x06proto3" var ( file_collector_proto_rawDescOnce sync.Once - file_collector_proto_rawDescData = file_collector_proto_rawDesc + file_collector_proto_rawDescData []byte ) func file_collector_proto_rawDescGZIP() []byte { file_collector_proto_rawDescOnce.Do(func() { - file_collector_proto_rawDescData = protoimpl.X.CompressGZIP(file_collector_proto_rawDescData) + file_collector_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_collector_proto_rawDesc), len(file_collector_proto_rawDesc))) }) return file_collector_proto_rawDescData } var file_collector_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_collector_proto_msgTypes = make([]protoimpl.MessageInfo, 9) -var file_collector_proto_goTypes = []interface{}{ +var file_collector_proto_goTypes = []any{ (CollectorModule)(0), // 0: agent.CollectorModule (*RegisterRequest)(nil), // 1: agent.RegisterRequest (*ListCollectorResponse)(nil), // 2: agent.ListCollectorResponse @@ -916,117 +833,7 @@ func file_collector_proto_init() { return } file_common_proto_init() - if !protoimpl.UnsafeEnabled { - file_collector_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RegisterRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_collector_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListCollectorResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_collector_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Collector); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_collector_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CollectorMessages); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_collector_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CollectorConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_collector_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CollectorConfigGroup); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_collector_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CollectorGroupConfigurations); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_collector_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ConfigKnowledge); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_collector_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ConfigRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_collector_proto_msgTypes[3].OneofWrappers = []interface{}{ + file_collector_proto_msgTypes[3].OneofWrappers = []any{ (*CollectorMessages_Config)(nil), (*CollectorMessages_Result)(nil), } @@ -1034,7 +841,7 @@ func file_collector_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_collector_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_collector_proto_rawDesc), len(file_collector_proto_rawDesc)), NumEnums: 1, NumMessages: 9, NumExtensions: 0, @@ -1046,7 +853,6 @@ func file_collector_proto_init() { MessageInfos: file_collector_proto_msgTypes, }.Build() File_collector_proto = out.File - file_collector_proto_rawDesc = nil file_collector_proto_goTypes = nil file_collector_proto_depIdxs = nil } diff --git a/agent-manager/agent/collector_imp.go b/agent-manager/agent/collector_imp.go index 17b5434e0..271ac5919 100644 --- a/agent-manager/agent/collector_imp.go +++ b/agent-manager/agent/collector_imp.go @@ -4,11 +4,13 @@ import ( context "context" "fmt" "io" + "os" "strconv" sync "sync" "time" "github.com/google/uuid" + "github.com/threatwinds/go-sdk/catcher" "github.com/utmstack/UTMStack/agent-manager/config" "github.com/utmstack/UTMStack/agent-manager/database" "github.com/utmstack/UTMStack/agent-manager/models" @@ -61,7 +63,8 @@ func InitCollectorService() { collectors := []models.Collector{} _, err := CollectorServ.DBConnection.GetAll(&collectors, "") if err != nil { - utils.ALogger.Fatal("failed to fetch collectors: %v", err) + catcher.Error("failed to fetch collectors", err, nil) + os.Exit(1) } for _, c := range collectors { CollectorServ.CacheCollectorKey[c.ID] = c.CollectorKey @@ -76,7 +79,7 @@ func InitCollectorService() { moduleConfig := &types.ConfigurationSection{} moduleConfig, err = client.GetUTMConfig(moduleType) if err != nil { - utils.ALogger.ErrorF("failed to get module config: %v", err) + catcher.Error("failed to get module config", err, nil) time.Sleep(5 * time.Second) continue external } @@ -86,7 +89,7 @@ func InitCollectorService() { var idInt int idInt, err = strconv.Atoi(group.CollectorID) if err != nil { - utils.ALogger.ErrorF("invalid collector ID: %v", err) + catcher.Error("invalid collector ID", err, nil) continue } @@ -128,7 +131,7 @@ func (s *CollectorService) RegisterCollector(ctx context.Context, req *RegisterR Key: oldCollector.CollectorKey, }, nil } else { - utils.ALogger.ErrorF("collector %s(%s) with id %d already registered with different IP", oldCollector.Hostname, oldCollector.Module, oldCollector.ID) + catcher.Error("collector already registered with different IP", nil, map[string]any{"hostname": oldCollector.Hostname, "module": oldCollector.Module, "id": oldCollector.ID}) return nil, status.Errorf(codes.AlreadyExists, "hostname has already been registered") } } @@ -137,7 +140,7 @@ func (s *CollectorService) RegisterCollector(ctx context.Context, req *RegisterR collector.CollectorKey = key err = s.DBConnection.Create(collector) if err != nil { - utils.ALogger.ErrorF("failed to create collector: %v", err) + catcher.Error("failed to create collector", err, nil) return nil, status.Error(codes.Internal, fmt.Sprintf("failed to create collector: %v", err)) } @@ -151,7 +154,7 @@ func (s *CollectorService) RegisterCollector(ctx context.Context, req *RegisterR LastPing: time.Now(), } - utils.ALogger.Info("Collector %s(%s) with id %d registered correctly", collector.Hostname, collector.Module, collector.ID) + catcher.Info("Collector registered correctly", map[string]any{"hostname": collector.Hostname, "module": collector.Module, "id": collector.ID}) return &AuthResponse{ Id: uint32(collector.ID), Key: key, @@ -170,12 +173,12 @@ func (s *CollectorService) DeleteCollector(ctx context.Context, req *DeleteReque err = s.DBConnection.Upsert(&models.Collector{}, "id = ?", map[string]interface{}{"deleted_by": req.DeletedBy}, id) if err != nil { - utils.ALogger.ErrorF("unable to delete collector: %v", err) + catcher.Error("unable to delete collector", err, nil) } err = s.DBConnection.Delete(&models.Collector{}, "id = ?", false, id) if err != nil { - utils.ALogger.ErrorF("unable to delete collector: %v", err) + catcher.Error("unable to delete collector", err, nil) return nil, status.Error(codes.Internal, fmt.Sprintf("unable to delete collector: %v", err.Error())) } @@ -187,7 +190,7 @@ func (s *CollectorService) DeleteCollector(ctx context.Context, req *DeleteReque delete(s.CollectorStreamMap, uint(idInt)) s.CollectorStreamMutex.Unlock() - utils.ALogger.Info("Collector with key %s deleted by %s", key, req.DeletedBy) + catcher.Info("Collector deleted", map[string]any{"key": key, "deleted_by": req.DeletedBy}) return &AuthResponse{ Id: uint32(idInt), Key: key, @@ -201,7 +204,7 @@ func (s *CollectorService) ListCollector(ctx context.Context, req *ListRequest) collectors := []models.Collector{} total, err := s.DBConnection.GetByPagination(&collectors, page, filter, "", false) if err != nil { - utils.ALogger.ErrorF("failed to fetch collectors: %v", err) + catcher.Error("failed to fetch collectors", err, nil) return nil, status.Errorf(codes.Internal, "failed to fetch collectors: %v", err) } return convertModelToCollectorResponse(collectors, total), nil @@ -211,7 +214,7 @@ func (s *CollectorService) ProcessPendingConfigs() { for configs := range s.CollectorPendigConfigChan { collectorID, err := strconv.Atoi(configs.CollectorId) if err != nil { - utils.ALogger.ErrorF("invalid collector ID: %v", err) + catcher.Error("invalid collector ID", err, nil) continue } @@ -228,7 +231,7 @@ func (s *CollectorService) ProcessPendingConfigs() { }, }) if err != nil { - utils.ALogger.ErrorF("failed to send config to collector: %v", err) + catcher.Error("failed to send config to collector", err, nil) } } } @@ -273,7 +276,7 @@ func (s *CollectorService) CollectorStream(stream CollectorService_CollectorStre switch msg := in.StreamMessage.(type) { case *CollectorMessages_Result: - utils.ALogger.Info("Received Knowlodge: %s", msg.Result.RequestId) + catcher.Info("Received Knowledge", map[string]any{"request_id": msg.Result.RequestId}) case *CollectorMessages_Config: // Not implemented diff --git a/agent-manager/agent/lastseen_imp.go b/agent-manager/agent/lastseen_imp.go index dace7d91e..6a877e982 100644 --- a/agent-manager/agent/lastseen_imp.go +++ b/agent-manager/agent/lastseen_imp.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/threatwinds/go-sdk/catcher" "github.com/utmstack/UTMStack/agent-manager/database" "github.com/utmstack/UTMStack/agent-manager/models" "github.com/utmstack/UTMStack/agent-manager/utils" @@ -50,7 +51,7 @@ func (s *LastSeenService) InitPingSync() { for { _, err := s.DBConnection.GetAll(&pings, "") if err != nil { - utils.ALogger.ErrorF("failed to get LastSeen items: %v", err) + catcher.Error("failed to get LastSeen items", err, nil) time.Sleep(5 * time.Second) continue } @@ -77,7 +78,7 @@ func (s *LastSeenService) processPings() { } } - utils.ALogger.Info("processPings goroutine ended") + catcher.Info("processPings goroutine ended", nil) } func (s *LastSeenService) flushLastSeenToDB() { @@ -106,7 +107,7 @@ func (s *LastSeenService) flushLastSeenToDB() { // Database operations dbOpsCount := len(pings) - + if dbOpsCount == 0 { continue } @@ -130,7 +131,7 @@ func (s *LastSeenService) flushLastSeenToDB() { for ping := range pingChan { err := s.DBConnection.Upsert(&ping, "connector_id = ?", nil, ping.ConnectorID) if err != nil { - utils.ALogger.ErrorF("failed to save LastSeen item for connector %d: %v", ping.ConnectorID, err) + catcher.Error("failed to save LastSeen item for connector", err, map[string]any{"connector_id": ping.ConnectorID}) select { case errorChan <- err: default: diff --git a/agent-manager/agent/parser.go b/agent-manager/agent/parser.go index 5e6bb388e..973dfe837 100644 --- a/agent-manager/agent/parser.go +++ b/agent-manager/agent/parser.go @@ -4,6 +4,7 @@ import ( "regexp" "strconv" + "github.com/threatwinds/go-sdk/catcher" "github.com/utmstack/UTMStack/agent-manager/config" "github.com/utmstack/UTMStack/agent-manager/models" "github.com/utmstack/UTMStack/agent-manager/utils" @@ -39,7 +40,7 @@ func createHistoryCommand(cmd *UtmCommand, cmdID string, agentId uint) *models.A func parseAgentToProto(agent models.Agent) *Agent { agentStatus, lastSeen, err := LastSeenServ.GetLastSeenStatus(agent.ID, "agent") if err != nil { - utils.ALogger.ErrorF("failed to get last seen status for agent %d: %v", agent.ID, err) + catcher.Error("failed to get last seen status for agent", err, map[string]any{"agent": agent.ID}) } agentResult := &Agent{ Id: uint32(agent.ID), @@ -107,7 +108,7 @@ func replaceSecretValues(input string) string { func modelToProtoCollector(model models.Collector) *Collector { collectorStatus, lastSeen, err := LastSeenServ.GetLastSeenStatus(model.ID, "collector") if err != nil { - utils.ALogger.ErrorF("failed to get last seen status for collector %d: %v", model.ID, err) + catcher.Error("failed to get last seen status for collector", err, map[string]any{"model": model.ID}) } return &Collector{ Id: int32(model.ID), diff --git a/agent-manager/agent/utmgrpc.go b/agent-manager/agent/utmgrpc.go index d26688805..6b2d9c01a 100644 --- a/agent-manager/agent/utmgrpc.go +++ b/agent-manager/agent/utmgrpc.go @@ -3,9 +3,10 @@ package agent import ( "crypto/tls" "net" + "os" + "github.com/threatwinds/go-sdk/catcher" "github.com/utmstack/UTMStack/agent-manager/config" - "github.com/utmstack/UTMStack/agent-manager/utils" grpc "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/health" @@ -15,7 +16,8 @@ import ( func InitGrpcServer() { err := InitAgentService() if err != nil { - utils.ALogger.Fatal("failed to init agent service: %v", err) + catcher.Error("failed to init agent service", err, nil) + os.Exit(1) } go InitCollectorService() @@ -27,14 +29,14 @@ func InitGrpcServer() { func StartGrpcServer() { listener, err := net.Listen("tcp", "0.0.0.0:50051") if err != nil { - utils.ALogger.Fatal("failed to listen: %v", err) - return + catcher.Error("failed to listen", err, nil) + os.Exit(1) } loadedCert, err := tls.LoadX509KeyPair(config.CertPath, config.CertKeyPath) if err != nil { - utils.ALogger.Fatal("failed to load TLS credentials: %v", err) - return + catcher.Error("failed to load TLS credentials: %v", err, nil) + os.Exit(1) } transportCredentials := credentials.NewTLS(&tls.Config{ @@ -57,8 +59,9 @@ func StartGrpcServer() { grpc_health_v1.RegisterHealthServer(grpcServer, healthServer) healthServer.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING) - utils.ALogger.Info("Starting gRPC server on 0.0.0.0:50051") + catcher.Info("Starting gRPC server on 0.0.0.0:50051", nil) if err := grpcServer.Serve(listener); err != nil { - utils.ALogger.Fatal("failed to serve: %v", err) + catcher.Error("failed to serve", err, nil) + os.Exit(1) } } diff --git a/agent-manager/go.mod b/agent-manager/go.mod index b06dfaa5f..1352089a5 100644 --- a/agent-manager/go.mod +++ b/agent-manager/go.mod @@ -7,7 +7,6 @@ require ( github.com/gin-contrib/gzip v1.2.3 github.com/gin-gonic/gin v1.10.1 github.com/google/uuid v1.6.0 - github.com/threatwinds/logger v1.2.2 github.com/utmstack/config-client-go v1.2.7 google.golang.org/grpc v1.74.2 google.golang.org/protobuf v1.36.6 @@ -15,6 +14,8 @@ require ( gorm.io/gorm v1.30.0 ) +require github.com/kr/text v0.2.0 // indirect + require ( github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect @@ -33,13 +34,12 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/threatwinds/go-sdk v1.0.45 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect golang.org/x/arch v0.19.0 // indirect @@ -49,6 +49,5 @@ require ( golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/agent-manager/go.sum b/agent-manager/go.sum index 0f9fd6ccf..39d1ca794 100644 --- a/agent-manager/go.sum +++ b/agent-manager/go.sum @@ -74,10 +74,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -90,8 +88,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/threatwinds/logger v1.2.2 h1:sVuT8yhbecPqP4tT8EwHfp1czNC6e1wdkE1ihNnuBdA= -github.com/threatwinds/logger v1.2.2/go.mod h1:Amq0QI1y7fkTpnBUgeGVu2Z/C4u4ys2pNLUOuj3UAAU= +github.com/threatwinds/go-sdk v1.0.45 h1:KZ3s3HviNRrOkg5EqjFnoauANFFzTqjNFyshPLY2SoI= +github.com/threatwinds/go-sdk v1.0.45/go.mod h1:tcWn6r6vqID/W/nL3UKfc5NafA3V/cSkiLvfJnwB58c= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -132,8 +130,6 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/agent-manager/main.go b/agent-manager/main.go index 0e1e39ba3..227e656f1 100644 --- a/agent-manager/main.go +++ b/agent-manager/main.go @@ -1,19 +1,21 @@ package main import ( + "os" + + "github.com/threatwinds/go-sdk/catcher" "github.com/utmstack/UTMStack/agent-manager/agent" "github.com/utmstack/UTMStack/agent-manager/database" "github.com/utmstack/UTMStack/agent-manager/updates" - "github.com/utmstack/UTMStack/agent-manager/utils" ) func main() { - utils.InitLogger() - utils.ALogger.Info("Starting Agent Manager v1.0.0 ...") + catcher.Info("Starting Agent Manager v1.0.0 ...", nil) err := database.MigrateDatabase() if err != nil { - utils.ALogger.Fatal("failed to migrate database: %v", err) + catcher.Error("failed to migrate database", err, nil) + os.Exit(1) } go updates.InitUpdatesManager() diff --git a/agent-manager/models/collector.go b/agent-manager/models/collector.go index 577ac86ba..ff4b084c7 100644 --- a/agent-manager/models/collector.go +++ b/agent-manager/models/collector.go @@ -5,7 +5,8 @@ import "gorm.io/gorm" type CollectorModule string const ( - AS_400 CollectorModule = "AS_400" + AS_400 CollectorModule = "AS_400" + UTMSTACK CollectorModule = "UTMSTACK" ) type Collector struct { diff --git a/agent-manager/protos/collector.proto b/agent-manager/protos/collector.proto index a4d3275ab..99fd4d551 100644 --- a/agent-manager/protos/collector.proto +++ b/agent-manager/protos/collector.proto @@ -19,6 +19,7 @@ service PanelCollectorService { enum CollectorModule{ AS_400 = 0; + UTMSTACK = 1; } message RegisterRequest { diff --git a/agent-manager/updates/updates.go b/agent-manager/updates/updates.go index e23c76581..71642889a 100644 --- a/agent-manager/updates/updates.go +++ b/agent-manager/updates/updates.go @@ -3,12 +3,13 @@ package updates import ( "crypto/tls" "net/http" + "os" "github.com/gin-contrib/gzip" + "github.com/threatwinds/go-sdk/catcher" "github.com/gin-gonic/gin" "github.com/utmstack/UTMStack/agent-manager/config" - "github.com/utmstack/UTMStack/agent-manager/utils" ) func InitUpdatesManager() { @@ -16,7 +17,7 @@ func InitUpdatesManager() { } func ServeDependencies() { - utils.ALogger.LogF(100, "Serving dependencies from %s", config.UpdatesDependenciesFolder) + catcher.Info("Serving dependencies", map[string]any{"path": config.UpdatesDependenciesFolder}) gin.SetMode(gin.ReleaseMode) r := gin.New() @@ -32,7 +33,8 @@ func ServeDependencies() { loadedCert, err := tls.LoadX509KeyPair(config.CertPath, config.CertKeyPath) if err != nil { - utils.ALogger.Fatal("failed to load TLS credentials: %v", err) + catcher.Error("failed to load TLS credentials", err, nil) + os.Exit(1) } tlsConfig := &tls.Config{ @@ -53,9 +55,9 @@ func ServeDependencies() { TLSConfig: tlsConfig, } - utils.ALogger.Info("Starting HTTP server on port 8080") + catcher.Info("Starting HTTP server on port 8080", nil) if err := server.ListenAndServeTLS("", ""); err != nil { - utils.ALogger.ErrorF("error starting HTTP server: %v", err) + catcher.Error("error starting HTTP server", err, nil) return } } diff --git a/agent-manager/utils/logger.go b/agent-manager/utils/logger.go deleted file mode 100644 index 70c32fd8f..000000000 --- a/agent-manager/utils/logger.go +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import ( - "os" - "strconv" - "sync" - - "github.com/threatwinds/logger" -) - -var ( - ALogger *logger.Logger - loggerOnceInstance sync.Once -) - -func InitLogger() { - logLevel := os.Getenv("LOG_LEVEL") - logLevelInt := 200 - if logLevel != "" { - logL, err := strconv.Atoi(logLevel) - if err == nil { - logLevelInt = logL - } - } - - loggerOnceInstance.Do(func() { - ALogger = logger.NewLogger( - &logger.Config{Format: "text", Level: logLevelInt, Output: "stdout"}, - ) - }) -} diff --git a/agent/config/const.go b/agent/config/const.go index ba9dbe6c8..ea0a86e2b 100644 --- a/agent/config/const.go +++ b/agent/config/const.go @@ -35,6 +35,11 @@ var ( VersionPath = filepath.Join(utils.GetMyPath(), "version.json") UpdaterSelfLinux = "utmstack_updater_self" + // TLS Configuration for Integrations + IntegrationCertPath = filepath.Join(utils.GetMyPath(), "certs", "integration.crt") + IntegrationKeyPath = filepath.Join(utils.GetMyPath(), "certs", "integration.key") + IntegrationCAPath = filepath.Join(utils.GetMyPath(), "certs", "integration-ca.crt") + DataTypeWindowsAgent DataType = "wineventlog" DataTypeSyslog DataType = "syslog" DataTypeVmware DataType = "vmware-esxi" diff --git a/agent/main.go b/agent/main.go index fc6cfd61b..60bd7a575 100644 --- a/agent/main.go +++ b/agent/main.go @@ -93,12 +93,80 @@ func main() { integration := os.Args[2] proto := os.Args[3] - port, err := modules.ChangeIntegrationStatus(integration, proto, arg == "enable-integration") + tlsEnabled := false + for _, arg := range os.Args[4:] { + if arg == "--tls" { + tlsEnabled = true + break + } + } + + var port string + var err error + + if arg == "enable-integration" && tlsEnabled { + port, err = modules.ChangeIntegrationStatus(integration, proto, true, true) + } else if arg == "enable-integration" { + port, err = modules.ChangeIntegrationStatus(integration, proto, true, false) + } else { + port, err = modules.ChangeIntegrationStatus(integration, proto, false) + } + if err != nil { - fmt.Println("Error trying to change integration status: ", err) + fmt.Println("Error:", err) os.Exit(1) } - fmt.Printf("Action %s %s %s correctly in port %s\n", arg, integration, proto, port) + + if arg == "enable-integration" && tlsEnabled { + fmt.Printf("Integration %s %s enabled with TLS on port %s\n", integration, proto, port) + } else if arg == "enable-integration" { + fmt.Printf("Integration %s %s enabled on port %s\n", integration, proto, port) + } else { + fmt.Printf("Integration %s %s disabled (port %s freed)\n", integration, proto, port) + } + time.Sleep(5 * time.Second) + + case "load-tls-certs": + if len(os.Args) < 4 { + fmt.Println("Usage: ./utmstack_agent load-tls-certs [ca_certificate_path]") + fmt.Println("Example: ./utmstack_agent load-tls-certs /path/to/server.crt /path/to/server.key /path/to/ca.crt") + os.Exit(1) + } + + userCertPath := os.Args[2] + userKeyPath := os.Args[3] + var userCAPath string + if len(os.Args) > 4 { + userCAPath = os.Args[4] + } + + fmt.Println("Loading user TLS certificates ...") + + fmt.Print("Validating certificate files ... ") + if err := utils.ValidateIntegrationCertificates(userCertPath, userKeyPath); err != nil { + fmt.Printf("\nError: Invalid certificate files: %v\n", err) + os.Exit(1) + } + fmt.Println("[OK]") + + fmt.Print("Installing certificates ... ") + src := utils.CertificateFiles{ + CertPath: userCertPath, + KeyPath: userKeyPath, + CAPath: userCAPath, + } + dest := utils.CertificateFiles{ + CertPath: config.IntegrationCertPath, + KeyPath: config.IntegrationKeyPath, + CAPath: config.IntegrationCAPath, + } + if err := utils.LoadUserCertificatesWithStruct(src, dest); err != nil { + fmt.Printf("\nError loading certificates: %v\n", err) + os.Exit(1) + } + fmt.Println("[OK]") + + fmt.Println("TLS certificates loaded successfully!") time.Sleep(5 * time.Second) case "change-port": @@ -179,11 +247,13 @@ func Help() { fmt.Println("Usage:") fmt.Println(" To run the service: ./utmstack_agent run") fmt.Println(" To install the service: ./utmstack_agent install") - fmt.Println(" To enable integration: ./utmstack_agent enable-integration ") + fmt.Println(" To enable integration: ./utmstack_agent enable-integration [--tls]") fmt.Println(" To disable integration: ./utmstack_agent disable-integration ") fmt.Println(" To change integration port: ./utmstack_agent change-port ") fmt.Println(" To change log retention: ./utmstack_agent change-retention ") fmt.Println(" To clean old logs: ./utmstack_agent clean-logs") + fmt.Println(" To load user TLS certificates: ./utmstack_agent load-tls-certs [ca]") + fmt.Println(" To check TLS certificates: ./utmstack_agent check-tls-certs") fmt.Println(" To uninstall the service: ./utmstack_agent uninstall") fmt.Println(" For help (this message): ./utmstack_agent help") fmt.Println() @@ -191,13 +261,27 @@ func Help() { fmt.Println(" run Run the UTMStackAgent service") fmt.Println(" install Install the UTMStackAgent service") fmt.Println(" enable-integration Enable integration for a specific and ") - fmt.Println(" disable-integration Disable integration for a specific and ") + fmt.Println(" Available flag: --tls (enable TLS for TCP only)") + fmt.Println(" disable-integration Disable integration for a specific and (auto-disables TLS)") fmt.Println(" change-port Change the port for a specific and to ") fmt.Println(" change-retention Change the log retention to . Retention must be a number of megabytes. Example: 20") fmt.Println(" clean-logs Clean old logs from the database") + fmt.Println(" load-tls-certs Load your own TLS certificates (RECOMMENDED for production)") + fmt.Println(" check-tls-certs Check status and validity of TLS certificates") fmt.Println(" uninstall Uninstall the UTMStackAgent service") fmt.Println(" help Display this help message") fmt.Println() + fmt.Println("TLS Certificate Management:") + fmt.Println(" # Load your own certificates (RECOMMENDED)") + fmt.Println(" ./utmstack_agent load-tls-certs /path/to/server.crt /path/to/server.key /path/to/ca.crt") + fmt.Println(" ./utmstack_agent load-tls-certs /path/to/server.crt /path/to/server.key # Without CA") + fmt.Println() + fmt.Println("TLS Integration Examples:") + fmt.Println(" ./utmstack_agent enable-integration syslog tcp --tls # Enable with TLS") + fmt.Println(" ./utmstack_agent enable-integration syslog tcp # Enable without TLS (default)") + fmt.Println(" ./utmstack_agent disable-integration syslog tcp # Disable (auto-disables TLS)") + fmt.Println(" ./utmstack_agent check-tls-certs # Check certificate status") + fmt.Println() fmt.Println("Note:") fmt.Println(" - Make sure to run commands with appropriate permissions.") fmt.Println(" - All commands require administrative privileges.") diff --git a/agent/modules/configuration.go b/agent/modules/configuration.go index 330ecaa54..dd5bf90e6 100644 --- a/agent/modules/configuration.go +++ b/agent/modules/configuration.go @@ -5,14 +5,16 @@ import ( "net" "os" "strings" + "time" "github.com/utmstack/UTMStack/agent/config" "github.com/utmstack/UTMStack/agent/utils" ) type Port struct { - IsListen bool `json:"enabled"` - Port string `json:"value"` + IsListen bool `json:"enabled"` + Port string `json:"value"` + TLSEnabled bool `json:"tls_enabled,omitempty"` } type Integration struct { @@ -47,7 +49,7 @@ func ConfigureCollectorFirstTime() error { return WriteCollectorConfig(integrations, config.CollectorFileName) } -func ChangeIntegrationStatus(logTyp string, proto string, isEnabled bool) (string, error) { +func ChangeIntegrationStatus(logTyp string, proto string, isEnabled bool, tlsOptions ...bool) (string, error) { var port string cnf, err := ReadCollectorConfig() if err != nil { @@ -63,9 +65,53 @@ func ChangeIntegrationStatus(logTyp string, proto string, isEnabled bool) (strin case "tcp": integration.TCP.IsListen = isEnabled port = integration.TCP.Port + + // Handle TLS configuration if specified + if len(tlsOptions) > 0 && isEnabled { + if tlsOptions[0] { + if !utils.CheckIfPathExist(config.IntegrationCertPath) || !utils.CheckIfPathExist(config.IntegrationKeyPath) { + return "", fmt.Errorf("TLS certificates not found. Please load certificates first") + } + // Enable TLS + integration.TCP.TLSEnabled = true + mod := GetModule(logTyp) + if mod != nil && mod.IsPortListen(proto) { + mod.DisablePort(proto) + time.Sleep(100 * time.Millisecond) + err := mod.EnablePort(proto, true) + if err != nil { + return "", fmt.Errorf("error enabling TLS on running module: %v", err) + } + } + } else { + // Disable TLS + integration.TCP.TLSEnabled = false + mod := GetModule(logTyp) + if mod != nil && mod.IsPortListen(proto) { + mod.DisablePort(proto) + time.Sleep(100 * time.Millisecond) + err := mod.EnablePort(proto, false) + if err != nil { + return "", fmt.Errorf("error disabling TLS on running module: %v", err) + } + } + } + } + + // Auto-disable TLS when disabling integration + if !isEnabled { + integration.TCP.TLSEnabled = false + } + case "udp": integration.UDP.IsListen = isEnabled port = integration.UDP.Port + + // TLS validation for UDP + if len(tlsOptions) > 0 && tlsOptions[0] { + return "", fmt.Errorf("TLS is not supported for UDP protocol. Use TCP for TLS connections") + } + default: return "", fmt.Errorf("invalid protocol: %s", proto) } @@ -133,7 +179,11 @@ func WriteCollectorConfig(integrations map[string]Integration, filename string) for name, integration := range integrations { fileContent += fmt.Sprintf(" \"%s\": {\n", name) if integration.TCP.Port != "" { - fileContent += fmt.Sprintf(" \"tcp_port\": {\"enabled\": %t, \"value\": \"%s\"},\n", integration.TCP.IsListen, integration.TCP.Port) + fileContent += fmt.Sprintf(" \"tcp_port\": {\"enabled\": %t, \"value\": \"%s\"", integration.TCP.IsListen, integration.TCP.Port) + if integration.TCP.TLSEnabled { + fileContent += fmt.Sprintf(", \"tls_enabled\": %t", integration.TCP.TLSEnabled) + } + fileContent += "},\n" } if integration.UDP.Port != "" { fileContent += fmt.Sprintf(" \"udp_port\": {\"enabled\": %t, \"value\": \"%s\"},\n", integration.UDP.IsListen, integration.UDP.Port) @@ -173,3 +223,73 @@ func WriteCollectorConfigFromModules(mod []Module, filename string) error { } return WriteCollectorConfig(integrations, filename) } + +func EnableTLSForIntegration(logTyp string, proto string) (string, error) { + cnf, err := ReadCollectorConfig() + if err != nil { + return "", fmt.Errorf("error reading collector config: %v", err) + } + + if valid := config.ValidateModuleType(logTyp); valid == "nil" { + return "", fmt.Errorf("invalid integration: %s", logTyp) + } + + integration := cnf.Integrations[logTyp] + var port string + + switch proto { + case "tcp": + if integration.TCP.Port == "" { + return "", fmt.Errorf("TCP port not configured for %s", logTyp) + } + port = integration.TCP.Port + integration.TCP.TLSEnabled = true + + mod := GetModule(logTyp) + if mod != nil && mod.IsPortListen(proto) { + mod.DisablePort(proto) + time.Sleep(100 * time.Millisecond) + err := mod.EnablePort(proto, true) + if err != nil { + return port, fmt.Errorf("error enabling TLS on running module: %v", err) + } + } + case "udp": + return "", fmt.Errorf("TLS not supported for UDP protocol") + default: + return "", fmt.Errorf("invalid protocol: %s", proto) + } + + cnf.Integrations[logTyp] = integration + return port, WriteCollectorConfig(cnf.Integrations, config.CollectorFileName) +} + +func DisableTLSForIntegration(logTyp string, proto string) error { + cnf, err := ReadCollectorConfig() + if err != nil { + return fmt.Errorf("error reading collector config: %v", err) + } + + integration := cnf.Integrations[logTyp] + switch proto { + case "tcp": + integration.TCP.TLSEnabled = false + + mod := GetModule(logTyp) + if mod != nil && mod.IsPortListen(proto) { + mod.DisablePort(proto) + time.Sleep(100 * time.Millisecond) + err := mod.EnablePort(proto, false) + if err != nil { + return fmt.Errorf("error disabling TLS on running module: %v", err) + } + } + case "udp": + return fmt.Errorf("TLS not supported for UDP protocol") + default: + return fmt.Errorf("invalid protocol: %s", proto) + } + + cnf.Integrations[logTyp] = integration + return WriteCollectorConfig(cnf.Integrations, config.CollectorFileName) +} diff --git a/agent/modules/modules.go b/agent/modules/modules.go index 4d54bab41..aa8424d58 100644 --- a/agent/modules/modules.go +++ b/agent/modules/modules.go @@ -20,7 +20,7 @@ type Module interface { IsPortListen(proto string) bool SetNewPort(proto string, port string) GetPort(proto string) string - EnablePort(proto string) + EnablePort(proto string, enableTLS bool) error DisablePort(proto string) } @@ -88,7 +88,12 @@ func StartModules() { if changeAllowed { moCache[index].SetNewPort(proto, port) if conf[1] { - moCache[index].EnablePort(proto) + enableTLS := proto == "tcp" && cnf.TCP.TLSEnabled + + err := moCache[index].EnablePort(proto, enableTLS) + if err != nil { + utils.Logger.ErrorF("error enabling port for %s %s: %v", intType, proto, err) + } } } else { utils.Logger.Info("change in port %s protocol %s not allowed for %s or out range %s-%s", port, proto, intType, config.PortRangeMin, config.PortRangeMax) diff --git a/agent/modules/netflow.go b/agent/modules/netflow.go index 791da8b22..ed245aaae 100644 --- a/agent/modules/netflow.go +++ b/agent/modules/netflow.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" "net" "strconv" "sync" @@ -44,7 +45,11 @@ func GetNetflowModule() *NetflowModule { return netflowModule } -func (m *NetflowModule) EnablePort(proto string) { +func (m *NetflowModule) EnablePort(proto string, enableTLS bool) error { + if enableTLS { + return fmt.Errorf("TLS not supported for NetFlow protocol") + } + if proto == "udp" && !m.IsEnabled { utils.Logger.Info("Server %s listening in port: %s protocol: UDP", m.DataType, config.ProtoPorts[config.DataTypeNetflow].UDP) m.IsEnabled = true @@ -52,7 +57,7 @@ func (m *NetflowModule) EnablePort(proto string) { port, err := strconv.Atoi(config.ProtoPorts[config.DataTypeNetflow].UDP) if err != nil { utils.Logger.ErrorF("error converting port to int: %v", err) - return + return err } m.Listener, err = net.ListenUDP("udp", &net.UDPAddr{ @@ -61,7 +66,7 @@ func (m *NetflowModule) EnablePort(proto string) { }) if err != nil { utils.Logger.ErrorF("error listening netflow: %v", err) - return + return err } m.CTX, m.Cancel = context.WithCancel(context.Background()) @@ -115,7 +120,9 @@ func (m *NetflowModule) EnablePort(proto string) { } } }() + return nil } + return fmt.Errorf("NetFlow only supports UDP protocol") } func (m *NetflowModule) DisablePort(proto string) { diff --git a/agent/modules/syslog.go b/agent/modules/syslog.go index 10e9d38bf..7915d95c3 100644 --- a/agent/modules/syslog.go +++ b/agent/modules/syslog.go @@ -3,10 +3,13 @@ package modules import ( "bufio" "context" + "crypto/tls" "errors" + "fmt" "io" "net" "os" + "strconv" "strings" "time" @@ -18,6 +21,20 @@ import ( "github.com/utmstack/UTMStack/agent/utils" ) +const ( + MinBufferSize = 480 + RecommendedBufferSize = 2048 + MaxBufferSize = 8192 + UDPBufferSize = 2048 +) + +type FramingMethod int + +const ( + FramingNewline FramingMethod = iota + FramingOctetCounting +) + type SyslogModule struct { DataType string TCPListener listenerTCP @@ -26,11 +43,12 @@ type SyslogModule struct { } type listenerTCP struct { - Listener net.Listener - CTX context.Context - Cancel context.CancelFunc - IsEnabled bool - Port string + Listener net.Listener + CTX context.Context + Cancel context.CancelFunc + IsEnabled bool + Port string + TLSEnabled bool } type listenerUDP struct { @@ -92,12 +110,26 @@ func (m *SyslogModule) GetPort(proto string) string { } } -func (m *SyslogModule) EnablePort(proto string) { +func (m *SyslogModule) EnablePort(proto string, enableTLS bool) error { switch proto { case "tcp": + if enableTLS { + if !utils.CheckIfPathExist(config.IntegrationCertPath) || !utils.CheckIfPathExist(config.IntegrationKeyPath) { + return fmt.Errorf("TLS certificates not found. Please load certificates first") + } + } + + m.TCPListener.TLSEnabled = enableTLS go m.enableTCP() + return nil case "udp": + if enableTLS { + return fmt.Errorf("TLS not supported for UDP protocol") + } go m.enableUDP() + return nil + default: + return fmt.Errorf("unsupported protocol: %s", proto) } } @@ -113,17 +145,14 @@ func (m *SyslogModule) DisablePort(proto string) { func (m *SyslogModule) enableTCP() { if !m.TCPListener.IsEnabled && m.TCPListener.Port != "" { utils.Logger.Info("Server %s listening in port: %s protocol: TCP", m.DataType, m.TCPListener.Port) + if m.TCPListener.TLSEnabled { + utils.Logger.Info("Server %s TLS enabled in port: %s protocol: TCP", m.DataType, m.TCPListener.Port) + } m.TCPListener.IsEnabled = true - listener, err := net.Listen("tcp", "0.0.0.0"+":"+m.TCPListener.Port) + listener, err := net.Listen("tcp", "0.0.0.0:"+m.TCPListener.Port) if err != nil { - utils.Logger.ErrorF("error listening TCp in port %s: %v", m.TCPListener.Port, err) - return - } - - tcpListener, ok := listener.(*net.TCPListener) - if !ok { - utils.Logger.ErrorF("could not assert to *net.TCPListener") + utils.Logger.ErrorF("error listening TCP in port %s: %v", m.TCPListener.Port, err) return } @@ -142,7 +171,6 @@ func (m *SyslogModule) enableTCP() { case <-m.TCPListener.CTX.Done(): return default: - tcpListener.SetDeadline(time.Now().Add(1 * time.Second)) conn, err := m.TCPListener.Listener.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { @@ -158,11 +186,16 @@ func (m *SyslogModule) enableTCP() { utils.Logger.ErrorF("error connecting with tcp listener: %v", err) continue } - go m.handleConnectionTCP(conn) + + // Connection handling based on TLS configuration + if m.TCPListener.TLSEnabled { + go m.handleTLSConnection(conn) + } else { + go m.handleConnectionTCP(conn) + } } } }() - select {} } } @@ -186,7 +219,7 @@ func (m *SyslogModule) enableUDP() { m.UDPListener.Listener = listener m.UDPListener.CTX, m.UDPListener.Cancel = context.WithCancel(context.Background()) - buffer := make([]byte, 1024) + buffer := make([]byte, UDPBufferSize) msgChannel := make(chan config.MSGDS) go m.handleConnectionUDP(msgChannel) @@ -246,6 +279,13 @@ func (m *SyslogModule) enableUDP() { func (m *SyslogModule) disableTCP() { if m.TCPListener.IsEnabled && m.TCPListener.Port != "" { utils.Logger.Info("Server %s closed in port: %s protocol: TCP", m.DataType, m.TCPListener.Port) + + if m.TCPListener.Listener != nil { + if err := m.TCPListener.Listener.Close(); err != nil { + utils.Logger.ErrorF("error closing TCP listener: %v", err) + } + } + m.TCPListener.Cancel() m.TCPListener.IsEnabled = false } @@ -254,11 +294,100 @@ func (m *SyslogModule) disableTCP() { func (m *SyslogModule) disableUDP() { if m.UDPListener.IsEnabled && m.UDPListener.Port != "" { utils.Logger.Info("Server %s closed in port: %s protocol: UDP", m.DataType, m.UDPListener.Port) + + if m.UDPListener.Listener != nil { + if err := m.UDPListener.Listener.Close(); err != nil { + utils.Logger.ErrorF("error closing UDP listener: %v", err) + } + } + m.UDPListener.Cancel() m.UDPListener.IsEnabled = false } } +// detectFramingMethod detects the syslog framing method by peeking at the first byte +func detectFramingMethod(reader *bufio.Reader) (FramingMethod, error) { + firstByte, err := reader.Peek(1) + if err != nil { + utils.Logger.ErrorF("failed to peek first byte for framing detection: %v", err) + return 0, fmt.Errorf("failed to peek first byte: %w", err) + } + + if firstByte[0] >= '0' && firstByte[0] <= '9' { + return FramingOctetCounting, nil + } + + if firstByte[0] == '<' { + return FramingNewline, nil + } + + utils.Logger.ErrorF("unknown framing method detected, first byte: 0x%02x", firstByte[0]) + return 0, fmt.Errorf("unknown framing method, first byte: 0x%02x", firstByte[0]) +} + +// readOctetCountingFrame reads a syslog message using octet counting framing method +func readOctetCountingFrame(reader *bufio.Reader) (string, error) { + lengthStr, err := reader.ReadString(' ') + if err != nil { + utils.Logger.ErrorF("failed to read message length in octet counting frame: %v", err) + return "", fmt.Errorf("failed to read message length: %w", err) + } + + lengthStr = strings.TrimSuffix(lengthStr, " ") + msgLen, err := strconv.Atoi(lengthStr) + if err != nil { + utils.Logger.ErrorF("invalid message length '%s' in octet counting frame: %v", lengthStr, err) + return "", fmt.Errorf("invalid message length '%s': %w", lengthStr, err) + } + + if msgLen < 1 { + utils.Logger.ErrorF("message length %d is too small (minimum 1 byte)", msgLen) + return "", fmt.Errorf("message length %d is too small (minimum 1)", msgLen) + } + if msgLen > MaxBufferSize { + utils.Logger.ErrorF("message length %d exceeds maximum %d bytes", msgLen, MaxBufferSize) + return "", fmt.Errorf("message length %d exceeds maximum %d", msgLen, MaxBufferSize) + } + + msgBytes := make([]byte, msgLen) + _, err = io.ReadFull(reader, msgBytes) + if err != nil { + utils.Logger.ErrorF("failed to read %d byte message body: %v", msgLen, err) + return "", fmt.Errorf("failed to read %d byte message body: %w", msgLen, err) + } + + return string(msgBytes), nil +} + +// readNewlineFrame reads a syslog message using newline-delimited framing method +func readNewlineFrame(reader *bufio.Reader) (string, error) { + message, err := reader.ReadString('\n') + if err != nil { + utils.Logger.ErrorF("failed to read newline-delimited message: %v", err) + return "", fmt.Errorf("failed to read newline-delimited message: %w", err) + } + return message, nil +} + +// readSyslogMessage reads a syslog message with automatic framing detection +func readSyslogMessage(reader *bufio.Reader) (string, error) { + method, err := detectFramingMethod(reader) + if err != nil { + return "", err + } + + switch method { + case FramingOctetCounting: + return readOctetCountingFrame(reader) + case FramingNewline: + return readNewlineFrame(reader) + default: + utils.Logger.ErrorF("unsupported framing method: %d", method) + return "", fmt.Errorf("unsupported framing method: %d", method) + } +} + func (m *SyslogModule) handleConnectionTCP(c net.Conn) { defer c.Close() reader := bufio.NewReader(c) @@ -277,6 +406,90 @@ func (m *SyslogModule) handleConnectionTCP(c net.Conn) { } } + // Detect and reject TLS connections when TLS is disabled + c.SetReadDeadline(time.Now().Add(5 * time.Second)) + firstBytes := make([]byte, 3) + n, err := reader.Read(firstBytes) + if err != nil { + utils.Logger.ErrorF("error reading initial bytes from %s: %v", remoteAddr, err) + return + } + + // TLS handshake starts with: 0x16 (22 decimal) for TLS 1.0-1.3 + if n >= 1 && firstBytes[0] == 0x16 { + utils.Logger.ErrorF("TLS connection rejected from %s: TLS is disabled, only plain text connections accepted", remoteAddr) + return + } + + // Reset deadline and create a new reader that includes the read bytes + c.SetReadDeadline(time.Time{}) + reader = bufio.NewReader(io.MultiReader(strings.NewReader(string(firstBytes[:n])), reader)) + + msgChannel := make(chan config.MSGDS) + go m.handleMessageTCP(msgChannel) + + for { + select { + case <-m.TCPListener.CTX.Done(): + return + default: + message, err := readSyslogMessage(reader) + if err != nil { + if err == io.EOF { + utils.Logger.Info("TCP connection closed by %s", remoteAddr) + return + } + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + utils.Logger.Info("TCP connection timeout from %s", remoteAddr) + return + } + utils.Logger.ErrorF("error reading syslog message from %s: %v", remoteAddr, err) + return + } + msgChannel <- config.MSGDS{ + DataSource: remoteAddr, + Message: message, + } + } + } +} + +func (m *SyslogModule) handleTLSConnection(conn net.Conn) { + defer conn.Close() + + remoteAddr := conn.RemoteAddr().String() + remoteAddr, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + utils.Logger.ErrorF("error splitting host and port: %v", err) + remoteAddr = "unknown" + } + + if remoteAddr == "127.0.0.1" { + if hostname, err := os.Hostname(); err == nil { + remoteAddr = hostname + } + } + + tlsConfig, err := utils.LoadIntegrationTLSConfig( + config.IntegrationCertPath, + config.IntegrationKeyPath, + ) + if err != nil { + utils.Logger.ErrorF("error loading TLS config: %v", err) + return + } + + tlsConn := tls.Server(conn, tlsConfig) + + conn.SetDeadline(time.Now().Add(10 * time.Second)) + if err := tlsConn.Handshake(); err != nil { + utils.Logger.ErrorF("TLS handshake failed from %s: %v", remoteAddr, err) + return + } + // Keep a reasonable read timeout instead of removing it entirely + conn.SetDeadline(time.Now().Add(30 * time.Second)) + + reader := bufio.NewReader(tlsConn) msgChannel := make(chan config.MSGDS) go m.handleMessageTCP(msgChannel) @@ -285,12 +498,19 @@ func (m *SyslogModule) handleConnectionTCP(c net.Conn) { case <-m.TCPListener.CTX.Done(): return default: - message, err := reader.ReadString('\n') + // Set read timeout for each message + conn.SetDeadline(time.Now().Add(30 * time.Second)) + message, err := readSyslogMessage(reader) if err != nil { - if err == io.EOF || err.(net.Error).Timeout() { + if err == io.EOF { + utils.Logger.Info("TLS connection closed by %s", remoteAddr) + return + } + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + utils.Logger.Info("TLS connection timeout from %s", remoteAddr) return } - utils.Logger.ErrorF("error reading tcp data: %v", err) + utils.Logger.ErrorF("error reading TLS data from %s: %v", remoteAddr, err) return } msgChannel <- config.MSGDS{ diff --git a/agent/utils/files.go b/agent/utils/files.go index 977b5689c..51da99255 100644 --- a/agent/utils/files.go +++ b/agent/utils/files.go @@ -157,3 +157,20 @@ func IsDirEmpty(path string) (bool, error) { } return false, err } + +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + return err +} diff --git a/agent/utils/int_tls.go b/agent/utils/int_tls.go new file mode 100644 index 000000000..5ca5928f3 --- /dev/null +++ b/agent/utils/int_tls.go @@ -0,0 +1,169 @@ +package utils + +import ( + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "fmt" + "os" + "path/filepath" + "time" +) + +const ( + CertFilePermissions = 0644 + KeyFilePermissions = 0600 + MinTLSVersion = tls.VersionTLS12 + MaxTLSVersion = tls.VersionTLS13 +) + +type TLSStatus struct { + Available bool `json:"available"` + CertExists bool `json:"cert_exists"` + KeyExists bool `json:"key_exists"` + CAExists bool `json:"ca_exists"` + Valid bool `json:"valid"` + Error string `json:"error,omitempty"` +} + +type CertificateFiles struct { + CertPath string + KeyPath string + CAPath string +} + +func LoadIntegrationTLSConfig(certPath, keyPath string) (*tls.Config, error) { + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, fmt.Errorf("error loading TLS certificate: %w", err) + } + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: MinTLSVersion, + MaxVersion: MaxTLSVersion, + CipherSuites: []uint16{ + // TLS 1.2 secure cipher suites - RSA key exchange + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + // TLS 1.2 secure cipher suites - ECDSA key exchange (for ECDSA certificates) + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + }, + CurvePreferences: []tls.CurveID{ + tls.X25519, // Modern and fast + tls.CurveP256, // NIST P-256 + tls.CurveP384, // NIST P-384 + tls.CurveP521, // NIST P-521 + }, + PreferServerCipherSuites: true, + }, nil +} + +func ValidateIntegrationCertificates(certPath, keyPath string) error { + if !CheckIfPathExist(certPath) { + return fmt.Errorf("certificate file not found: %s", certPath) + } + + if !CheckIfPathExist(keyPath) { + return fmt.Errorf("private key file not found: %s", keyPath) + } + + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return fmt.Errorf("invalid certificate or private key: %w", err) + } + + x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + return fmt.Errorf("error parsing certificate: %w", err) + } + + // 1. Check validity dates + now := time.Now() + if now.Before(x509Cert.NotBefore) { + return fmt.Errorf("certificate is not yet valid (valid from: %s)", + x509Cert.NotBefore.Format("2006-01-02 15:04:05 UTC")) + } + + if now.After(x509Cert.NotAfter) { + return fmt.Errorf("certificate has expired (valid until: %s)", + x509Cert.NotAfter.Format("2006-01-02 15:04:05 UTC")) + } + + // 2. Warn if the certificate expires soon (30 days) + if now.Add(30 * 24 * time.Hour).After(x509Cert.NotAfter) { + fmt.Printf("WARNING: Certificate expires soon (%s)\n", + x509Cert.NotAfter.Format("2006-01-02 15:04:05 UTC")) + } + + // 3. Check signature algorithm (reject weak algorithms) + switch x509Cert.SignatureAlgorithm { + case x509.SHA1WithRSA, x509.MD5WithRSA: + return fmt.Errorf("certificate uses weak signature algorithm: %s (use SHA256+ instead)", + x509Cert.SignatureAlgorithm) + } + + // 4. Check RSA key size (minimum 2048 bits) + if x509Cert.PublicKeyAlgorithm == x509.RSA { + if rsaKey, ok := x509Cert.PublicKey.(*rsa.PublicKey); ok { + keySize := rsaKey.Size() * 8 // Convert bytes to bits + if keySize < 2048 { + return fmt.Errorf("RSA key size too small: %d bits (minimum 2048 bits required)", keySize) + } + } + } + + return nil +} + +func LoadUserCertificatesWithStruct(src, dest CertificateFiles) error { + // Validate source certificates + if !CheckIfPathExist(src.CertPath) { + return fmt.Errorf("user certificate file not found: %s", src.CertPath) + } + if !CheckIfPathExist(src.KeyPath) { + return fmt.Errorf("user private key file not found: %s", src.KeyPath) + } + if err := ValidateIntegrationCertificates(src.CertPath, src.KeyPath); err != nil { + return err + } + + // Prepare destination directory + certsDir := filepath.Dir(dest.CertPath) + if err := CreatePathIfNotExist(certsDir); err != nil { + return fmt.Errorf("error creating certificates directory: %w", err) + } + + // Copy certificate files + if err := copyFile(src.CertPath, dest.CertPath); err != nil { + return fmt.Errorf("error copying certificate: %w", err) + } + if err := copyFile(src.KeyPath, dest.KeyPath); err != nil { + return fmt.Errorf("error copying private key: %w", err) + } + + // Copy CA certificate (use source CA if exists, otherwise use cert as CA) + caSource := src.CAPath + if caSource == "" || !CheckIfPathExist(caSource) { + caSource = src.CertPath + } + if err := copyFile(caSource, dest.CAPath); err != nil { + return fmt.Errorf("error copying CA certificate: %w", err) + } + + // Set file permissions + if err := os.Chmod(dest.CertPath, CertFilePermissions); err != nil { + return fmt.Errorf("error setting certificate permissions: %w", err) + } + if err := os.Chmod(dest.KeyPath, KeyFilePermissions); err != nil { + return fmt.Errorf("error setting private key permissions: %w", err) + } + if err := os.Chmod(dest.CAPath, CertFilePermissions); err != nil { + return fmt.Errorf("error setting CA permissions: %w", err) + } + + return nil +} diff --git a/backend/.gitignore b/backend/.gitignore index 834dd8821..518edd788 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -62,6 +62,7 @@ local.properties *.orig classes/ out/ +.nvim/ ###################### # Visual Studio Code diff --git a/backend/Dockerfile b/backend/Dockerfile index 3947d9d0b..b1e8eb732 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM eclipse-temurin:11 +FROM eclipse-temurin:17 ADD target/utmstack.war ./ diff --git a/backend/pom.xml b/backend/pom.xml index abb29ba28..f7936229e 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -14,7 +14,7 @@ 3.3.9 - 11 + 17 UTF-8 UTF-8 yyyyMMddHHmmss @@ -33,7 +33,7 @@ 7.3.1 - 2.5.5 + 3.1.5 5.4.32.Final @@ -192,6 +192,10 @@ org.springframework.boot spring-boot-starter-mail + + org.springframework.boot + spring-boot-starter-oauth2-client + org.springframework.boot spring-boot-starter-security @@ -273,7 +277,7 @@ org.springdoc springdoc-openapi-ui - 1.6.7 + 1.6.15 com.utmstack @@ -333,7 +337,7 @@ com.utmstack.grpc.jclient collector-client-4j - 1.2.12 + 1.2.13 @@ -376,6 +380,12 @@ 3.0.5 + + commons-net + commons-net + 3.9.0 + + diff --git a/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java new file mode 100644 index 000000000..b84c2ea14 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/advice/GlobalExceptionHandler.java @@ -0,0 +1,67 @@ +package com.park.utmstack.advice; + + +import com.park.utmstack.security.TooMuchLoginAttemptsException; +import com.park.utmstack.service.application_events.ApplicationEventService; +import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.exceptions.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; +import java.util.NoSuchElementException; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class GlobalExceptionHandler { + + private final ApplicationEventService applicationEventService; + + @ExceptionHandler(TfaVerificationException.class) + public ResponseEntity TfaVerificationException(TfaVerificationException e, HttpServletRequest request) { + return ResponseUtil.buildErrorResponse(HttpStatus.PRECONDITION_FAILED, e.getMessage()); + } + + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity handleForbidden(BadCredentialsException e, HttpServletRequest request) { + return ResponseUtil.buildUnauthorizedResponse(e.getMessage()); + } + + @ExceptionHandler(TooMuchLoginAttemptsException.class) + public ResponseEntity handleTooManyLoginAttempts(TooMuchLoginAttemptsException e, HttpServletRequest request) { + return ResponseUtil.buildLockedResponse(e.getMessage()); + } + + @ExceptionHandler({NoSuchElementException.class, + ApiKeyNotFoundException.class}) + public ResponseEntity handleNotFound(Exception e, HttpServletRequest request) { + return ResponseUtil.buildNotFoundResponse(e.getMessage()); + } + + @ExceptionHandler(TooManyRequestsException.class) + public ResponseEntity handleTooManyRequests(TooManyRequestsException e, HttpServletRequest request) { + return ResponseUtil.buildErrorResponse(HttpStatus.TOO_MANY_REQUESTS, e.getMessage()); + } + + @ExceptionHandler({NoAlertsProvidedException.class}) + public ResponseEntity handleNoAlertsProvided(Exception e, HttpServletRequest request) { + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage()); + } + + @ExceptionHandler({IncidentAlertConflictException.class, + ApiKeyExistException.class}) + public ResponseEntity handleConflict(Exception e, HttpServletRequest request) { + return ResponseUtil.buildErrorResponse(HttpStatus.CONFLICT, e.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException(Exception e, HttpServletRequest request) { + return ResponseUtil.buildInternalServerErrorResponse(e.getMessage()); + } +} diff --git a/backend/src/main/java/com/park/utmstack/aop/logging/AuditEvent.java b/backend/src/main/java/com/park/utmstack/aop/logging/AuditEvent.java new file mode 100644 index 000000000..133077af4 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/aop/logging/AuditEvent.java @@ -0,0 +1,19 @@ +package com.park.utmstack.aop.logging; + +import com.park.utmstack.domain.application_events.enums.ApplicationEventType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuditEvent { + ApplicationEventType attemptType(); + String attemptMessage(); + + ApplicationEventType successType(); + String successMessage(); +} + diff --git a/backend/src/main/java/com/park/utmstack/aop/logging/Loggable.java b/backend/src/main/java/com/park/utmstack/aop/logging/Loggable.java new file mode 100644 index 000000000..01bfe5054 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/aop/logging/Loggable.java @@ -0,0 +1,13 @@ +package com.park.utmstack.aop.logging; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Loggable { +} + + diff --git a/backend/src/main/java/com/park/utmstack/aop/logging/NoLogException.java b/backend/src/main/java/com/park/utmstack/aop/logging/NoLogException.java new file mode 100644 index 000000000..4325d84e0 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/aop/logging/NoLogException.java @@ -0,0 +1,11 @@ +package com.park.utmstack.aop.logging; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface NoLogException {} + diff --git a/backend/src/main/java/com/park/utmstack/aop/logging/AlertLoggingAspect.java b/backend/src/main/java/com/park/utmstack/aop/logging/impl/AlertLoggingAspect.java similarity index 92% rename from backend/src/main/java/com/park/utmstack/aop/logging/AlertLoggingAspect.java rename to backend/src/main/java/com/park/utmstack/aop/logging/impl/AlertLoggingAspect.java index aac5188e4..405710b18 100644 --- a/backend/src/main/java/com/park/utmstack/aop/logging/AlertLoggingAspect.java +++ b/backend/src/main/java/com/park/utmstack/aop/logging/impl/AlertLoggingAspect.java @@ -1,11 +1,11 @@ -package com.park.utmstack.aop.logging; +package com.park.utmstack.aop.logging.impl; import com.park.utmstack.config.Constants; import com.park.utmstack.domain.UtmAlertLog; import com.park.utmstack.domain.chart_builder.types.query.FilterType; import com.park.utmstack.domain.chart_builder.types.query.OperatorType; import com.park.utmstack.domain.index_pattern.enums.SystemIndexPattern; -import com.park.utmstack.domain.shared_types.AlertType; +import com.park.utmstack.domain.shared_types.alert.UtmAlert; import com.park.utmstack.security.SecurityUtils; import com.park.utmstack.service.UtmAlertLogService; import com.park.utmstack.service.elasticsearch.ElasticsearchService; @@ -87,14 +87,14 @@ public Object logManualAlertStatusChange(ProceedingJoinPoint joinPoint) throws T Integer status = (Integer) args[1]; String statusObservation = (String) args[2]; - List alerts = getAlerts(alertIds); + List alerts = getAlerts(alertIds); joinPoint.proceed(); if (CollectionUtils.isEmpty(alerts)) return null; - for (AlertType alert : alerts) { + for (UtmAlert alert : alerts) { UtmAlertLog alertLog = new UtmAlertLog(); alertLog.setAlertId(alert.getId()); alertLog.setLogUser(SecurityUtils.getCurrentUserLogin().orElse("system")); @@ -146,19 +146,19 @@ public Object logAutomaticAlertStatusChange(ProceedingJoinPoint joinPoint) throw .size(Constants.LOG_ANALYZER_TOTAL_RESULTS) .query(query)); - HitsMetadata response = elasticsearchService.search(sr, AlertType.class).hits(); + HitsMetadata response = elasticsearchService.search(sr, UtmAlert.class).hits(); joinPoint.proceed(); if (response.total().value() <= 0) return null; - List alerts = response.hits().stream().map(Hit::source).collect(Collectors.toList()); + List alerts = response.hits().stream().map(Hit::source).collect(Collectors.toList()); if (CollectionUtils.isEmpty(alerts)) return null; - for (AlertType alert : alerts) { + for (UtmAlert alert : alerts) { UtmAlertLog alertLog = new UtmAlertLog(); alertLog.setAlertId(alert.getId()); alertLog.setLogUser("system"); @@ -197,7 +197,7 @@ public Object logManualAlertTagsChange(ProceedingJoinPoint joinPoint) throws Thr List alertIds = (List) args[0]; List tags = (List) args[1]; - List alerts = getAlerts(alertIds); + List alerts = getAlerts(alertIds); joinPoint.proceed(); @@ -206,7 +206,7 @@ public Object logManualAlertTagsChange(ProceedingJoinPoint joinPoint) throws Thr String user = SecurityUtils.getCurrentUserLogin().orElse("system"); - for (AlertType alert : alerts) { + for (UtmAlert alert : alerts) { UtmAlertLog alertLog = new UtmAlertLog(); alertLog.setAlertId(alert.getId()); alertLog.setLogUser(user); @@ -247,21 +247,21 @@ public Object logAutomaticAlertTagsChange(ProceedingJoinPoint joinPoint) throws SearchRequest request = SearchRequest.of(s -> s.query(query) .size(Constants.LOG_ANALYZER_TOTAL_RESULTS).index(indexPattern)); - HitsMetadata hits = elasticsearchService.search(request, AlertType.class).hits(); + HitsMetadata hits = elasticsearchService.search(request, UtmAlert.class).hits(); joinPoint.proceed(); if (hits.total().value() <= 0) return null; - List alerts = hits.hits().stream().map(Hit::source).collect(Collectors.toList()); + List alerts = hits.hits().stream().map(Hit::source).collect(Collectors.toList()); if (CollectionUtils.isEmpty(alerts)) return null; String user = SecurityUtils.getCurrentUserLogin().orElse("system"); - for (AlertType alert : alerts) { + for (UtmAlert alert : alerts) { UtmAlertLog alertLog = new UtmAlertLog(); alertLog.setAlertId(alert.getId()); alertLog.setLogUser(user); @@ -294,7 +294,7 @@ public Object logManualAlertNotesChange(ProceedingJoinPoint joinPoint) throws Th String alertId = (String) args[0]; String notes = (String) args[1]; - List alerts = getAlerts(Collections.singletonList(alertId)); + List alerts = getAlerts(Collections.singletonList(alertId)); joinPoint.proceed(); @@ -303,7 +303,7 @@ public Object logManualAlertNotesChange(ProceedingJoinPoint joinPoint) throws Th String user = SecurityUtils.getCurrentUserLogin().orElse("system"); - for (AlertType alert : alerts) { + for (UtmAlert alert : alerts) { UtmAlertLog alertLog = new UtmAlertLog(); alertLog.setAlertId(alert.getId()); alertLog.setLogUser(user); @@ -347,19 +347,19 @@ public Object logConvertToIncident(ProceedingJoinPoint joinPoint) throws Throwab SearchRequest request = SearchRequest.of(s -> s.size(Constants.LOG_ANALYZER_TOTAL_RESULTS) .query(query).index(indexPattern)); - HitsMetadata hits = elasticsearchService.search(request, AlertType.class).hits(); + HitsMetadata hits = elasticsearchService.search(request, UtmAlert.class).hits(); joinPoint.proceed(); if (hits.total().value() <= 0) return null; - List alerts = hits.hits().stream().map(Hit::source).collect(Collectors.toList()); + List alerts = hits.hits().stream().map(Hit::source).collect(Collectors.toList()); if (CollectionUtils.isEmpty(alerts)) return null; - for (AlertType alert : alerts) { + for (UtmAlert alert : alerts) { UtmAlertLog alertLog = new UtmAlertLog(); alertLog.setAlertId(alert.getId()); alertLog.setLogUser(incidentCreatedBy); @@ -393,14 +393,14 @@ public Object logConvertToIncident(ProceedingJoinPoint joinPoint) throws Throwab * @return * @throws Exception */ - private List getAlerts(List ids) throws Exception { + private List getAlerts(List ids) throws Exception { final String ctx = CLASS_NAME + ".getAlerts"; try { List filters = new ArrayList<>(); filters.add(new FilterType(Constants.alertIdKeyword, OperatorType.IS_ONE_OF_TERMS, ids)); SearchRequest request = SearchRequest.of(s -> s.query(SearchUtil.toQuery(filters)) .index(Constants.SYS_INDEX_PATTERN.get(SystemIndexPattern.ALERTS))); - HitsMetadata hits = elasticsearchService.search(request, AlertType.class).hits(); + HitsMetadata hits = elasticsearchService.search(request, UtmAlert.class).hits(); if (hits.total().value() <= 0) return Collections.emptyList(); return hits.hits().stream().map(Hit::source).collect(Collectors.toList()); diff --git a/backend/src/main/java/com/park/utmstack/aop/logging/impl/AuditAspect.java b/backend/src/main/java/com/park/utmstack/aop/logging/impl/AuditAspect.java new file mode 100644 index 000000000..6f4737a05 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/aop/logging/impl/AuditAspect.java @@ -0,0 +1,82 @@ +package com.park.utmstack.aop.logging.impl; + +import com.park.utmstack.aop.logging.AuditEvent; +import com.park.utmstack.aop.logging.NoLogException; +import com.park.utmstack.domain.application_events.enums.ApplicationEventType; +import com.park.utmstack.domain.shared_types.ApplicationLayer; +import com.park.utmstack.loggin.LogContextBuilder; +import com.park.utmstack.service.application_events.ApplicationEventService; +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.logstash.logback.argument.StructuredArguments; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Aspect +@Component +@Slf4j +@RequiredArgsConstructor +public class AuditAspect { + + private final ApplicationEventService applicationEventService; + private final LogContextBuilder logContextBuilder; + + @Around("@annotation(auditEvent)") + public Object logAuditEvent(ProceedingJoinPoint joinPoint, AuditEvent auditEvent) throws Throwable { + return handleAudit(joinPoint, auditEvent.attemptType(), auditEvent.successType(), + auditEvent.attemptMessage(), auditEvent.successMessage()); + } + + private Object handleAudit(ProceedingJoinPoint joinPoint, + ApplicationEventType attemptType, + ApplicationEventType successType, + String attemptMessage, + String successMessage) throws Throwable { + + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String context = signature.getDeclaringType().getSimpleName() + "." + signature.getMethod().getName(); + MDC.put("context", context); + + Map extra = extractAuditData(joinPoint.getArgs()); + extra.put("layer", ApplicationLayer.CONTROLLER.getValue()); + + try { + applicationEventService.createEvent(attemptMessage, attemptType, extra); + + Object result = joinPoint.proceed(); + + if (successType != ApplicationEventType.UNDEFINED) { + applicationEventService.createEvent(successMessage, successType, extra); + } + + return result; + + } catch (Exception e) { + if (!e.getClass().isAnnotationPresent(NoLogException.class)) { + String msg = String.format("%s: %s", context, e.getMessage()); + log.error(msg, e, StructuredArguments.keyValue("args", logContextBuilder.buildArgs(e))); + } + + throw e; + } + } + + private Map extractAuditData(Object[] args) { + Map extra = new HashMap<>(); + for (Object arg : args) { + if (arg instanceof AuditableDTO auditable) { + extra.putAll(auditable.toAuditMap()); + } + } + return extra; + } +} + diff --git a/backend/src/main/java/com/park/utmstack/aop/logging/LoggingAspect.java b/backend/src/main/java/com/park/utmstack/aop/logging/impl/LoggingAspect.java similarity index 95% rename from backend/src/main/java/com/park/utmstack/aop/logging/LoggingAspect.java rename to backend/src/main/java/com/park/utmstack/aop/logging/impl/LoggingAspect.java index dbeabd956..d3bc4747e 100644 --- a/backend/src/main/java/com/park/utmstack/aop/logging/LoggingAspect.java +++ b/backend/src/main/java/com/park/utmstack/aop/logging/impl/LoggingAspect.java @@ -1,4 +1,4 @@ -package com.park.utmstack.aop.logging; +package com.park.utmstack.aop.logging.impl; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; @@ -56,7 +56,7 @@ public void applicationPackagePointcut() { * @param joinPoint join point for advice * @param e exception */ - @AfterThrowing(pointcut = "applicationPackagePointcut() && springBeanPointcut()", throwing = "e") + /*@AfterThrowing(pointcut = "applicationPackagePointcut() && springBeanPointcut()", throwing = "e") public void logAfterThrowing(JoinPoint joinPoint, Throwable e) { if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) { log.error("Exception in {}.{}() with cause = \'{}\' and exception = \'{}\'", joinPoint.getSignature().getDeclaringTypeName(), @@ -66,7 +66,7 @@ public void logAfterThrowing(JoinPoint joinPoint, Throwable e) { log.error("Exception in {}.{}() with cause = {}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), e.getCause() != null ? e.getCause() : "NULL"); } - } + }*/ /** * Advice that logs when a method is entered and exited. diff --git a/backend/src/main/java/com/park/utmstack/aop/logging/impl/LoggingMethodAspect.java b/backend/src/main/java/com/park/utmstack/aop/logging/impl/LoggingMethodAspect.java new file mode 100644 index 000000000..96c7dba37 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/aop/logging/impl/LoggingMethodAspect.java @@ -0,0 +1,39 @@ +package com.park.utmstack.aop.logging.impl; + +import com.park.utmstack.config.Constants; +import com.park.utmstack.loggin.LogContextBuilder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.logstash.logback.argument.StructuredArguments; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class LoggingMethodAspect { + private final LogContextBuilder logContextBuilder; + + @Around("@annotation(com.park.utmstack.aop.logging.Loggable)") + public Object logExecution(ProceedingJoinPoint joinPoint) throws Throwable { + String traceId = MDC.get(Constants.TRACE_ID_KEY); + String methodName = joinPoint.getSignature().toShortString(); + long start = System.currentTimeMillis(); + + try { + Object result = joinPoint.proceed(); + long duration = System.currentTimeMillis() - start; + String msg = String.format("Method %s executed successfully in %sms", methodName, duration); + log.info( msg, StructuredArguments.keyValue("args", logContextBuilder.buildArgs(methodName, String.valueOf(duration)))); + return result; + } catch (Exception ex) { + String msg = String.format("%s Method %s failed: 5s", traceId, methodName); + log.error(msg, ex, StructuredArguments.keyValue("args", logContextBuilder.buildArgs(ex))); + throw ex; + } + } +} diff --git a/backend/src/main/java/com/park/utmstack/config/Constants.java b/backend/src/main/java/com/park/utmstack/config/Constants.java index 888b86e45..a36fe1733 100644 --- a/backend/src/main/java/com/park/utmstack/config/Constants.java +++ b/backend/src/main/java/com/park/utmstack/config/Constants.java @@ -2,7 +2,9 @@ import com.park.utmstack.domain.index_pattern.enums.SystemIndexPattern; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; public final class Constants { @@ -36,8 +38,8 @@ public final class Constants { public static final String PROP_NETWORK_SCAN_API_URL = "utmstack.networkScan.apiUrl"; public static final String PROP_TFA_ENABLE = "utmstack.tfa.enable"; public static final String PROP_TFA_METHOD = "utmstack.tfa.method"; - public static final int EXPIRES_IN_SECONDS = 30; - public static final int INIT_EXPIRES_IN_SECONDS = 300; + public static final int EXPIRES_IN_SECONDS_TOTP = 30; // Google Authenticator + public static final int EXPIRES_IN_SECONDS_EMAIL = 120; // Email OTP public static final String TFA_ISSUER = "UTMStack"; // ---------------------------------------------------------------------------------- @@ -74,6 +76,7 @@ public final class Constants { // - Alert index common fields // ---------------------------------------------------------------------------------- public static final String alertIdKeyword = "id.keyword"; + public static final String alertParentIdKeyword = "parentId.keyword"; public static final String alertStatus = "status"; public static final String alertTags = "tags"; public static final String alertIsIncident = "isIncident"; @@ -136,6 +139,28 @@ public final class Constants { // Defines the index pattern for querying Elasticsearch statistics indexes. // ---------------------------------------------------------------------------------- public static final String STATISTICS_INDEX_PATTERN = "v11-statistics-*"; + public static final String V11_API_ACCESS_LOGS = "v11-api-access-logs-*"; + + // Logging + public static final String TRACE_ID_KEY = "traceId"; + public static final String CONTEXT_KEY = "context"; + public static final String USERNAME_KEY = "username"; + public static final String METHOD_KEY = "method"; + public static final String PATH_KEY = "path"; + public static final String REMOTE_ADDR_KEY = "remoteAddr"; + public static final String DURATION_KEY = "duration"; + public static final String CAUSE_KEY = "cause"; + + public static final String ENV_TFA_ENABLE = "APP_TFA_ENABLED"; + public static final String TFA_EXEMPTION_HEADER = "X-Bypass-TFA"; + + // Configuration data types for moduleGroupConfiguration + + public static final String CONF_TYPE_PASSWORD = "password"; + public static final String CONF_TYPE_FILE = "file"; + + public static final String API_KEY_HEADER = "Utm-Api-Key"; + public static final List API_ENDPOINT_IGNORE = Collections.emptyList(); private Constants() { } diff --git a/backend/src/main/java/com/park/utmstack/config/LoggingAspectConfiguration.java b/backend/src/main/java/com/park/utmstack/config/LoggingAspectConfiguration.java index 2e438501e..49e666ead 100644 --- a/backend/src/main/java/com/park/utmstack/config/LoggingAspectConfiguration.java +++ b/backend/src/main/java/com/park/utmstack/config/LoggingAspectConfiguration.java @@ -1,6 +1,6 @@ package com.park.utmstack.config; -import com.park.utmstack.aop.logging.LoggingAspect; +import com.park.utmstack.aop.logging.impl.LoggingAspect; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; diff --git a/backend/src/main/java/com/park/utmstack/config/LoggingConfiguration.java b/backend/src/main/java/com/park/utmstack/config/LoggingConfiguration.java index 75b9a14d6..03a9aa9cb 100644 --- a/backend/src/main/java/com/park/utmstack/config/LoggingConfiguration.java +++ b/backend/src/main/java/com/park/utmstack/config/LoggingConfiguration.java @@ -3,8 +3,11 @@ import ch.qos.logback.classic.LoggerContext; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.park.utmstack.loggin.filter.MdcCleanupFilter; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import tech.jhipster.config.JHipsterProperties; @@ -45,4 +48,13 @@ public LoggingConfiguration( addContextListener(context, customFields, loggingProperties); } } + + @Bean + public FilterRegistrationBean mdcCleanupFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new MdcCleanupFilter()); + registrationBean.setOrder(Integer.MAX_VALUE); + registrationBean.addUrlPatterns("/*"); + return registrationBean; + } } diff --git a/backend/src/main/java/com/park/utmstack/config/OpenApiConfiguration.java b/backend/src/main/java/com/park/utmstack/config/OpenApiConfiguration.java index 46a30dd02..dd1414d6d 100644 --- a/backend/src/main/java/com/park/utmstack/config/OpenApiConfiguration.java +++ b/backend/src/main/java/com/park/utmstack/config/OpenApiConfiguration.java @@ -23,11 +23,16 @@ public OpenApiConfiguration(InfoEndpoint infoEndpoint) { @Bean public OpenAPI customOpenAPI() { final String securitySchemeBearer = "bearerAuth"; + final String securitySchemeApiInternalKey = "ApiInternalKeyAuth"; final String securitySchemeApiKey = "ApiKeyAuth"; + final String apiTitle = "UTMStack API"; String version = MapUtil.flattenToStringMap(infoEndpoint.info(), true).get("build.version"); return new OpenAPI() - .addSecurityItem(new SecurityRequirement().addList(securitySchemeBearer).addList(securitySchemeApiKey)) + .addSecurityItem(new SecurityRequirement() + .addList(securitySchemeBearer) + .addList(securitySchemeApiInternalKey) + .addList(securitySchemeApiKey)) .components(new Components() .addSecuritySchemes(securitySchemeBearer, new SecurityScheme() @@ -35,9 +40,13 @@ public OpenAPI customOpenAPI() { .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT")) - .addSecuritySchemes(securitySchemeApiKey, new SecurityScheme() + .addSecuritySchemes(securitySchemeApiInternalKey, new SecurityScheme() .name("Utm-Internal-Key") .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER)) + .addSecuritySchemes(securitySchemeApiKey, new SecurityScheme() + .name(Constants.API_KEY_HEADER) + .type(SecurityScheme.Type.APIKEY) .in(SecurityScheme.In.HEADER))) .info(new Info().title(apiTitle).version(version)) .addServersItem(new Server().url("/").description("Default Server URL")); diff --git a/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java b/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java index 0413a950e..13c8081e0 100644 --- a/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java +++ b/backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java @@ -1,10 +1,17 @@ package com.park.utmstack.config; +import com.park.utmstack.repository.UserRepository; import com.park.utmstack.security.AuthoritiesConstants; +import com.park.utmstack.security.api_key.ApiKeyConfigurer; +import com.park.utmstack.security.api_key.ApiKeyFilter; import com.park.utmstack.security.internalApiKey.InternalApiKeyConfigurer; import com.park.utmstack.security.internalApiKey.InternalApiKeyProvider; import com.park.utmstack.security.jwt.JWTConfigurer; import com.park.utmstack.security.jwt.TokenProvider; +import com.park.utmstack.security.oauth.OAuth2LoginFailureHandler; +import lombok.RequiredArgsConstructor; +import com.park.utmstack.security.oauth.OAuth2LoginSuccessHandler; +import com.park.utmstack.security.oauth.CustomOAuth2UserService; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -21,6 +28,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.filter.CorsFilter; import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport; @@ -28,7 +36,9 @@ import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletResponse; + @Configuration +@RequiredArgsConstructor @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) @Import(SecurityProblemSupport.class) @@ -39,17 +49,9 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private final TokenProvider tokenProvider; private final CorsFilter corsFilter; private final InternalApiKeyProvider internalApiKeyProvider; + private final ApiKeyFilter apiKeyFilter; + private final UserRepository userRepository; - public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, - UserDetailsService userDetailsService, - TokenProvider tokenProvider, - CorsFilter corsFilter, InternalApiKeyProvider internalApiKeyProvider) { - this.authenticationManagerBuilder = authenticationManagerBuilder; - this.userDetailsService = userDetailsService; - this.tokenProvider = tokenProvider; - this.corsFilter = corsFilter; - this.internalApiKeyProvider = internalApiKeyProvider; - } @PostConstruct public void init() { @@ -106,9 +108,12 @@ public void configure(HttpSecurity http) throws Exception { .antMatchers("/api/healthcheck").permitAll() .antMatchers("/api/releaseInfo").permitAll() .antMatchers("/api/account/reset-password/init").permitAll() - .antMatchers("/api/account/reset-password/finish").permitAll() + .antMatchers("/api/utm-providers").permitAll() .antMatchers("/api/images/all").permitAll() - .antMatchers("/api/tfa/**").hasAnyAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER, AuthoritiesConstants.ADMIN, AuthoritiesConstants.USER) + .antMatchers("/api/enrollment/**").hasAnyAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER) + .antMatchers("/api/tfa/verify-code").hasAnyAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER, AuthoritiesConstants.USER, AuthoritiesConstants.ADMIN) + .antMatchers("/api/tfa/refresh").hasAnyAuthority(AuthoritiesConstants.PRE_VERIFICATION_USER, AuthoritiesConstants.USER, AuthoritiesConstants.ADMIN) + .antMatchers("/api/tfa/**").hasAnyAuthority(AuthoritiesConstants.ADMIN, AuthoritiesConstants.USER) .antMatchers("/api/utm-incident-jobs").hasAuthority(AuthoritiesConstants.ADMIN) .antMatchers("/api/utm-incident-jobs/**").hasAuthority(AuthoritiesConstants.ADMIN) .antMatchers("/api/utm-incident-variables/**").hasAuthority(AuthoritiesConstants.ADMIN) @@ -120,12 +125,32 @@ public void configure(HttpSecurity http) throws Exception { .antMatchers("/management/info").permitAll() .antMatchers("/management/**").hasAnyAuthority(AuthoritiesConstants.ADMIN, AuthoritiesConstants.USER) .and() + .oauth2Login() + .authorizationEndpoint().baseUri("/oauth2/authorization") + .and() + .redirectionEndpoint().baseUri("/login/oauth2/code/*") + .and() + .userInfoEndpoint() + .oidcUserService(customOAuth2UserService(userRepository)) + .and() + .successHandler(new OAuth2LoginSuccessHandler(tokenProvider)) + .failureHandler(new OAuth2LoginFailureHandler()) + .and() .apply(securityConfigurerAdapterForJwt()) .and() - .apply(securityConfigurerAdapterForInternalApiKey()); + .apply(securityConfigurerAdapterForInternalApiKey()) + .and() + .apply(securityConfigurerAdapterForApiKey()) ; + } + @Bean + public CustomOAuth2UserService customOAuth2UserService(UserRepository userRepository) { + return new CustomOAuth2UserService(userRepository); + } + + private JWTConfigurer securityConfigurerAdapterForJwt() { return new JWTConfigurer(tokenProvider); } @@ -133,4 +158,9 @@ private JWTConfigurer securityConfigurerAdapterForJwt() { private InternalApiKeyConfigurer securityConfigurerAdapterForInternalApiKey() { return new InternalApiKeyConfigurer(internalApiKeyProvider); } + + private ApiKeyConfigurer securityConfigurerAdapterForApiKey() { + return new ApiKeyConfigurer(apiKeyFilter); + } + } diff --git a/backend/src/main/java/com/park/utmstack/config/SwaggerHeaderConfig.java b/backend/src/main/java/com/park/utmstack/config/SwaggerHeaderConfig.java new file mode 100644 index 000000000..09e4093c9 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/config/SwaggerHeaderConfig.java @@ -0,0 +1,26 @@ +package com.park.utmstack.config; + +import io.swagger.v3.oas.models.parameters.Parameter; +import org.springdoc.core.customizers.OpenApiCustomiser; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import io.swagger.v3.oas.models.media.StringSchema; + +@Configuration +public class SwaggerHeaderConfig { + + @Bean + public OpenApiCustomiser addBypassTFAHeader() { + return openApi -> openApi.getPaths().values().forEach(pathItem -> + pathItem.readOperations().forEach(operation -> { + Parameter header = new Parameter() + .in("header") + .name(Constants.TFA_EXEMPTION_HEADER) + .required(false) + .schema(new StringSchema().example("true")); + operation.addParametersItem(header); + }) + ); + } +} + diff --git a/backend/src/main/java/com/park/utmstack/config/oauth/OAuth2ClientConfig.java b/backend/src/main/java/com/park/utmstack/config/oauth/OAuth2ClientConfig.java new file mode 100644 index 000000000..5fa4fdc4b --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/config/oauth/OAuth2ClientConfig.java @@ -0,0 +1,15 @@ +package com.park.utmstack.config.oauth; + +import com.park.utmstack.repository.idp_provider.IdentityProviderConfigRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; + +@Configuration +public class OAuth2ClientConfig { + + @Bean + public ClientRegistrationRepository clientRegistrationRepository(IdentityProviderConfigRepository repo) { + return new OAuth2ClientRegistrationRepository(repo); + } +} diff --git a/backend/src/main/java/com/park/utmstack/config/oauth/OAuth2ClientRegistrationRepository.java b/backend/src/main/java/com/park/utmstack/config/oauth/OAuth2ClientRegistrationRepository.java new file mode 100644 index 000000000..b788399e3 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/config/oauth/OAuth2ClientRegistrationRepository.java @@ -0,0 +1,79 @@ +package com.park.utmstack.config.oauth; + +import com.park.utmstack.config.Constants; +import com.park.utmstack.domain.idp_provider.IdentityProviderConfig; +import com.park.utmstack.domain.idp_provider.enums.ClientAuthMethod; +import com.park.utmstack.domain.idp_provider.enums.ProviderType; +import com.park.utmstack.repository.idp_provider.IdentityProviderConfigRepository; +import com.park.utmstack.util.CipherUtil; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.util.StringUtils; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class OAuth2ClientRegistrationRepository implements ClientRegistrationRepository { + + private final Map registrations = new ConcurrentHashMap<>(); + + public OAuth2ClientRegistrationRepository(IdentityProviderConfigRepository jpaClientRegistrationRepository) { + loadProviders(jpaClientRegistrationRepository); + } + + @Override + public ClientRegistration findByRegistrationId(String registrationId) { + return registrations.get(registrationId); + } + + private String getUserNameAttributeName(ProviderType providerType) { + return switch (providerType) { + case GOOGLE -> "sub"; + case MICROSOFT -> "oid"; + default -> "sub"; + }; + } + + public void reloadProviders(IdentityProviderConfigRepository jpaClientRegistrationRepository) { + registrations.clear(); + loadProviders(jpaClientRegistrationRepository); + } + + private void loadProviders(IdentityProviderConfigRepository jpaClientRegistrationRepository) { + jpaClientRegistrationRepository.findAllByActiveTrue().forEach(entity -> { + ClientRegistration registration = buildClientRegistration(entity); + registrations.put(entity.getProviderType().name().toLowerCase(), registration); + }); + } + + private ClientRegistration buildClientRegistration(IdentityProviderConfig entity) { + ClientAuthenticationMethod authMethod = Optional.ofNullable(entity.getClientAuthMethod()) + .map(ClientAuthMethod::toSpringMethod) + .orElse(ClientAuthenticationMethod.CLIENT_SECRET_BASIC); + + ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(entity.getProviderType().name().toLowerCase()) + .clientId(entity.getClientId()) + .clientSecret(CipherUtil.decrypt(entity.getClientSecret(), System.getenv(Constants.ENV_ENCRYPTION_KEY))) + .userNameAttributeName(this.getUserNameAttributeName(entity.getProviderType())) + .clientAuthenticationMethod(authMethod) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri(entity.getRedirectUri()) + .scope(StringUtils.commaDelimitedListToStringArray(entity.getScopes())) + .authorizationUri(entity.getAuthUri()) + .tokenUri(entity.getTokenUri()) + .clientName(entity.getName()); + + if (entity.getUserInfoUri() != null) { + builder.userInfoUri(entity.getUserInfoUri()); + } + + if (entity.getJwksUri() != null) { + builder.jwkSetUri(entity.getJwksUri()); + } + + return builder.build(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/park/utmstack/config/oauth/ProviderChangeListener.java b/backend/src/main/java/com/park/utmstack/config/oauth/ProviderChangeListener.java new file mode 100644 index 000000000..db6976674 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/config/oauth/ProviderChangeListener.java @@ -0,0 +1,23 @@ +package com.park.utmstack.config.oauth; + +import com.park.utmstack.repository.idp_provider.IdentityProviderConfigRepository; +import com.park.utmstack.util.events.ProviderChangedEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ProviderChangeListener { + + private final ClientRegistrationRepository repository; + private final IdentityProviderConfigRepository identityProviderConfigRepository; + + @EventListener + public void handleProviderChanged(ProviderChangedEvent event) { + if (repository instanceof OAuth2ClientRegistrationRepository customRepo) { + customRepo.reloadProviders(identityProviderConfigRepository); + } + } +} diff --git a/backend/src/main/java/com/park/utmstack/domain/User.java b/backend/src/main/java/com/park/utmstack/domain/User.java index 04afd27ce..736115273 100644 --- a/backend/src/main/java/com/park/utmstack/domain/User.java +++ b/backend/src/main/java/com/park/utmstack/domain/User.java @@ -2,7 +2,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.park.utmstack.config.Constants; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; import org.apache.commons.lang3.StringUtils; import org.hibernate.annotations.BatchSize; @@ -22,7 +23,8 @@ * A user. */ @Entity -@Data +@Getter +@Setter @Table(name = "jhi_user") public class User extends AbstractAuditingEntity implements Serializable { @@ -95,7 +97,7 @@ public class User extends AbstractAuditingEntity implements Serializable { private Boolean defaultPassword; @JsonIgnore - @ManyToMany + @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "jhi_user_authority", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")}) @BatchSize(size = 20) diff --git a/backend/src/main/java/com/park/utmstack/domain/UtmDataInputStatus.java b/backend/src/main/java/com/park/utmstack/domain/UtmDataInputStatus.java index 3b6eb5e60..ae9dfffe4 100644 --- a/backend/src/main/java/com/park/utmstack/domain/UtmDataInputStatus.java +++ b/backend/src/main/java/com/park/utmstack/domain/UtmDataInputStatus.java @@ -35,16 +35,6 @@ public class UtmDataInputStatus implements Serializable { @Column(name = "source", length = 256, nullable = false) private String source; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "source", referencedColumnName = "asset_name", insertable = false, updatable = false, nullable = false) - @JsonIgnore - private UtmNetworkScan assetName; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "source", referencedColumnName = "asset_ip", insertable = false, updatable = false, nullable = false) - @JsonIgnore - private UtmNetworkScan assetIp; - @NotNull @Size(max = 50) @Column(name = "data_type", length = 50, nullable = false) @@ -69,4 +59,16 @@ public Boolean isDown() { long now = Instant.now().getEpochSecond(); return (now - timestamp) > (median * 1.5); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UtmDataInputStatus that)) return false; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseActionTemplate.java b/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseActionTemplate.java index 58cc4799b..35d4ef2e5 100644 --- a/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseActionTemplate.java +++ b/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseActionTemplate.java @@ -41,14 +41,7 @@ public class UtmAlertResponseActionTemplate implements Serializable { @Column(name = "system_owner", nullable = false) private Boolean systemOwner; - @ManyToMany - @JoinTable( - name = "utm_alert_response_rule_template", - joinColumns = @JoinColumn(name = "rule_id"), - inverseJoinColumns = @JoinColumn(name = "template_id") - ) - private List utmAlertResponseActionTemplates = new ArrayList<>(); - - + @ManyToMany(mappedBy = "utmAlertResponseActionTemplates") + private List utmAlertResponseRules = new ArrayList<>(); } diff --git a/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseRule.java b/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseRule.java index a1bdb042a..96b7f303a 100644 --- a/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseRule.java +++ b/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseRule.java @@ -5,6 +5,7 @@ import com.park.utmstack.service.dto.UtmAlertResponseRuleDTO; import lombok.Getter; import lombok.Setter; +import org.hibernate.annotations.GenericGenerator; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; @@ -30,7 +31,8 @@ public class UtmAlertResponseRule implements Serializable { private static final long serialVersionUID = 1L; @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + @GenericGenerator(name = "CustomIdentityGenerator", strategy = "com.park.utmstack.util.CustomIdentityGenerator") + @GeneratedValue(generator = "CustomIdentityGenerator") private Long id; @Column(name = "rule_name", length = 150, nullable = false) private String ruleName; @@ -62,7 +64,13 @@ public class UtmAlertResponseRule implements Serializable { @Column(name = "last_modified_date") private Instant lastModifiedDate; - @ManyToMany + @Column(name = "system_owner", nullable = false) + private Boolean systemOwner; + + @OneToMany(mappedBy = "rule", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + List utmAlertResponseRuleExecutions; + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) @JoinTable( name = "utm_alert_response_rule_template", joinColumns = @JoinColumn(name = "rule_id"), @@ -84,6 +92,7 @@ public UtmAlertResponseRule(UtmAlertResponseRuleDTO dto) { this.ruleActive = dto.getActive(); this.agentPlatform = dto.getAgentPlatform(); this.defaultAgent = dto.getDefaultAgent(); + this.systemOwner = dto.getSystemOwner(); if (!CollectionUtils.isEmpty(dto.getExcludedAgents())) this.excludedAgents = String.join(",", dto.getExcludedAgents()); else @@ -99,6 +108,7 @@ public UtmAlertResponseRule(UtmAlertResponseRuleDTO dto) { template.setTitle(templateDto.getTitle()); template.setDescription(templateDto.getDescription()); template.setCommand(templateDto.getCommand()); + template.setSystemOwner(false); return template; }) .collect(Collectors.toList()); diff --git a/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseRuleExecution.java b/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseRuleExecution.java index acdd815cc..f6def7306 100644 --- a/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseRuleExecution.java +++ b/backend/src/main/java/com/park/utmstack/domain/alert_response_rule/UtmAlertResponseRuleExecution.java @@ -3,6 +3,7 @@ import com.park.utmstack.domain.alert_response_rule.enums.RuleExecutionStatus; import com.park.utmstack.domain.alert_response_rule.enums.RuleNonExecutionCause; +import lombok.Data; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -14,6 +15,7 @@ @Entity @Table(name = "utm_alert_response_rule_execution") +@Data @EntityListeners(AuditingEntityListener.class) public class UtmAlertResponseRuleExecution implements Serializable { @@ -61,84 +63,8 @@ public class UtmAlertResponseRuleExecution implements Serializable { @Column(name = "execution_retries") private Integer executionRetries = 0; + @ManyToOne + @JoinColumn(name = "rule_id", referencedColumnName = "id", insertable = false, updatable = false) + UtmAlertResponseRule rule; - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Long getRuleId() { - return ruleId; - } - - public void setRuleId(Long ruleId) { - this.ruleId = ruleId; - } - - public String getAlertId() { - return alertId; - } - - public void setAlertId(String alertId) { - this.alertId = alertId; - } - - public String getCommand() { - return command; - } - - public void setCommand(String command) { - this.command = command; - } - - public String getCommandResult() { - return commandResult; - } - - public void setCommandResult(String commandResult) { - this.commandResult = commandResult; - } - - public String getAgent() { - return agent; - } - - public void setAgent(String agent) { - this.agent = agent; - } - - public Instant getExecutionDate() { - return executionDate; - } - - public void setExecutionDate(Instant executionDate) { - this.executionDate = executionDate; - } - - public RuleExecutionStatus getExecutionStatus() { - return executionStatus; - } - - public void setExecutionStatus(RuleExecutionStatus executionStatus) { - this.executionStatus = executionStatus; - } - - public RuleNonExecutionCause getNonExecutionCause() { - return nonExecutionCause; - } - - public void setNonExecutionCause(RuleNonExecutionCause nonExecutionCause) { - this.nonExecutionCause = nonExecutionCause; - } - - public Integer getExecutionRetries() { - return executionRetries; - } - - public void setExecutionRetries(Integer executionRetries) { - this.executionRetries = executionRetries; - } } diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java new file mode 100644 index 000000000..4986a7b58 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKey.java @@ -0,0 +1,44 @@ +package com.park.utmstack.domain.api_keys; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.io.Serializable; +import java.time.Instant; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "api_keys") +public class ApiKey implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long userId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String apiKey; + + @Column + private String allowedIp; + + @Column(nullable = false) + private Instant createdAt; + + private Instant generatedAt; + + @Column + private Instant expiresAt; +} diff --git a/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java new file mode 100644 index 000000000..54e0c8d15 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/api_keys/ApiKeyUsageLog.java @@ -0,0 +1,49 @@ +package com.park.utmstack.domain.api_keys; + +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.*; + +import java.util.HashMap; +import java.util.Map; + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ApiKeyUsageLog implements AuditableDTO { + + private String id; + private String apiKeyId; + private String apiKeyName; + private String userId; + private String timestamp; + private String endpoint; + private String address; + private String errorMessage; + private String queryParams; + private String payload; + private String userAgent; + private String httpMethod; + private String statusCode; + + @Override + public Map toAuditMap() { + Map map = new HashMap<>(); + + map.put("id", id); + map.put("api_key_id", apiKeyId); + map.put("api_key_name", apiKeyName); + map.put("user_id", userId); + map.put("timestamp", timestamp != null ? timestamp : null); + map.put("endpoint", endpoint); + map.put("address", address); + map.put("error_message", errorMessage); + map.put("query_params", queryParams); + map.put("user_agent", userAgent); + map.put("http_method", httpMethod); + map.put("status_code", statusCode); + + return map; + } +} diff --git a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java index 1a901c7ac..eca45ec19 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_events/enums/ApplicationEventType.java @@ -1,7 +1,50 @@ package com.park.utmstack.domain.application_events.enums; public enum ApplicationEventType { + AUTH_ATTEMPT, + AUTH_SUCCESS, + AUTH_FAILURE, + TFA_CODE_SENT, + TFA_CODE_VERIFY_ATTEMPT, + TFA_VERIFIED, + AUTH_LOGOUT, + CONFIG_CHANGED, + USER_MANAGEMENT, + ACCESS_DENIED, + ALERT_UPDATE_ATTEMPT, + ALERT_UPDATE_SUCCESS, + ALERT_STATUS_UPDATE_ATTEMPT, + ALERT_STATUS_UPDATE_SUCCESS, + ALERT_NOTE_UPDATE_ATTEMPT, + ALERT_NOTE_UPDATE_SUCCESS, + ALERT_TAG_UPDATE_ATTEMPT, + ALERT_CONVERT_TO_INCIDENT_ATTEMPT, + ALERT_CONVERT_TO_INCIDENT_SUCCESS, + ALERT_TAG_UPDATE_SUCCESS, + CONFIG_GROUP_CREATE_ATTEMPT, + CONFIG_GROUP_CREATE_SUCCESS, + CONFIG_GROUP_UPDATE_ATTEMPT, + CONFIG_GROUP_UPDATE_SUCCESS, + CONFIG_GROUP_DELETE_ATTEMPT, + CONFIG_GROUP_DELETE_SUCCESS, + CONFIG_GROUP_BULK_DELETE_ATTEMPT, + CONFIG_GROUP_BULK_DELETE_SUCCESS, + CONFIG_UPDATE_ATTEMPT, + CONFIG_UPDATE_SUCCESS, + INCIDENT_CREATION_ATTEMPT, + INCIDENT_CREATED, + INCIDENT_CREATION_SUCCESS, + INCIDENT_ALERTS_ADDED, + INCIDENT_ALERT_ADD_ATTEMPT, + INCIDENT_ALERT_ADD_SUCCESS, + INCIDENT_UPDATE_ATTEMPT, + INCIDENT_UPDATE_SUCCESS, ERROR, WARNING, - INFO + INFO, + MODULE_ACTIVATION_ATTEMPT, + MODULE_ACTIVATION_SUCCESS, + API_KEY_ACCESS_SUCCESS, + API_KEY_ACCESS_FAILURE, + UNDEFINED } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_events/types/ApplicationEvent.java b/backend/src/main/java/com/park/utmstack/domain/application_events/types/ApplicationEvent.java index ef0195d5b..1ceadc3e4 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_events/types/ApplicationEvent.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_events/types/ApplicationEvent.java @@ -1,88 +1,21 @@ package com.park.utmstack.domain.application_events.types; import com.fasterxml.jackson.annotation.JsonProperty; - +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor public class ApplicationEvent { + @JsonProperty("@timestamp") private String timestamp; + private String source; private String message; private String type; - - public ApplicationEvent() { - } - - public ApplicationEvent(String timestamp, String source, String message, String type) { - this.timestamp = timestamp; - this.source = source; - this.message = message; - this.type = type; - } - - public String getTimestamp() { - return timestamp; - } - - public void setTimestamp(String timestamp) { - this.timestamp = timestamp; - } - - public String getSource() { - return source; - } - - public void setSource(String source) { - this.source = source; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - private String timestamp; - private String source; - private String message; - private String type; - - public ApplicationEvent build() { - return new ApplicationEvent(timestamp, source, message, type); - } - - public Builder timestamp(String timestamp) { - this.timestamp = timestamp; - return this; - } - - public Builder source(String source) { - this.source = source; - return this; - } - - public Builder message(String message) { - this.message = message; - return this; - } - - public Builder type(String type) { - this.type = type; - return this; - } - } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/enums/ModuleName.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/enums/ModuleName.java index 6bcdf84ef..b773d45d6 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/enums/ModuleName.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/enums/ModuleName.java @@ -64,5 +64,6 @@ public enum ModuleName { SOC_AI, PFSENSE, ORACLE, - SURICATA + SURICATA, + UTMSTACK } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/IModule.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/IModule.java index 0511ff785..299e2202e 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/IModule.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/IModule.java @@ -2,6 +2,7 @@ import com.park.utmstack.domain.application_modules.UtmModule; import com.park.utmstack.domain.application_modules.UtmModuleGroupConfiguration; +import com.park.utmstack.domain.application_modules.enums.ModuleName; import com.park.utmstack.domain.application_modules.types.ModuleConfigurationKey; import com.park.utmstack.domain.application_modules.types.ModuleRequirement; @@ -18,4 +19,6 @@ public interface IModule { default boolean validateConfiguration(UtmModule module, List configuration) throws Exception { return true; } + + ModuleName getName(); } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/ModuleFactory.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/ModuleFactory.java index 41e58fd17..d0d3d41e9 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/ModuleFactory.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/ModuleFactory.java @@ -2,325 +2,32 @@ import com.park.utmstack.domain.application_modules.enums.ModuleName; import com.park.utmstack.domain.application_modules.factory.impl.*; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + @Component +@RequiredArgsConstructor public class ModuleFactory { - private final ModuleFileIntegrity moduleFileIntegrity; - private final ModuleO365 moduleO365; - private final ModuleAzure moduleAzure; - private final ModuleAwsCloudTrail moduleAwsCloudTrail; - private final ModuleAwsIamUser moduleAwsIamUser; - private final ModuleAwsTrafficMirror moduleAwsTrafficMirror; - private final ModuleAwsBeanstalk moduleAwsBeanstalk; - private final ModuleAwsFargate moduleAwsFargate; - private final ModuleAwsLambda moduleAwsLambda; - private final ModuleLinuxLogs moduleLinuxLogs; - private final ModuleNetflow moduleNetflow; - private final ModuleAwsPostgresql moduleAwsPostgresql; - private final ModuleSophos moduleSophos; - private final ModuleAwsSqlServer moduleAwsSqlServer; - private final ModuleSyslog moduleSyslog; - private final ModuleVMWare moduleVMWare; - private final ModuleWindowsAgent moduleWindowsAgent; - private final ModuleIIS moduleIIS; - private final ModuleGcp moduleGcp; - private final ModuleJson moduleJson; - private final ModuleMacOsAgent moduleMacOsAgent; - private final ModuleLinuxAgent moduleLinuxAgent; - private final ModuleApache moduleApache; - private final ModuleApache2 moduleApache2; - private final ModuleAuditD moduleAuditD; - private final ModuleElasticsearch moduleElasticsearch; - private final ModuleHaProxy moduleHaProxy; - private final ModuleKafka moduleKafka; - private final ModuleKibana moduleKibana; - private final ModuleLogstash moduleLogstash; - private final ModuleMongoDb moduleMongoDb; - private final ModuleMySql moduleMySql; - private final ModuleNats moduleNats; - private final ModuleNginx moduleNginx; - private final ModuleOsQuery moduleOsQuery; - private final ModulePostgreSql modulePostgreSql; - private final ModuleRedis moduleRedis; - private final ModuleTraefik moduleTraefik; - private final ModuleCisco moduleCisco; - private final ModuleMeraki moduleMeraki; - private final ModuleEset moduleEset; - private final ModuleKaspersky moduleKaspersky; - private final ModuleSentinelOne moduleSentinelOne; - private final ModuleFortiGate moduleFortiGate; - private final ModuleSophosXg moduleSophosXg; - private final ModuleFirePower moduleFirePower; - private final ModuleUFW moduleUFW; - private final ModuleMacOS moduleMacOS; - private final ModuleMikrotik moduleMikrotik; - private final ModulePaloAlto modulePaloAlto; - private final ModuleCiscoSwitch moduleCiscoSwitch; - private final ModuleSonicWall moduleSonicWall; - private final ModuleDeceptiveBytes moduleDeceptiveBytes; - private final ModuleGitHub moduleGitHub; - private final ModuleBitdefender moduleBitdefender; - private final ModuleSalesforce moduleSalesforce; - private final ModuleIbmAs400 moduleIbmAs400; - private final ModuleSocAi moduleSocAi; - private final ModulePfsense modulePfsense; - private final ModuleFortiWeb moduleFortiWeb; - private final ModuleAix moduleAix; - private final ModuleSuricata moduleSuricata; + private final Map moduleBeans; + private Map moduleMap; - public ModuleFactory(ModuleFileIntegrity moduleFileIntegrity, - ModuleO365 moduleO365, - ModuleAzure moduleAzure, - ModuleAwsCloudTrail moduleAwsCloudTrail, - ModuleAwsIamUser moduleAwsIamUser, - ModuleAwsTrafficMirror moduleAwsTrafficMirror, - ModuleAwsBeanstalk moduleAwsBeanstalk, - ModuleAwsFargate moduleAwsFargate, - ModuleAwsLambda moduleAwsLambda, - ModuleLinuxLogs moduleLinuxLogs, - ModuleNetflow moduleNetflow, - ModuleAwsPostgresql moduleAwsPostgresql, - ModuleSophos moduleSophos, - ModuleAwsSqlServer moduleAwsSqlServer, - ModuleSyslog moduleSyslog, - ModuleVMWare moduleVMWare, - ModuleWindowsAgent moduleWindowsAgent, - ModuleIIS moduleIIS, - ModuleGcp moduleGcp, - ModuleJson moduleJson, - ModuleMacOsAgent moduleMacOsAgent, - ModuleLinuxAgent moduleLinuxAgent, - ModuleApache moduleApache, - ModuleApache2 moduleApache2, - ModuleAuditD moduleAuditD, - ModuleElasticsearch moduleElasticsearch, - ModuleHaProxy moduleHaProxy, - ModuleKafka moduleKafka, - ModuleKibana moduleKibana, - ModuleLogstash moduleLogstash, - ModuleMongoDb moduleMongoDb, - ModuleMySql moduleMySql, - ModuleNats moduleNats, - ModuleNginx moduleNginx, - ModuleOsQuery moduleOsQuery, - ModulePostgreSql modulePostgreSql, - ModuleRedis moduleRedis, - ModuleTraefik moduleTraefik, - ModuleCisco moduleCisco, - ModuleMeraki moduleMeraki, - ModuleEset moduleEset, - ModuleKaspersky moduleKaspersky, - ModuleSentinelOne moduleSentinelOne, - ModuleFortiGate moduleFortiGate, - ModuleSophosXg moduleSophosXg, - ModuleFirePower moduleFirePower, - ModuleUFW moduleUFW, - ModuleMacOS moduleMacOS, - ModuleMikrotik moduleMikrotik, - ModulePaloAlto modulePaloAlto, - ModuleCiscoSwitch moduleCiscoSwitch, - ModuleSonicWall moduleSonicWall, - ModuleDeceptiveBytes moduleDeceptiveBytes, - ModuleGitHub moduleGitHub, - ModuleBitdefender moduleBitdefender, - ModuleSalesforce moduleSalesforce, - ModuleIbmAs400 moduleIbmAs400, - ModuleSocAi moduleSocAi, - ModulePfsense modulePfsense, - ModuleFortiWeb moduleFortiWeb, - ModuleAix moduleAix, - ModuleSuricata moduleSuricata) { - this.moduleFileIntegrity = moduleFileIntegrity; - this.moduleO365 = moduleO365; - this.moduleAzure = moduleAzure; - this.moduleAwsCloudTrail = moduleAwsCloudTrail; - this.moduleAwsIamUser = moduleAwsIamUser; - this.moduleAwsTrafficMirror = moduleAwsTrafficMirror; - this.moduleAwsBeanstalk = moduleAwsBeanstalk; - this.moduleAwsFargate = moduleAwsFargate; - this.moduleAwsLambda = moduleAwsLambda; - this.moduleLinuxLogs = moduleLinuxLogs; - this.moduleNetflow = moduleNetflow; - this.moduleAwsPostgresql = moduleAwsPostgresql; - this.moduleSophos = moduleSophos; - this.moduleAwsSqlServer = moduleAwsSqlServer; - this.moduleSyslog = moduleSyslog; - this.moduleVMWare = moduleVMWare; - this.moduleWindowsAgent = moduleWindowsAgent; - this.moduleIIS = moduleIIS; - this.moduleGcp = moduleGcp; - this.moduleJson = moduleJson; - this.moduleMacOsAgent = moduleMacOsAgent; - this.moduleLinuxAgent = moduleLinuxAgent; - this.moduleApache = moduleApache; - this.moduleApache2 = moduleApache2; - this.moduleAuditD = moduleAuditD; - this.moduleElasticsearch = moduleElasticsearch; - this.moduleHaProxy = moduleHaProxy; - this.moduleKafka = moduleKafka; - this.moduleKibana = moduleKibana; - this.moduleLogstash = moduleLogstash; - this.moduleMongoDb = moduleMongoDb; - this.moduleMySql = moduleMySql; - this.moduleNats = moduleNats; - this.moduleNginx = moduleNginx; - this.moduleOsQuery = moduleOsQuery; - this.modulePostgreSql = modulePostgreSql; - this.moduleRedis = moduleRedis; - this.moduleTraefik = moduleTraefik; - this.moduleCisco = moduleCisco; - this.moduleMeraki = moduleMeraki; - this.moduleEset = moduleEset; - this.moduleKaspersky = moduleKaspersky; - this.moduleSentinelOne = moduleSentinelOne; - this.moduleFortiGate = moduleFortiGate; - this.moduleSophosXg = moduleSophosXg; - this.moduleFirePower = moduleFirePower; - this.moduleUFW = moduleUFW; - this.moduleMacOS = moduleMacOS; - this.moduleMikrotik = moduleMikrotik; - this.modulePaloAlto = modulePaloAlto; - this.moduleCiscoSwitch = moduleCiscoSwitch; - this.moduleSonicWall = moduleSonicWall; - this.moduleDeceptiveBytes = moduleDeceptiveBytes; - this.moduleGitHub = moduleGitHub; - this.moduleBitdefender = moduleBitdefender; - this.moduleSalesforce = moduleSalesforce; - this.moduleIbmAs400 = moduleIbmAs400; - this.moduleSocAi = moduleSocAi; - this.modulePfsense = modulePfsense; - this.moduleFortiWeb = moduleFortiWeb; - this.moduleAix = moduleAix; - this.moduleSuricata = moduleSuricata; + @PostConstruct + void init() { + moduleMap = moduleBeans.values().stream() + .collect(Collectors.toMap(IModule::getName, Function.identity())); } public IModule getInstance(ModuleName nameShort) { - if (nameShort.equals(ModuleName.FILE_INTEGRITY)) - return moduleFileIntegrity; - if (nameShort.equals(ModuleName.O365)) - return moduleO365; - if (nameShort.equals(ModuleName.AZURE)) - return moduleAzure; - if (nameShort.equals(ModuleName.NETFLOW)) - return moduleNetflow; - if (nameShort.equals(ModuleName.WINDOWS_AGENT)) - return moduleWindowsAgent; - if (nameShort.equals(ModuleName.SYSLOG)) - return moduleSyslog; - if (nameShort.equals(ModuleName.LINUX_LOGS)) - return moduleLinuxLogs; - if (nameShort.equals(ModuleName.VMWARE)) - return moduleVMWare; - if (nameShort.equals(ModuleName.AWS_TRAFFIC_MIRROR)) - return moduleAwsTrafficMirror; - if (nameShort.equals(ModuleName.AWS_IAM_USER)) - return moduleAwsIamUser; - if (nameShort.equals(ModuleName.AWS_CLOUDTRAIL)) - return moduleAwsCloudTrail; - if (nameShort.equals(ModuleName.AWS_SQL_SERVER)) - return moduleAwsSqlServer; - if (nameShort.equals(ModuleName.AWS_POSTGRESQL)) - return moduleAwsPostgresql; - if (nameShort.equals(ModuleName.AWS_BEANSTALK)) - return moduleAwsBeanstalk; - if (nameShort.equals(ModuleName.AWS_FARGATE)) - return moduleAwsFargate; - if (nameShort.equals(ModuleName.AWS_LAMBDA)) - return moduleAwsLambda; - if (nameShort.equals(ModuleName.SOPHOS)) - return moduleSophos; - if (nameShort.equals(ModuleName.IIS)) - return moduleIIS; - if (nameShort.equals(ModuleName.GCP)) - return moduleGcp; - if (nameShort.equals(ModuleName.JSON)) - return moduleJson; - if (nameShort.equals(ModuleName.MACOS_AGENT)) - return moduleMacOsAgent; - if (nameShort.equals(ModuleName.LINUX_AGENT)) - return moduleLinuxAgent; - if (nameShort.equals(ModuleName.APACHE)) - return moduleApache; - if (nameShort.equals(ModuleName.APACHE2)) - return moduleApache2; - if (nameShort.equals(ModuleName.AUDITD)) - return moduleAuditD; - if (nameShort.equals(ModuleName.ELASTICSEARCH)) - return moduleElasticsearch; - if (nameShort.equals(ModuleName.HAPROXY)) - return moduleHaProxy; - if (nameShort.equals(ModuleName.KAFKA)) - return moduleKafka; - if (nameShort.equals(ModuleName.KIBANA)) - return moduleKibana; - if (nameShort.equals(ModuleName.LOGSTASH)) - return moduleLogstash; - if (nameShort.equals(ModuleName.MONGODB)) - return moduleMongoDb; - if (nameShort.equals(ModuleName.MYSQL)) - return moduleMySql; - if (nameShort.equals(ModuleName.NATS)) - return moduleNats; - if (nameShort.equals(ModuleName.NGINX)) - return moduleNginx; - if (nameShort.equals(ModuleName.OSQUERY)) - return moduleOsQuery; - if (nameShort.equals(ModuleName.POSTGRESQL)) - return modulePostgreSql; - if (nameShort.equals(ModuleName.REDIS)) - return moduleRedis; - if (nameShort.equals(ModuleName.TRAEFIK)) - return moduleTraefik; - if (nameShort.equals(ModuleName.CISCO)) - return moduleCisco; - if (nameShort.equals(ModuleName.MERAKI)) - return moduleMeraki; - if (nameShort.equals(ModuleName.ESET)) - return moduleEset; - if (nameShort.equals(ModuleName.KASPERSKY)) - return moduleKaspersky; - if (nameShort.equals(ModuleName.SENTINEL_ONE)) - return moduleSentinelOne; - if (nameShort.equals(ModuleName.FORTIGATE)) - return moduleFortiGate; - if (nameShort.equals(ModuleName.SOPHOS_XG)) - return moduleSophosXg; - if (nameShort.equals(ModuleName.FIRE_POWER)) - return moduleFirePower; - if (nameShort.equals(ModuleName.UFW)) - return moduleUFW; - if (nameShort.equals(ModuleName.MACOS)) - return moduleMacOS; - if (nameShort.equals(ModuleName.MIKROTIK)) - return moduleMikrotik; - if (nameShort.equals(ModuleName.PALO_ALTO)) - return modulePaloAlto; - if (nameShort.equals(ModuleName.CISCO_SWITCH)) - return moduleCiscoSwitch; - if (nameShort.equals(ModuleName.SONIC_WALL)) - return moduleSonicWall; - if (nameShort.equals(ModuleName.DECEPTIVE_BYTES)) - return moduleDeceptiveBytes; - if (nameShort.equals(ModuleName.GITHUB)) - return moduleGitHub; - if (nameShort.equals(ModuleName.BITDEFENDER)) - return moduleBitdefender; - if (nameShort.equals(ModuleName.SALESFORCE)) - return moduleSalesforce; - if (nameShort.equals(ModuleName.AS_400)) - return moduleIbmAs400; - if (nameShort.equals(ModuleName.SOC_AI)) - return moduleSocAi; - if (nameShort.equals(ModuleName.PFSENSE)) - return modulePfsense; - if (nameShort.equals(ModuleName.FORTIWEB)) - return moduleFortiWeb; - if (nameShort.equals(ModuleName.AIX)) - return moduleAix; - if (nameShort.equals(ModuleName.SURICATA)) - return moduleSuricata; - throw new RuntimeException("Unrecognized module " + nameShort.name()); + IModule module = moduleMap.get(nameShort); + if (module == null) { + throw new IllegalArgumentException("Unrecognized module: " + nameShort.name()); + } + return module; } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAix.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAix.java index 8642a0657..5cd6c3390 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAix.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAix.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.AIX; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleApache.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleApache.java index 799e18daf..99926b557 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleApache.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleApache.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.APACHE; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleApache2.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleApache2.java index b938ec653..9c6595a49 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleApache2.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleApache2.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.APACHE2; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAuditD.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAuditD.java index 2c83cf238..f284c3cd0 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAuditD.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAuditD.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.AUDITD; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsBeanstalk.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsBeanstalk.java index 516d5c992..719711712 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsBeanstalk.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsBeanstalk.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.AWS_BEANSTALK; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsCloudTrail.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsCloudTrail.java index 70ea984b7..41b045941 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsCloudTrail.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsCloudTrail.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.AWS_CLOUDTRAIL; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsFargate.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsFargate.java index a4424e845..d2c8c5296 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsFargate.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsFargate.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.AWS_FARGATE; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsIamUser.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsIamUser.java index 6d97a4627..a6c40e090 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsIamUser.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsIamUser.java @@ -78,4 +78,9 @@ public List getConfigurationKeys(Long groupId) throws Ex public boolean validateConfiguration(UtmModule module, List configuration) throws Exception { return utmStackConfigValidator.validate(module, configuration); } + + @Override + public ModuleName getName() { + return ModuleName.AWS_IAM_USER; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsLambda.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsLambda.java index 80fb9bb75..8fb12d928 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsLambda.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsLambda.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.AWS_LAMBDA; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsPostgresql.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsPostgresql.java index 7e892fa7f..9270a8d0e 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsPostgresql.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsPostgresql.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.AWS_POSTGRESQL; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsSqlServer.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsSqlServer.java index 7882e011e..7901a7e89 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsSqlServer.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsSqlServer.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.AWS_SQL_SERVER; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsTrafficMirror.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsTrafficMirror.java index e22a3652e..0e29e0b6b 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsTrafficMirror.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAwsTrafficMirror.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.AWS_TRAFFIC_MIRROR; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAzure.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAzure.java index 134ab0b0a..8950ce3c8 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAzure.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleAzure.java @@ -88,4 +88,9 @@ public List getConfigurationKeys(Long groupId) throws Ex public boolean validateConfiguration(UtmModule module, List configuration) throws Exception { return utmStackConfigValidator.validate(module, configuration); } + + @Override + public ModuleName getName() { + return ModuleName.AZURE; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleBitdefender.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleBitdefender.java index bbaded990..134bf9fe3 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleBitdefender.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleBitdefender.java @@ -88,4 +88,9 @@ public List getConfigurationKeys(Long groupId) throws Ex public boolean validateConfiguration(UtmModule module, List configuration) throws Exception { return utmStackConfigValidator.validate(module, configuration); } + + @Override + public ModuleName getName() { + return ModuleName.BITDEFENDER; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleCisco.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleCisco.java index 630519786..f74050f10 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleCisco.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleCisco.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.CISCO; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleCiscoSwitch.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleCiscoSwitch.java index c0b0f5efb..5bc6e487a 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleCiscoSwitch.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleCiscoSwitch.java @@ -83,4 +83,9 @@ public List getConfigurationKeys(Long groupId) throws Ex .build()); return keys; } + + @Override + public ModuleName getName() { + return ModuleName.CISCO_SWITCH; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleDeceptiveBytes.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleDeceptiveBytes.java index 6ce1e8dc3..2bda2a754 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleDeceptiveBytes.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleDeceptiveBytes.java @@ -82,4 +82,9 @@ public List getConfigurationKeys(Long groupId) throws Ex .build()); return keys; } + + @Override + public ModuleName getName() { + return ModuleName.DECEPTIVE_BYTES; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleElasticsearch.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleElasticsearch.java index c27ea2f9a..2027641cb 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleElasticsearch.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleElasticsearch.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.ELASTICSEARCH; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleEset.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleEset.java index 7fcafd5b8..8b5c67ee9 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleEset.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleEset.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.ESET; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFileIntegrity.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFileIntegrity.java index e3d21e7df..74e8c7fe8 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFileIntegrity.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFileIntegrity.java @@ -50,4 +50,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.FILE_INTEGRITY; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFirePower.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFirePower.java index 7a133cc2d..598556708 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFirePower.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFirePower.java @@ -85,4 +85,8 @@ public List getConfigurationKeys(Long groupId) throws Ex return keys; } + @Override + public ModuleName getName() { + return ModuleName.FIRE_POWER; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFortiGate.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFortiGate.java index 2200cf5b7..cfcfe1166 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFortiGate.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFortiGate.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.FORTIGATE; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFortiWeb.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFortiWeb.java index 5391f3f57..d2ab56a5a 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFortiWeb.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleFortiWeb.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.FORTIWEB; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleGcp.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleGcp.java index a9d255279..b31a65adb 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleGcp.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleGcp.java @@ -89,4 +89,9 @@ public List getConfigurationKeys(Long groupId) throws Ex public boolean validateConfiguration(UtmModule module, List configuration) throws Exception { return utmStackConfigValidator.validate(module, configuration); } + + @Override + public ModuleName getName() { + return ModuleName.GCP; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleGitHub.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleGitHub.java index 804e916ba..f3a023e62 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleGitHub.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleGitHub.java @@ -82,4 +82,9 @@ public List getConfigurationKeys(Long groupId) throws Ex .build()); return keys; } + + @Override + public ModuleName getName() { + return ModuleName.GITHUB; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleHaProxy.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleHaProxy.java index d5b2e54ce..82af81dbc 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleHaProxy.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleHaProxy.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.HAPROXY; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleIIS.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleIIS.java index 15fc70ac0..ae40db0a2 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleIIS.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleIIS.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.IIS; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleIbmAs400.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleIbmAs400.java index 094e42fe5..3248aaf7c 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleIbmAs400.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleIbmAs400.java @@ -68,4 +68,9 @@ public List getConfigurationKeys(Long groupId) throws Ex return keys; } + + @Override + public ModuleName getName() { + return ModuleName.AS_400; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleJson.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleJson.java index fdb82a093..23c8146b0 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleJson.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleJson.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.JSON; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKafka.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKafka.java index 558b5e828..fb79ed7bb 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKafka.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKafka.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.KAFKA; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKaspersky.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKaspersky.java index 7a8a7c3b3..717c12a76 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKaspersky.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKaspersky.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.KASPERSKY; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKibana.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKibana.java index efe345b90..37a6b8583 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKibana.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleKibana.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.KIBANA; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLinuxAgent.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLinuxAgent.java index df6cdd246..72a8638c5 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLinuxAgent.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLinuxAgent.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.LINUX_AGENT; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLinuxLogs.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLinuxLogs.java index 969da0de5..8ed9efea6 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLinuxLogs.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLinuxLogs.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.LINUX_LOGS; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLogstash.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLogstash.java index 23c49072e..cef8dde4f 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLogstash.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleLogstash.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.LOGSTASH; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMacOS.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMacOS.java index c5da6e153..264e99862 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMacOS.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMacOS.java @@ -82,4 +82,9 @@ public List getConfigurationKeys(Long groupId) throws Ex .build()); return keys; } + + @Override + public ModuleName getName() { + return ModuleName.MACOS; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMacOsAgent.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMacOsAgent.java index 5f1a7ea9b..6be18ce99 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMacOsAgent.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMacOsAgent.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.MACOS_AGENT; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMeraki.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMeraki.java index 0d13e83ac..3b53c573e 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMeraki.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMeraki.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.MERAKI; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMikrotik.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMikrotik.java index 24efa85dd..28909fc06 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMikrotik.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMikrotik.java @@ -83,4 +83,9 @@ public List getConfigurationKeys(Long groupId) throws Ex return keys; } + @Override + public ModuleName getName() { + return ModuleName.MIKROTIK; + } + } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMongoDb.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMongoDb.java index d891c6e1d..66e55819a 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMongoDb.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMongoDb.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.MONGODB; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMySql.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMySql.java index e5f414ec7..c0728dc34 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMySql.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleMySql.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.MYSQL; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNats.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNats.java index cdb73f7ab..8bf67719c 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNats.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNats.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.NATS; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNetflow.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNetflow.java index a5e1f4d52..cd0c5a5f2 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNetflow.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNetflow.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.NETFLOW; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNginx.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNginx.java index a62c56a1a..72c865ac1 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNginx.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleNginx.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.NGINX; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleO365.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleO365.java index afe1e6f4f..385ef681e 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleO365.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleO365.java @@ -78,4 +78,9 @@ public List getConfigurationKeys(Long groupId) throws Ex public boolean validateConfiguration(UtmModule module, List configuration) throws Exception { return utmStackConfigValidator.validate(module, configuration); } + + @Override + public ModuleName getName() { + return ModuleName.O365; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleOsQuery.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleOsQuery.java index 456166b15..d8aa48f02 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleOsQuery.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleOsQuery.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.OSQUERY; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePaloAlto.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePaloAlto.java index 0201cc151..b58f3801f 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePaloAlto.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePaloAlto.java @@ -81,4 +81,9 @@ public List getConfigurationKeys(Long groupId) throws Ex .build()); return keys; } + + @Override + public ModuleName getName() { + return ModuleName.PALO_ALTO; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePfsense.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePfsense.java index 5628bd26c..c021dad7d 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePfsense.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePfsense.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.PFSENSE; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePostgreSql.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePostgreSql.java index 0406d10ef..095990766 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePostgreSql.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModulePostgreSql.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.POSTGRESQL; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleRedis.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleRedis.java index f755c9dd4..516ec2cff 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleRedis.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleRedis.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.REDIS; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSalesforce.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSalesforce.java index d4f57233a..d19e66e6d 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSalesforce.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSalesforce.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.SALESFORCE; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSentinelOne.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSentinelOne.java index a1f1541d1..819a5c642 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSentinelOne.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSentinelOne.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.SENTINEL_ONE; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSocAi.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSocAi.java index e62f90007..3b7801561 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSocAi.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSocAi.java @@ -105,4 +105,9 @@ public List getConfigurationKeys(Long groupId) throws Ex public boolean validateConfiguration(UtmModule module, List configuration) throws Exception { return utmStackConfigValidator.validate(module, configuration); } + + @Override + public ModuleName getName() { + return ModuleName.SOC_AI; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSonicWall.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSonicWall.java index e840f84a2..4eb4d0f89 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSonicWall.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSonicWall.java @@ -83,4 +83,9 @@ public List getConfigurationKeys(Long groupId) throws Ex .build()); return keys; } + + @Override + public ModuleName getName() { + return ModuleName.SONIC_WALL; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSophos.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSophos.java index 4f7c86964..0b39e0dde 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSophos.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSophos.java @@ -68,4 +68,9 @@ public List getConfigurationKeys(Long groupId) throws Ex public boolean validateConfiguration(UtmModule module, List configuration) throws Exception { return utmStackConfigValidator.validate(module, configuration); } + + @Override + public ModuleName getName() { + return ModuleName.SOPHOS; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSophosXg.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSophosXg.java index 302c3b63a..69f46080d 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSophosXg.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSophosXg.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.SOPHOS_XG; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSuricata.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSuricata.java index 8878eea50..c2ccfb395 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSuricata.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSuricata.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.SURICATA; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSyslog.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSyslog.java index 1cb49780a..3ccf6c263 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSyslog.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleSyslog.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.SYSLOG; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleTraefik.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleTraefik.java index 32a94e723..2c45d2306 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleTraefik.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleTraefik.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.TRAEFIK; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleUFW.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleUFW.java index b36d81a15..dd7abfa40 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleUFW.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleUFW.java @@ -82,4 +82,9 @@ public List getConfigurationKeys(Long groupId) throws Ex .build()); return keys; } + + @Override + public ModuleName getName() { + return ModuleName.UFW; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleUtmstack.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleUtmstack.java new file mode 100644 index 000000000..163c6be5a --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleUtmstack.java @@ -0,0 +1,48 @@ +package com.park.utmstack.domain.application_modules.factory.impl; + +import com.park.utmstack.domain.application_modules.UtmModule; +import com.park.utmstack.domain.application_modules.enums.ModuleName; +import com.park.utmstack.domain.application_modules.factory.IModule; +import com.park.utmstack.domain.application_modules.types.ModuleConfigurationKey; +import com.park.utmstack.domain.application_modules.types.ModuleRequirement; +import com.park.utmstack.service.application_modules.UtmModuleService; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; + +@Component +public class ModuleUtmstack implements IModule { + private static final String CLASSNAME = "ModuleUtmstack"; + + private final UtmModuleService moduleService; + + public ModuleUtmstack(UtmModuleService moduleService) { + this.moduleService = moduleService; + } + + @Override + public UtmModule getDetails(Long serverId) throws Exception { + final String ctx = CLASSNAME + ".getDetails"; + try { + return moduleService.findByServerIdAndModuleName(serverId, ModuleName.UTMSTACK); + } catch (Exception e) { + throw new Exception(ctx + ": " + e.getMessage()); + } + } + + @Override + public List checkRequirements(Long serverId) throws Exception { + return Collections.emptyList(); + } + + @Override + public List getConfigurationKeys(Long groupId) throws Exception { + return Collections.emptyList(); + } + + @Override + public ModuleName getName() { + return ModuleName.UTMSTACK; + } +} diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleVMWare.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleVMWare.java index c1ce0814e..0dcf2fd93 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleVMWare.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleVMWare.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.VMWARE; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleWindowsAgent.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleWindowsAgent.java index 6016fe40a..035207eac 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleWindowsAgent.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/factory/impl/ModuleWindowsAgent.java @@ -40,4 +40,9 @@ public List checkRequirements(Long serverId) throws Exception public List getConfigurationKeys(Long groupId) throws Exception { return Collections.emptyList(); } + + @Override + public ModuleName getName() { + return ModuleName.WINDOWS_AGENT; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/validators/UtmModuleConfigValidator.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/validators/UtmModuleConfigValidator.java index ed6f41af3..e9b57393e 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/validators/UtmModuleConfigValidator.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/validators/UtmModuleConfigValidator.java @@ -21,47 +21,40 @@ public class UtmModuleConfigValidator { private final UtmModuleGroupConfigurationRepository moduleGroupConfigurationRepository; private final UtmStackConnectionService utmStackConnectionService; - /** - * Validates if the given configuration allows a successful connection to UTMStack. - * - * @param keys A list of configuration keys that should include at least `ipAddress` and `connectionKey`. - * @return true if login and ping are successful; false otherwise. - * @throws Exception If required fields are missing or the connection fails. - */ public boolean validate(UtmModule module, List keys) throws Exception { if (keys.isEmpty()) return false; - List configurations = moduleGroupConfigurationRepository - .findAllByGroupId(keys.get(0).getGroupId()) - .stream() - .map(c -> { - if (this.containsConfigInKeys(keys, c.getConfKey()) != null) { - return this.containsConfigInKeys(keys, c.getConfKey()); - } else { - if ("password".equals(c.getConfDataType())) { - c.setConfValue(CipherUtil.decrypt( - c.getConfValue(), - System.getenv(Constants.ENV_ENCRYPTION_KEY) - )); - } - return c; - } - }) - .collect(Collectors.toList()); + List dbConfigs = moduleGroupConfigurationRepository + .findAllByGroupId(keys.get(0).getGroupId()); + + List configDTOs = dbConfigs.stream() + .map(dbConf -> { + UtmModuleGroupConfiguration override = findInKeys(keys, dbConf.getConfKey()); + UtmModuleGroupConfiguration source = override != null ? override : dbConf; - List configDTOs = configurations.stream() - .map(entity -> new UtmModuleGroupConfDTO(entity.getConfKey(), entity.getConfValue())) - .collect(Collectors.toList()); + return new UtmModuleGroupConfDTO( + source.getConfKey(), + decryptIfNeeded(source.getConfDataType(), source.getConfValue()) + ); + }) + .toList(); UtmModuleGroupConfWrapperDTO body = new UtmModuleGroupConfWrapperDTO(configDTOs); - return utmStackConnectionService.testConnection(module.getModuleName().name(),body); + return utmStackConnectionService.testConnection(module.getModuleName().name(), body); } - private UtmModuleGroupConfiguration containsConfigInKeys(List keys, String confKey) { + private UtmModuleGroupConfiguration findInKeys(List keys, String confKey) { return keys.stream() - .filter(key -> key.getConfKey().equals(confKey)) + .filter(k -> k.getConfKey().equals(confKey)) .findFirst() .orElse(null); } + + private String decryptIfNeeded(String dataType, String value) { + if (Constants.CONF_TYPE_PASSWORD.equals(dataType) || Constants.CONF_TYPE_FILE.equals(dataType)) { + return CipherUtil.decrypt(value, System.getenv(Constants.ENV_ENCRYPTION_KEY)); + } + return value; + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/chart_builder/types/query/OperatorType.java b/backend/src/main/java/com/park/utmstack/domain/chart_builder/types/query/OperatorType.java index 0dd6ac04d..4a3298667 100644 --- a/backend/src/main/java/com/park/utmstack/domain/chart_builder/types/query/OperatorType.java +++ b/backend/src/main/java/com/park/utmstack/domain/chart_builder/types/query/OperatorType.java @@ -10,6 +10,7 @@ public enum OperatorType { IS_ONE_OF, IS_NOT_ONE_OF, IS_ONE_OF_TERMS, + IS_ONE_OF_TERMS_OR, EXIST, DOES_NOT_EXIST, IS_BETWEEN, @@ -22,4 +23,5 @@ public enum OperatorType { NOT_START_WITH, IS_GREATER_THAN, IS_LESS_THAN_OR_EQUALS + } diff --git a/backend/src/main/java/com/park/utmstack/domain/correlation/rules/UtmGroupRulesDataType.java b/backend/src/main/java/com/park/utmstack/domain/correlation/rules/UtmGroupRulesDataType.java index 828595cd7..cc2766e66 100644 --- a/backend/src/main/java/com/park/utmstack/domain/correlation/rules/UtmGroupRulesDataType.java +++ b/backend/src/main/java/com/park/utmstack/domain/correlation/rules/UtmGroupRulesDataType.java @@ -1,5 +1,7 @@ package com.park.utmstack.domain.correlation.rules; +import com.park.utmstack.domain.correlation.config.UtmDataTypes; + import javax.persistence.*; import java.io.Serializable; import java.time.Clock; @@ -11,54 +13,19 @@ @Entity @Table(name = "utm_group_rules_data_type") public class UtmGroupRulesDataType implements Serializable { - - private static final long serialVersionUID = 1L; - - @Id - @Column(name = "rule_id", nullable = false) - private Long ruleId; - - @Id - @Column(name = "data_type_id", nullable = false) - private Long dataTypeId; + @EmbeddedId + private UtmGroupRulesDataTypeKey id; @Column(name = "last_update", nullable = false) private Instant lastUpdate; - public UtmGroupRulesDataType(Long id, Long ruleId, Long dataTypeId, Instant lastUpdate) { - this.ruleId = ruleId; - this.dataTypeId = dataTypeId; - this.lastUpdate = lastUpdate; - } - - public UtmGroupRulesDataType() { - } - - public Long getRuleId() { - return ruleId; - } - - public void setRuleId(Long ruleId) { - this.ruleId = ruleId; - } - - public Long getDataTypeId() { - return dataTypeId; - } - - public void setDataTypeId(Long dataTypeId) { - this.dataTypeId = dataTypeId; - } - - public Instant getLastUpdate() { - return lastUpdate; - } - - public void setLastUpdate() { - this.lastUpdate = Instant.now(Clock.systemUTC()); - } + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("ruleId") + @JoinColumn(name = "rule_id", insertable = false, updatable = false) + private UtmCorrelationRules rule; - public void setLastUpdate(Instant lastUpdate) { - this.lastUpdate = lastUpdate; - } + @ManyToOne(fetch = FetchType.LAZY) + @MapsId("dataTypeId") + @JoinColumn(name = "data_type_id", insertable = false, updatable = false) + private UtmDataTypes dataType; } diff --git a/backend/src/main/java/com/park/utmstack/domain/correlation/rules/UtmGroupRulesDataTypeKey.java b/backend/src/main/java/com/park/utmstack/domain/correlation/rules/UtmGroupRulesDataTypeKey.java new file mode 100644 index 000000000..9336bb8ca --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/correlation/rules/UtmGroupRulesDataTypeKey.java @@ -0,0 +1,29 @@ +package com.park.utmstack.domain.correlation.rules; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +public class UtmGroupRulesDataTypeKey implements Serializable { + @Column(name = "rule_id") + private Long ruleId; + + @Column(name = "data_type_id") + private Long dataTypeId; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UtmGroupRulesDataTypeKey that)) return false; + return Objects.equals(ruleId, that.ruleId) && + Objects.equals(dataTypeId, that.dataTypeId); + } + + @Override + public int hashCode() { + return Objects.hash(ruleId, dataTypeId); + } +} + diff --git a/backend/src/main/java/com/park/utmstack/domain/idp_provider/CustomOidcUser.java b/backend/src/main/java/com/park/utmstack/domain/idp_provider/CustomOidcUser.java new file mode 100644 index 000000000..26bb067dc --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/idp_provider/CustomOidcUser.java @@ -0,0 +1,52 @@ +package com.park.utmstack.domain.idp_provider; + +import com.park.utmstack.domain.User; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +public class CustomOidcUser implements OidcUser { + + private final OidcUser oidcUser; + private final User user; + + @Override + public Map getAttributes() { + return oidcUser.getAttributes(); + } + + @Override + public Collection getAuthorities() { + return user.getAuthorities().stream().map(role -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toList()); + } + + @Override + public String getName() { + return user.getLogin(); + } + + @Override + public Map getClaims() { + return oidcUser.getClaims(); + } + + @Override + public OidcUserInfo getUserInfo() { + return oidcUser.getUserInfo(); + } + + @Override + public OidcIdToken getIdToken() { + return oidcUser.getIdToken(); + } +} + diff --git a/backend/src/main/java/com/park/utmstack/domain/idp_provider/IdentityProviderConfig.java b/backend/src/main/java/com/park/utmstack/domain/idp_provider/IdentityProviderConfig.java new file mode 100644 index 000000000..347239a9a --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/idp_provider/IdentityProviderConfig.java @@ -0,0 +1,65 @@ +package com.park.utmstack.domain.idp_provider; + +import com.park.utmstack.domain.idp_provider.enums.ClientAuthMethod; +import com.park.utmstack.domain.idp_provider.enums.ProviderType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "utm_identity_provider_config") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class IdentityProviderConfig { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @EqualsAndHashCode.Include + private Long id; + + private String name; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ProviderType providerType; + + @Column(nullable = false) + private String clientId; + + @Column(nullable = false) + private String clientSecret; + + @Column(nullable = false) + private String authUri; + + @Column(nullable = false) + private String tokenUri; + + @Column(nullable = false) + private String redirectUri; + + private String userInfoUri; + private String jwksUri; + + @Enumerated(EnumType.STRING) + private ClientAuthMethod clientAuthMethod; + + private String scopes; + private String allowedDomains; + + @Column(nullable = false) + private Boolean active; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @Column(nullable = false) + private LocalDateTime updatedAt; + +} diff --git a/backend/src/main/java/com/park/utmstack/domain/idp_provider/enums/ClientAuthMethod.java b/backend/src/main/java/com/park/utmstack/domain/idp_provider/enums/ClientAuthMethod.java new file mode 100644 index 000000000..6e746e152 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/idp_provider/enums/ClientAuthMethod.java @@ -0,0 +1,19 @@ +package com.park.utmstack.domain.idp_provider.enums; + +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +public enum ClientAuthMethod { + CLIENT_SECRET_BASIC, + CLIENT_SECRET_POST, + NONE, + PRIVATE_KEY_JWT; + + public ClientAuthenticationMethod toSpringMethod() { + return new ClientAuthenticationMethod(this.name()); + } + + public static ClientAuthMethod from(String value) { + return ClientAuthMethod.valueOf(value.toUpperCase()); + } +} + diff --git a/backend/src/main/java/com/park/utmstack/domain/idp_provider/enums/ProviderType.java b/backend/src/main/java/com/park/utmstack/domain/idp_provider/enums/ProviderType.java new file mode 100644 index 000000000..a0b4a2ada --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/idp_provider/enums/ProviderType.java @@ -0,0 +1,10 @@ +package com.park.utmstack.domain.idp_provider.enums; + +public enum ProviderType { + GOOGLE, + MICROSOFT; + + public static ProviderType from(String value) { + return ProviderType.valueOf(value.toUpperCase()); + } +} diff --git a/backend/src/main/java/com/park/utmstack/domain/index_template/IndexTemplate.java b/backend/src/main/java/com/park/utmstack/domain/index_template/IndexTemplate.java index 0d040a719..6a80cb936 100644 --- a/backend/src/main/java/com/park/utmstack/domain/index_template/IndexTemplate.java +++ b/backend/src/main/java/com/park/utmstack/domain/index_template/IndexTemplate.java @@ -2,11 +2,15 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@Setter +@Getter @JsonInclude(JsonInclude.Include.NON_NULL) public class IndexTemplate { @@ -24,30 +28,6 @@ private IndexTemplate(List indexPatterns, Integer priority, Template tem this.template = template; } - public List getIndexPatterns() { - return indexPatterns; - } - - public void setIndexPatterns(List indexPatterns) { - this.indexPatterns = indexPatterns; - } - - public Integer getPriority() { - return priority; - } - - public void setPriority(Integer priority) { - this.priority = priority; - } - - public Template getTemplate() { - return template; - } - - public void setTemplate(Template template) { - this.template = template; - } - public static Builder builder() { return new Builder(); } diff --git a/backend/src/main/java/com/park/utmstack/domain/index_template/IndexTemplateSettings.java b/backend/src/main/java/com/park/utmstack/domain/index_template/IndexTemplateSettings.java index 929b767fd..a49d4abd3 100644 --- a/backend/src/main/java/com/park/utmstack/domain/index_template/IndexTemplateSettings.java +++ b/backend/src/main/java/com/park/utmstack/domain/index_template/IndexTemplateSettings.java @@ -2,7 +2,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +@Getter @JsonInclude(value = JsonInclude.Include.NON_NULL) public class IndexTemplateSettings { @@ -18,34 +20,18 @@ public class IndexTemplateSettings { @JsonProperty("number_of_replicas") private Integer numberOfReplicas; - public String getIndexPolicyId() { - return indexPolicyId; - } - public void setIndexPolicyId(String indexPolicyId) { this.indexPolicyId = indexPolicyId; } - public Integer getTotalFieldsLimit() { - return totalFieldsLimit; - } - public void setTotalFieldsLimit(Integer totalFieldsLimit) { this.totalFieldsLimit = totalFieldsLimit; } - public Integer getNumberOfShards() { - return numberOfShards; - } - public void setNumberOfShards(Integer numberOfShards) { this.numberOfShards = numberOfShards; } - public Integer getNumberOfReplicas() { - return numberOfReplicas; - } - public void setNumberOfReplicas(Integer numberOfReplicas) { this.numberOfReplicas = numberOfReplicas; } diff --git a/backend/src/main/java/com/park/utmstack/domain/index_template/Template.java b/backend/src/main/java/com/park/utmstack/domain/index_template/Template.java index bdb46b739..fd185783e 100644 --- a/backend/src/main/java/com/park/utmstack/domain/index_template/Template.java +++ b/backend/src/main/java/com/park/utmstack/domain/index_template/Template.java @@ -1,9 +1,9 @@ package com.park.utmstack.domain.index_template; +import lombok.Getter; + +@Getter public class Template { private final IndexTemplateSettings settings = new IndexTemplateSettings(); - public IndexTemplateSettings getSettings() { - return settings; - } } diff --git a/backend/src/main/java/com/park/utmstack/domain/network_scan/UtmNetworkScan.java b/backend/src/main/java/com/park/utmstack/domain/network_scan/UtmNetworkScan.java index 3d771747e..a5614a0fa 100644 --- a/backend/src/main/java/com/park/utmstack/domain/network_scan/UtmNetworkScan.java +++ b/backend/src/main/java/com/park/utmstack/domain/network_scan/UtmNetworkScan.java @@ -140,12 +140,6 @@ public class UtmNetworkScan implements Serializable { @OneToMany(mappedBy = "asset", fetch = FetchType.LAZY) private List metrics; - @OneToMany(mappedBy = "assetName", fetch = FetchType.LAZY) - private List dataInputSourceList; - - @OneToMany(mappedBy = "assetIp", fetch = FetchType.LAZY) - private List dataInputIpList; - @OneToOne @JoinColumn(name = "group_id", referencedColumnName = "id", insertable = false, updatable = false) private UtmAssetGroup assetGroup; diff --git a/backend/src/main/java/com/park/utmstack/domain/network_scan/enums/CompositePropertyFilter.java b/backend/src/main/java/com/park/utmstack/domain/network_scan/enums/CompositePropertyFilter.java index 615f77fbf..eb959dd36 100644 --- a/backend/src/main/java/com/park/utmstack/domain/network_scan/enums/CompositePropertyFilter.java +++ b/backend/src/main/java/com/park/utmstack/domain/network_scan/enums/CompositePropertyFilter.java @@ -8,7 +8,8 @@ public enum CompositePropertyFilter implements Property { RULE_DATA_TYPES(new String[]{"u0.dataType"}, "UtmCorrelationRules", new String[]{"p.dataTypes"}), - DATA_TYPES(new String[]{"u0.dataType", "u1.dataType"}, "UtmNetworkScan", new String[]{"p.dataInputSourceList", "p.dataInputIpList"}); + + DATA_TYPES(new String[]{"u.dataType"}, "UtmNetworkScan", new String[]{}); private final String[] propertyNames; @@ -29,15 +30,17 @@ public String getPropertyName() { @Override public String getJoinTable() { - StringBuilder joinClause = new StringBuilder(); + if (this == DATA_TYPES) { + return " JOIN UtmDataInputStatus u ON u.source = p.assetIp OR u.source = p.assetName"; + } + StringBuilder joinClause = new StringBuilder(); for (int i = 0; i < joinTables.length; i++) { joinClause.append(" LEFT JOIN ") .append(joinTables[i]) .append(" u") .append(i); } - return joinClause.toString(); } } diff --git a/backend/src/main/java/com/park/utmstack/domain/reports/types/IncidentType.java b/backend/src/main/java/com/park/utmstack/domain/reports/types/IncidentType.java index c54a2dfe8..f7ae469b8 100644 --- a/backend/src/main/java/com/park/utmstack/domain/reports/types/IncidentType.java +++ b/backend/src/main/java/com/park/utmstack/domain/reports/types/IncidentType.java @@ -1,20 +1,20 @@ package com.park.utmstack.domain.reports.types; import com.park.utmstack.domain.incident_response.UtmIncidentJob; -import com.park.utmstack.domain.shared_types.AlertType; +import com.park.utmstack.domain.shared_types.alert.UtmAlert; import java.util.List; public class IncidentType { - private AlertType incident; + private UtmAlert incident; private List srcResponses; private List destResponses; - public AlertType getIncident() { + public UtmAlert getIncident() { return incident; } - public void setIncident(AlertType incident) { + public void setIncident(UtmAlert incident) { this.incident = incident; } diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/AlertType.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/AlertType.java deleted file mode 100644 index fa9b328f0..000000000 --- a/backend/src/main/java/com/park/utmstack/domain/shared_types/AlertType.java +++ /dev/null @@ -1,424 +0,0 @@ -package com.park.utmstack.domain.shared_types; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.park.utmstack.util.enums.AlertStatus; -import org.springframework.util.StringUtils; - -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Locale; - -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public class AlertType { - @JsonProperty("@timestamp") - private String timestamp; - private String id; - private Integer status; - private AlertStatus statusLabel; - private String statusObservation; - private Boolean isIncident; - private IncidentDetail incidentDetail; - private String name; - private String category; - private Integer severity; - private String severityLabel; - private String protocol; - private String description; - private String solution; - private String tactic; - private List reference; - private String dataType; - private String dataSource; - private Host source; - private Host destination; - private List logs; - private List tags; - private String notes; - @JsonProperty("TagRulesApplied") - private List tagRulesApplied; - - public String getTimestamp() { - return timestamp; - } - - public Instant getTimestampAsInstant() { - if (StringUtils.hasText(timestamp)) - return Instant.parse(timestamp); - return null; - } - - public String getTimestampFormatted() { - try { - if (!StringUtils.hasText(timestamp)) - return null; - return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withLocale(Locale.getDefault()).withZone( - ZoneId.systemDefault()).format(Instant.parse(timestamp)); - } catch (Exception e) { - return null; - } - } - - public void setTimestamp(String timestamp) { - this.timestamp = timestamp; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public Integer getStatus() { - return status; - } - - public void setStatus(Integer status) { - this.status = status; - } - - public AlertStatus getStatusLabel() { - return statusLabel; - } - - public void setStatusLabel(AlertStatus statusLabel) { - this.statusLabel = statusLabel; - } - - public String getStatusObservation() { - return statusObservation; - } - - public void setStatusObservation(String statusObservation) { - this.statusObservation = statusObservation; - } - - public Boolean getIncident() { - return isIncident != null && isIncident; - } - - public void setIncident(Boolean incident) { - isIncident = incident; - } - - public IncidentDetail getIncidentDetail() { - return incidentDetail; - } - - public void setIncidentDetail(IncidentDetail incidentDetail) { - this.incidentDetail = incidentDetail; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } - - public Integer getSeverity() { - return severity; - } - - public void setSeverity(Integer severity) { - this.severity = severity; - } - - public String getSeverityLabel() { - return severityLabel; - } - - public void setSeverityLabel(String severityLabel) { - this.severityLabel = severityLabel; - } - - public String getProtocol() { - return protocol; - } - - public void setProtocol(String protocol) { - this.protocol = protocol; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public String getSolution() { - return solution; - } - - public void setSolution(String solution) { - this.solution = solution; - } - - public String getTactic() { - return tactic; - } - - public void setTactic(String tactic) { - this.tactic = tactic; - } - - public List getReference() { - return reference; - } - - public void setReference(List reference) { - this.reference = reference; - } - - public String getDataType() { - return dataType; - } - - public void setDataType(String dataType) { - this.dataType = dataType; - } - - public String getDataSource() { - return dataSource; - } - - public void setDataSource(String dataSource) { - this.dataSource = dataSource; - } - - public Host getSource() { - return source; - } - - public void setSource(Host source) { - this.source = source; - } - - public Host getDestination() { - return destination; - } - - public void setDestination(Host destination) { - this.destination = destination; - } - - public List getLogs() { - return logs; - } - - public void setLogs(List logs) { - this.logs = logs; - } - - public List getTags() { - return tags; - } - - public void setTags(List tags) { - this.tags = tags; - } - - public String getNotes() { - return notes; - } - - public void setNotes(String notes) { - this.notes = notes; - } - - public List getTagRulesApplied() { - return tagRulesApplied; - } - - public void setTagRulesApplied(List tagRulesApplied) { - this.tagRulesApplied = tagRulesApplied; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Host { - private String user; - private String host; - private String ip; - private Integer port; - private String country; - private String countryCode; - private String city; - private Float[] coordinates; - private Integer accuracyRadius; - private Integer asn; - private String aso; - private Boolean isSatelliteProvider; - private Boolean isAnonymousProxy; - - public String getUser() { - return user; - } - - public void setUser(String user) { - this.user = user; - } - - public String getHost() { - return host; - } - - public void setHost(String host) { - this.host = host; - } - - public String getIp() { - return ip; - } - - public void setIp(String ip) { - this.ip = ip; - } - - public Integer getPort() { - return port; - } - - public void setPort(Integer port) { - this.port = port; - } - - public String getCountry() { - return country; - } - - public void setCountry(String country) { - this.country = country; - } - - public String getCountryCode() { - return countryCode; - } - - public void setCountryCode(String countryCode) { - this.countryCode = countryCode; - } - - public String getCity() { - return city; - } - - public void setCity(String city) { - this.city = city; - } - - public Float[] getCoordinates() { - return coordinates; - } - - public void setCoordinates(Float[] coordinates) { - this.coordinates = coordinates; - } - - public Integer getAccuracyRadius() { - return accuracyRadius; - } - - public void setAccuracyRadius(Integer accuracyRadius) { - this.accuracyRadius = accuracyRadius; - } - - public Integer getAsn() { - return asn; - } - - public void setAsn(Integer asn) { - this.asn = asn; - } - - public String getAso() { - return aso; - } - - public void setAso(String aso) { - this.aso = aso; - } - - public Boolean getSatelliteProvider() { - return isSatelliteProvider; - } - - public void setSatelliteProvider(Boolean satelliteProvider) { - isSatelliteProvider = satelliteProvider; - } - - public Boolean getAnonymousProxy() { - return isAnonymousProxy; - } - - public void setAnonymousProxy(Boolean anonymousProxy) { - isAnonymousProxy = anonymousProxy; - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class IncidentDetail { - private String createdBy; - private String incidentName; - - private Integer incidentId; - private String creationDate; - private String source; - - public String getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(String createdBy) { - this.createdBy = createdBy; - } - - public String getIncidentName() { - return incidentName; - } - - public void setIncidentName(String incidentName) { - this.incidentName = incidentName; - } - - public Integer getIncidentId() { - return incidentId; - } - - public void setIncidentId(Integer incidentId) { - this.incidentId = incidentId; - } - - public String getCreationDate() { - return creationDate; - } - - public void setCreationDate(String creationDate) { - this.creationDate = creationDate; - } - - public String getSource() { - return source; - } - - public void setSource(String source) { - this.source = source; - } - } -} diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java new file mode 100644 index 000000000..ed1a2af7f --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/ApplicationLayer.java @@ -0,0 +1,14 @@ +package com.park.utmstack.domain.shared_types; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum ApplicationLayer { + SERVICE ("SERVICE"), + API ("API"), + CONTROLLER ("CONTROLLER"); + + private final String value; +} diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/Geolocation.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/Geolocation.java new file mode 100644 index 000000000..3f9e9c900 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/Geolocation.java @@ -0,0 +1,37 @@ +package com.park.utmstack.domain.shared_types; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Geolocation { + + @JsonProperty("country") + private String country; + + @JsonProperty("city") + private String city; + + @JsonProperty("latitude") + private Double latitude; + + @JsonProperty("longitude") + private Double longitude; + + @JsonProperty("asn") + private Long asn; + + @JsonProperty("aso") + private String aso; + + @JsonProperty("countryCode") + private String countryCode; + + @JsonProperty("accuracy") + private Integer accuracy; +} + diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/ComplianceValues.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/ComplianceValues.java new file mode 100644 index 000000000..b1abcc609 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/ComplianceValues.java @@ -0,0 +1,14 @@ +package com.park.utmstack.domain.shared_types.alert; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; +import java.util.List; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ComplianceValues { + private List values; +} + diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/DiskInfo.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/DiskInfo.java new file mode 100644 index 000000000..4815410ff --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/DiskInfo.java @@ -0,0 +1,15 @@ +package com.park.utmstack.domain.shared_types.alert; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class DiskInfo { + private String name; + private Long totalSpace; + private Integer usedPercent; +} + diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/Event.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/Event.java new file mode 100644 index 000000000..2c28cb807 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/Event.java @@ -0,0 +1,43 @@ +package com.park.utmstack.domain.shared_types.alert; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Event { + + private String id; + + @JsonProperty("@timestamp") + private String timestamp; + + private String deviceTime; + private String dataType; + private String dataSource; + private String tenantId; + private String tenantName; + private String raw; + + private Map log; + + private Side target; + private Side origin; + + private String protocol; + private String connectionStatus; + private Integer statusCode; + private String actionResult; + private String action; + private String severity; + + private List errors; + private Map compliance; +} + diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/Impact.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/Impact.java new file mode 100644 index 000000000..9d34a4105 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/Impact.java @@ -0,0 +1,15 @@ +package com.park.utmstack.domain.shared_types.alert; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Impact { + private Integer confidentiality; + private Integer integrity; + private Integer availability; +} + diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/IncidentDetail.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/IncidentDetail.java new file mode 100644 index 000000000..380996951 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/IncidentDetail.java @@ -0,0 +1,15 @@ +package com.park.utmstack.domain.shared_types.alert; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class IncidentDetail { + private String createdBy; + private String observation; + private String creationDate; + private String source; +} \ No newline at end of file diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/Side.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/Side.java new file mode 100644 index 000000000..59c376451 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/Side.java @@ -0,0 +1,118 @@ +package com.park.utmstack.domain.shared_types.alert; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.park.utmstack.domain.shared_types.Geolocation; +import lombok.Data; +import java.util.List; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Side { + + // Network traffic attributes + private Double bytesSent; + private Double bytesReceived; + private Long packagesSent; + private Long packagesReceived; + + // Network identification attributes + private String ip; + private String host; + private String user; + private String group; + private Integer port; + private String domain; + private String mac; + private Geolocation geolocation; + private String url; + private String cidr; + + // Certificate and fingerprint attributes + private String certificateFingerprint; + private String ja3Fingerprint; + private String jarmFingerprint; + private String sshBanner; + private String sshFingerprint; + + // Web attributes + private String cookie; + private String jabberId; + + // Email attributes + private String email; + private String dkim; + private String dkimSignature; + private String emailAddress; + private String emailBody; + private String emailDisplayName; + private String emailSubject; + private String emailThreadIndex; + private String emailXMailer; + + // WHOIS attributes + private String whoisRegistrant; + private String whoisRegistrar; + + // Process-related attributes + private String process; + private String processState; + private String command; + private String windowsScheduledTask; + private String windowsServiceDisplayName; + private String windowsServiceName; + + // File-related attributes + private String file; + private String path; + private String filename; + private String sizeInBytes; + private String mimeType; + + // Hash-related attributes + private String hash; + private String authentihash; + private String cdhash; + private String md5; + private String sha1; + private String sha224; + private String sha256; + private String sha384; + private String sha3224; + private String sha3256; + private String sha3384; + private String sha3512; + private String sha512; + private String sha512224; + private String sha512256; + private String hex; + private String base64; + + // System-related attributes + private String operatingSystem; + private String chromeExtension; + private String mobileAppId; + + // Vulnerability-related attributes + private String cpe; + private String cve; + + // Malware-related attributes + private String malware; + private String malwareFamily; + private String malwareType; + + // Key-related attributes + private String pgpPrivateKey; + private String pgpPublicKey; + + // Resources attributes + private Long connections; + private Integer usedCpuPercent; + private Integer usedMemPercent; + private Integer totalCpuUnits; + private Long totalMem; + private List disks; +} + diff --git a/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/UtmAlert.java b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/UtmAlert.java new file mode 100644 index 000000000..362b3f2bb --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/shared_types/alert/UtmAlert.java @@ -0,0 +1,133 @@ +package com.park.utmstack.domain.shared_types.alert; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.park.utmstack.util.enums.AlertStatus; +import lombok.Getter; +import lombok.Setter; +import org.springframework.util.StringUtils; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Locale; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@Getter +@Setter +public class UtmAlert { + @JsonProperty("@timestamp") + private String timestamp; + + @JsonProperty("id") + private String id; + + @JsonProperty("parentId") + private String parentId; + + @JsonProperty("status") + private Integer status; + + @JsonProperty("statusLabel") + private AlertStatus statusLabel; + + @JsonProperty("statusObservation") + private String statusObservation; + + @JsonProperty("isIncident") + private Boolean isIncident; + + @JsonProperty("incidentDetail") + private IncidentDetail incidentDetail; + + @JsonProperty("name") + private String name; + + @JsonProperty("category") + private String category; + + @JsonProperty("severity") + private Integer severity; + + @JsonProperty("severityLabel") + private String severityLabel; + + @JsonProperty("description") + private String description; + + @JsonProperty("solution") + private String solution; + + @JsonProperty("technique") + private String technique; + + @JsonProperty("reference") + private List reference; + + @JsonProperty("dataType") + private String dataType; + + @JsonProperty("impact") + private Impact impact; + + @JsonProperty("impactScore") + private Integer impactScore; + + @JsonProperty("dataSource") + private String dataSource; + + @JsonProperty("adversary") + private Side adversary; + + @JsonProperty("target") + private Side target; + + @JsonProperty("events") + private List events; + + @JsonProperty("lastEvent") + private Event lastEvent; + + @JsonProperty("tags") + private List tags; + + @JsonProperty("notes") + private String notes; + + @JsonProperty("tagRulesApplied") + private List tagRulesApplied; + + @JsonProperty("deduplicatedBy") + private List deduplicatedBy; + + @JsonProperty("logs") + private List logs; + + private String assetGroupName; + + private Long assetGroupId; + + public Instant getTimestampAsInstant() { + if (StringUtils.hasText(timestamp)) + return Instant.parse(timestamp); + return null; + } + + public String getTimestampFormatted() { + try { + if (!StringUtils.hasText(timestamp)) + return null; + return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withLocale(Locale.getDefault()).withZone( + ZoneId.systemDefault()).format(Instant.parse(timestamp)); + } catch (Exception e) { + return null; + } + } + + public Boolean getIncident() { + return isIncident != null && isIncident; + } +} diff --git a/backend/src/main/java/com/park/utmstack/domain/tfa/TfaSetupState.java b/backend/src/main/java/com/park/utmstack/domain/tfa/TfaSetupState.java index c7c14a172..7ecd9b4e1 100644 --- a/backend/src/main/java/com/park/utmstack/domain/tfa/TfaSetupState.java +++ b/backend/src/main/java/com/park/utmstack/domain/tfa/TfaSetupState.java @@ -9,10 +9,14 @@ public class TfaSetupState { private long expiresAt; private long setupStartedAt; + private long lastChallengeAt; + private static final long COOLDOWN_MS = 28_000; + public TfaSetupState(String secret, long expiresAt) { this.secret = secret; this.expiresAt = expiresAt; this.setupStartedAt = System.currentTimeMillis(); + this.lastChallengeAt = 0; } public boolean isExpired() { @@ -23,6 +27,19 @@ public long getRemainingSeconds() { long remaining = expiresAt - System.currentTimeMillis(); return Math.max(remaining / 1000, 0); } + + public boolean canRequestChallenge() { + return System.currentTimeMillis() - lastChallengeAt >= COOLDOWN_MS; + } + + public long getCooldownRemainingSeconds() { + long remaining = (lastChallengeAt + COOLDOWN_MS) - System.currentTimeMillis(); + return Math.max(remaining / 1000, 0); + } + + public void markChallengeRequested() { + this.lastChallengeAt = System.currentTimeMillis(); + } } diff --git a/backend/src/main/java/com/park/utmstack/domain/tfa/TfaStage.java b/backend/src/main/java/com/park/utmstack/domain/tfa/TfaStage.java new file mode 100644 index 000000000..1bd87893f --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/domain/tfa/TfaStage.java @@ -0,0 +1,7 @@ +package com.park.utmstack.domain.tfa; + +public enum TfaStage { + INIT, + VERIFY, + COMPLETE +} diff --git a/backend/src/main/java/com/park/utmstack/event_processor/EventProcessorManagerService.java b/backend/src/main/java/com/park/utmstack/event_processor/EventProcessorManagerService.java index d210b4f8a..2736efaf3 100644 --- a/backend/src/main/java/com/park/utmstack/event_processor/EventProcessorManagerService.java +++ b/backend/src/main/java/com/park/utmstack/event_processor/EventProcessorManagerService.java @@ -62,8 +62,8 @@ public void decryptModuleConfig (UtmModule module){ Set groups = module.getModuleGroups(); groups.forEach((gp) -> { gp.getModuleGroupConfigurations().forEach((gpc) -> { - if ((gpc.getConfDataType().equals("password") && StringUtils.hasText(gpc.getConfValue())) - || (gpc.getConfDataType().equals("file") && StringUtils.hasText(gpc.getConfValue())) && typeFileNeedsDecryptList.contains(module.getModuleName())) { + if ((gpc.getConfDataType().equals(Constants.CONF_TYPE_PASSWORD) && StringUtils.hasText(gpc.getConfValue())) + || (gpc.getConfDataType().equals(Constants.CONF_TYPE_FILE) && StringUtils.hasText(gpc.getConfValue())) && typeFileNeedsDecryptList.contains(module.getModuleName())) { gpc.setConfValue(CipherUtil.decrypt(gpc.getConfValue(), System.getenv(Constants.ENV_ENCRYPTION_KEY))); } }); diff --git a/backend/src/main/java/com/park/utmstack/loggin/LogContextBuilder.java b/backend/src/main/java/com/park/utmstack/loggin/LogContextBuilder.java new file mode 100644 index 000000000..666718072 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/loggin/LogContextBuilder.java @@ -0,0 +1,79 @@ +package com.park.utmstack.loggin; + +import com.park.utmstack.config.Constants; +import com.park.utmstack.security.SecurityUtils; +import com.park.utmstack.util.RequestContextUtils; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@Component +public class LogContextBuilder { + + public Map buildArgs(Exception e) { + return RequestContextUtils.getCurrentRequest() + .map(request -> buildArgs(e, request)) + .orElse(buildFallbackArgs(e)); + } + + public Map buildArgs() { + return RequestContextUtils.getCurrentRequest() + .map(this::buildArgs) + .orElse(buildFallbackArgs(null)); + } + + public Map buildArgs(String methodName, String duration) { + Map args = new HashMap<>(); + args.put(Constants.USERNAME_KEY, SecurityUtils.getCurrentUserLogin().orElse("anonymous")); + args.put(Constants.CONTEXT_KEY, methodName); + args.put(Constants.DURATION_KEY, duration); + args.put(Constants.TRACE_ID_KEY, MDC.get(Constants.TRACE_ID_KEY)); + return args; + } + + public Map buildArgs(HttpServletRequest request) { + Map args = new HashMap<>(); + args.put(Constants.USERNAME_KEY, SecurityUtils.getCurrentUserLogin().orElse("anonymous")); + args.put(Constants.METHOD_KEY, request.getMethod()); + args.put(Constants.PATH_KEY, request.getRequestURI()); + args.put(Constants.REMOTE_ADDR_KEY, request.getRemoteAddr()); + args.put(Constants.CONTEXT_KEY, MDC.get(Constants.CONTEXT_KEY)); + args.put(Constants.TRACE_ID_KEY, MDC.get(Constants.TRACE_ID_KEY)); + return args; + } + + public Map buildArgs(Exception e, HttpServletRequest request) { + Map args = buildArgs(request); + if (e != null && e.getCause() != null) { + args.put(Constants.CAUSE_KEY, e.getCause().toString()); + } + return args; + } + + public Map buildArgs(Map extra) { + Map base = buildArgs(); + return mergeArgs(base, extra); + } + + private Map buildFallbackArgs(Exception e) { + Map args = new HashMap<>(); + args.put(Constants.USERNAME_KEY, SecurityUtils.getCurrentUserLogin().orElse("anonymous")); + args.put(Constants.CONTEXT_KEY, MDC.get(Constants.CONTEXT_KEY)); + args.put(Constants.TRACE_ID_KEY, MDC.get(Constants.TRACE_ID_KEY)); + if (e != null && e.getCause() != null) { + args.put(Constants.CAUSE_KEY, e.getCause().toString()); + } + return args; + } + + private Map mergeArgs(Map base, Map extra) { + if (extra != null) { + base.putAll(extra); + } + return base; + } +} diff --git a/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java b/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java new file mode 100644 index 000000000..444f22576 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/loggin/api_key/ApiKeyUsageLoggingService.java @@ -0,0 +1,139 @@ +package com.park.utmstack.loggin.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; +import com.park.utmstack.domain.application_events.enums.ApplicationEventType; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.application_events.ApplicationEventService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.UUID; + +import static org.postgresql.PGProperty.APPLICATION_NAME; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ApiKeyUsageLoggingService { + + private final ApiKeyService apiKeyService; + private final ApplicationEventService applicationEventService; + private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; + + public void logUsage(ContentCachingRequestWrapper request, + ContentCachingResponseWrapper response, + ApiKey apiKey, + String ipAddress, + String message) { + + if (Boolean.TRUE.equals(request.getAttribute(LOG_USAGE_FLAG))) { + return; + } + + try { + String payload = extractPayload(request); + String errorText = extractErrorText(response); + int status = safeStatus(response); + + ApiKeyUsageLog usage = buildUsageLog(apiKey, ipAddress, request, status, errorText, payload); + + apiKeyService.logUsage(usage); + + ApplicationEventType eventType = (status >= 400) + ? ApplicationEventType.API_KEY_ACCESS_FAILURE + : ApplicationEventType.API_KEY_ACCESS_SUCCESS; + + String eventMessage = (status >= 400) + ? "API key access failure" + : "API key access"; + + applicationEventService.createEvent(eventMessage, eventType, usage.toAuditMap()); + + } catch (Exception e) { + log.error("Error while logging API key usage: {}", e.getMessage(), e); + } finally { + request.setAttribute(LOG_USAGE_FLAG, Boolean.TRUE); + } + } + + private int safeStatus(HttpServletResponse response) { + try { + return response.getStatus(); + } catch (Exception e) { + return 0; + } + } + + private String extractPayload(ContentCachingRequestWrapper request) { + try { + if (!"GET".equalsIgnoreCase(request.getMethod()) && !"DELETE".equalsIgnoreCase(request.getMethod())) { + byte[] content = request.getContentAsByteArray(); + return content.length > 0 ? new String(content, StandardCharsets.UTF_8) : null; + } + } catch (Exception ex) { + log.error("Error extracting payload: {}", ex.getMessage()); + } + return null; + } + + private String extractErrorText(ContentCachingResponseWrapper response) { + int statusCode = response.getStatus(); + if (statusCode >= 400) { + byte[] content = response.getContentAsByteArray(); + String responseError = content.length > 0 ? new String(content, StandardCharsets.UTF_8) : null; + String errorHeader = response.getHeader("X-" + APPLICATION_NAME + "-error"); + return StringUtils.hasText(responseError) ? responseError : errorHeader; + } + return null; + } + + private ApiKeyUsageLog buildUsageLog(ApiKey apiKey, + String ipAddress, + HttpServletRequest request, + int status, + String errorText, + String payload) { + + String id = UUID.randomUUID().toString(); + String apiKeyId = apiKey != null && apiKey.getId() != null ? apiKey.getId().toString() : null; + String apiKeyName = apiKey != null ? apiKey.getName() : null; + String userId = apiKey != null && apiKey.getUserId() != null ? apiKey.getUserId().toString() : null; + String timestamp = Instant.now().toString(); + String endpoint = request != null ? request.getRequestURI() : null; + String queryParams = request != null ? request.getQueryString() : null; + String userAgent = request != null ? request.getHeader("User-Agent") : null; + String httpMethod = request != null ? request.getMethod() : null; + String statusCode = String.valueOf(status); + + String safePayload = null; + if (payload != null) { + int PAYLOAD_MAX_LENGTH = 2000; + safePayload = payload.length() > PAYLOAD_MAX_LENGTH ? payload.substring(0, PAYLOAD_MAX_LENGTH) : payload; + } + + return ApiKeyUsageLog.builder() + .id(id) + .apiKeyId(apiKeyId) + .apiKeyName(apiKeyName) + .userId(userId) + .timestamp(timestamp) + .endpoint(endpoint) + .address(ipAddress) + .errorMessage(errorText) + .queryParams(queryParams) + .payload(safePayload) + .userAgent(userAgent) + .httpMethod(httpMethod) + .statusCode(statusCode) + .build(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/loggin/filter/MdcCleanupFilter.java b/backend/src/main/java/com/park/utmstack/loggin/filter/MdcCleanupFilter.java new file mode 100644 index 000000000..1e3f25735 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/loggin/filter/MdcCleanupFilter.java @@ -0,0 +1,27 @@ +package com.park.utmstack.loggin.filter; + +import org.jetbrains.annotations.NotNull; +import org.slf4j.MDC; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class MdcCleanupFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(@NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } finally { + MDC.clear(); + } + } +} + diff --git a/backend/src/main/java/com/park/utmstack/loggin/filter/TraceIdFilter.java b/backend/src/main/java/com/park/utmstack/loggin/filter/TraceIdFilter.java new file mode 100644 index 000000000..566adbfd3 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/loggin/filter/TraceIdFilter.java @@ -0,0 +1,37 @@ +package com.park.utmstack.loggin.filter; + +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.UUID; + +import static com.park.utmstack.config.Constants.CONTEXT_KEY; +import static com.park.utmstack.config.Constants.TRACE_ID_KEY; + +@Component +public class TraceIdFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + String traceId = UUID.randomUUID().toString(); + MDC.put(TRACE_ID_KEY, traceId); + + String context = request.getMethod() + " " + request.getRequestURI(); + MDC.put(CONTEXT_KEY, context); + try { + filterChain.doFilter(request, response); + } finally { + MDC.remove(TRACE_ID_KEY); + MDC.remove(CONTEXT_KEY); + } + } +} diff --git a/backend/src/main/java/com/park/utmstack/repository/UtmDataInputStatusRepository.java b/backend/src/main/java/com/park/utmstack/repository/UtmDataInputStatusRepository.java index 37e86a8da..439c79d86 100644 --- a/backend/src/main/java/com/park/utmstack/repository/UtmDataInputStatusRepository.java +++ b/backend/src/main/java/com/park/utmstack/repository/UtmDataInputStatusRepository.java @@ -52,4 +52,7 @@ public interface UtmDataInputStatusRepository extends JpaRepository findByDataType(String dataType); Optional findBySourceAndDataType(String source, String dataType); + + @Query("SELECT s FROM UtmDataInputStatus s WHERE s.source = :ip OR s.source = :hostname") + List findByIpOrHostname(@Param("ip") String ip, @Param("hostname") String hostname); } diff --git a/backend/src/main/java/com/park/utmstack/repository/alert_response_rule/UtmAlertResponseRuleExecutionRepository.java b/backend/src/main/java/com/park/utmstack/repository/alert_response_rule/UtmAlertResponseRuleExecutionRepository.java index ec6b68eec..485a707ea 100644 --- a/backend/src/main/java/com/park/utmstack/repository/alert_response_rule/UtmAlertResponseRuleExecutionRepository.java +++ b/backend/src/main/java/com/park/utmstack/repository/alert_response_rule/UtmAlertResponseRuleExecutionRepository.java @@ -12,6 +12,6 @@ @SuppressWarnings("unused") @Repository public interface UtmAlertResponseRuleExecutionRepository extends JpaRepository, JpaSpecificationExecutor { - List findAllByExecutionStatus(RuleExecutionStatus status); + List findAllRuleByExecutionStatusAndRule_RuleActiveTrue(RuleExecutionStatus status); } diff --git a/backend/src/main/java/com/park/utmstack/repository/alert_response_rule/UtmAlertResponseRuleRepository.java b/backend/src/main/java/com/park/utmstack/repository/alert_response_rule/UtmAlertResponseRuleRepository.java index 463db3fe7..792228645 100644 --- a/backend/src/main/java/com/park/utmstack/repository/alert_response_rule/UtmAlertResponseRuleRepository.java +++ b/backend/src/main/java/com/park/utmstack/repository/alert_response_rule/UtmAlertResponseRuleRepository.java @@ -1,12 +1,14 @@ package com.park.utmstack.repository.alert_response_rule; import com.park.utmstack.domain.alert_response_rule.UtmAlertResponseRule; +import com.park.utmstack.domain.correlation.rules.UtmCorrelationRules; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; /** @@ -24,4 +26,6 @@ public interface UtmAlertResponseRuleRepository extends JpaRepository findAllByRuleActiveIsTrue(); + Optional findFirstBySystemOwnerIsTrueOrderByIdDesc(); + } diff --git a/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java new file mode 100644 index 000000000..ece32eba9 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/repository/api_key/ApiKeyRepository.java @@ -0,0 +1,29 @@ +package com.park.utmstack.repository.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import javax.validation.constraints.NotNull; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface ApiKeyRepository extends JpaRepository { + + Optional findByIdAndUserId(Long id, Long userId); + + Page findByUserId(Long userId, Pageable pageable); + + @Cacheable(cacheNames = "apikey", key = "#root.args[0]") + Optional findOneByApiKey(@NotNull String apiKey); + + Optional findByNameAndUserId(@NotNull String name, Long userId); + + List findAllByExpiresAtAfterAndExpiresAtLessThanEqual(Instant now, Instant fiveDaysFromNow); +} diff --git a/backend/src/main/java/com/park/utmstack/repository/idp_provider/IdentityProviderConfigRepository.java b/backend/src/main/java/com/park/utmstack/repository/idp_provider/IdentityProviderConfigRepository.java new file mode 100644 index 000000000..e4ef2e09e --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/repository/idp_provider/IdentityProviderConfigRepository.java @@ -0,0 +1,18 @@ +package com.park.utmstack.repository.idp_provider; + +import com.park.utmstack.domain.idp_provider.IdentityProviderConfig; +import com.park.utmstack.domain.idp_provider.enums.ProviderType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface IdentityProviderConfigRepository extends JpaRepository, JpaSpecificationExecutor { + + Optional findByProviderTypeAndActiveTrue(ProviderType providerType); + + List findAllByActiveTrue(); +} diff --git a/backend/src/main/java/com/park/utmstack/repository/network_scan/UtmNetworkScanRepository.java b/backend/src/main/java/com/park/utmstack/repository/network_scan/UtmNetworkScanRepository.java index fd81181eb..8a7168dc0 100644 --- a/backend/src/main/java/com/park/utmstack/repository/network_scan/UtmNetworkScanRepository.java +++ b/backend/src/main/java/com/park/utmstack/repository/network_scan/UtmNetworkScanRepository.java @@ -54,10 +54,14 @@ public interface UtmNetworkScanRepository extends JpaRepository assetIds, @Query(nativeQuery = true, value = "select n.asset_name from utm_network_scan n where n.asset_name is not null and n.is_agent is true and n.asset_alive is true and n.asset_status <> 'MISSING' and n.asset_os_platform = :platform") List findAgentNamesByPlatform(@Param("platform") String platform); + + @Query("SELECT ns.assetName, ns.groupId, ag.groupName " + + "FROM UtmNetworkScan ns " + + "JOIN UtmAssetGroup ag ON ns.groupId = ag.id " + + "WHERE ns.groupId IS NOT NULL") + List findAllAssetGroupMappings(); } diff --git a/backend/src/main/java/com/park/utmstack/security/SecurityUtils.java b/backend/src/main/java/com/park/utmstack/security/SecurityUtils.java index 0e22b7bf0..ba2cac6b1 100644 --- a/backend/src/main/java/com/park/utmstack/security/SecurityUtils.java +++ b/backend/src/main/java/com/park/utmstack/security/SecurityUtils.java @@ -23,8 +23,7 @@ public static Optional getCurrentUserLogin() { SecurityContext securityContext = SecurityContextHolder.getContext(); return Optional.ofNullable(securityContext.getAuthentication()) .map(authentication -> { - if (authentication.getPrincipal() instanceof UserDetails) { - UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal(); + if (authentication.getPrincipal() instanceof UserDetails springSecurityUser) { return springSecurityUser.getUsername(); } else if (authentication.getPrincipal() instanceof String) { return (String) authentication.getPrincipal(); diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyConfigurer.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyConfigurer.java new file mode 100644 index 000000000..871260236 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyConfigurer.java @@ -0,0 +1,20 @@ +package com.park.utmstack.security.api_key; + +import com.park.utmstack.security.jwt.JWTFilter; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; + +public class ApiKeyConfigurer extends SecurityConfigurerAdapter { + + private final ApiKeyFilter apiKeyFilter; + + public ApiKeyConfigurer(ApiKeyFilter apiKeyFilter) { + this.apiKeyFilter = apiKeyFilter; + } + + @Override + public void configure(HttpSecurity http) throws Exception { + http.addFilterAfter(apiKeyFilter, JWTFilter.class); + } +} diff --git a/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java new file mode 100644 index 000000000..5e5fd9100 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/security/api_key/ApiKeyFilter.java @@ -0,0 +1,185 @@ +package com.park.utmstack.security.api_key; + + +import com.park.utmstack.config.Constants; +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.loggin.api_key.ApiKeyUsageLoggingService; +import com.park.utmstack.repository.UserRepository; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.application_events.ApplicationEventService; +import com.park.utmstack.util.exceptions.ApiKeyInvalidAccessException; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.net.util.SubnetUtils; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Pattern; + +import static com.park.utmstack.config.Constants.API_ENDPOINT_IGNORE; + +@Slf4j +@Component +@AllArgsConstructor +public class ApiKeyFilter extends OncePerRequestFilter { + + private static final String LOG_USAGE_FLAG = "LOG_USAGE_DONE"; + private static final Pattern CIDR_PATTERN = Pattern.compile( + "^((25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)/(\\d|[1-2]\\d|3[0-2])$" + ); + + + private final UserRepository userRepository; + private final ApiKeyService apiKeyService; + private final ConcurrentMap invalidApiKeyBlackList = new ConcurrentHashMap<>(); + private final ConcurrentMap cidrCache = new ConcurrentHashMap<>(); + private final ApiKeyUsageLoggingService apiKeyUsageLoggingService; + + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + + if (API_ENDPOINT_IGNORE.contains(request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + + if (request.getAttribute(LOG_USAGE_FLAG) != null) { + filterChain.doFilter(request, response); + return; + } + + String apiKey = request.getHeader(Constants.API_KEY_HEADER); + + if (!StringUtils.hasText(apiKey)) { + filterChain.doFilter(request, response); + return; + } + + String ipAddress = request.getRemoteAddr(); + var key = getApiKey(apiKey); + + var wrappedRequest = new ContentCachingRequestWrapper(request); + var wrappedResponse = new ContentCachingResponseWrapper(response); + + UsernamePasswordAuthenticationToken authentication; + + try { + authentication = getAuthentication(key, ipAddress); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(wrappedRequest)); + SecurityContextHolder.getContext().setAuthentication(authentication); + + } catch (ApiKeyInvalidAccessException e) { + apiKeyUsageLoggingService.logUsage(wrappedRequest, wrappedResponse, key, ipAddress, e.getMessage()); + throw e; + } + + filterChain.doFilter(wrappedRequest, wrappedResponse); + wrappedResponse.copyBodyToResponse(); + + apiKeyUsageLoggingService.logUsage(wrappedRequest, wrappedResponse, key, ipAddress, null); + } + + private ApiKey getApiKey(String apiKey) { + if (invalidApiKeyBlackList.containsKey(apiKey)) { + log.warn("Access attempt with invalid API key (cached)"); + throw new ApiKeyInvalidAccessException("Invalid API key"); + } + + return apiKeyService.findOneByApiKey(apiKey) + .orElseGet(() -> { + invalidApiKeyBlackList.put(apiKey, Boolean.TRUE); + log.warn("Access attempt with invalid API key (not found in DB)"); + throw new ApiKeyInvalidAccessException("Invalid API key"); + }); + } + + public UsernamePasswordAuthenticationToken getAuthentication(ApiKey apiKey, String remoteIpAddress) { + Objects.requireNonNull(apiKey, "API key must not be null"); + Objects.requireNonNull(remoteIpAddress, "Remote IP address must not be null"); + + if (!allowAccessToRemoteIp(apiKey.getAllowedIp(), remoteIpAddress)) { + log.warn("Access denied: IP [{}] not allowed for API key [{}]", remoteIpAddress, apiKey.getApiKey()); + throw new ApiKeyInvalidAccessException( + "Invalid IP address: " + remoteIpAddress + ". If you recognize this IP, add it to allowed IP list." + ); + } + + if (apiKey.getExpiresAt() != null && !apiKey.getExpiresAt().isAfter(Instant.now())) { + log.warn("Access denied: API key [{}] expired at {}", apiKey.getApiKey(), apiKey.getExpiresAt()); + throw new ApiKeyInvalidAccessException("API key expired at " + apiKey.getExpiresAt()); + } + + var userEntityOpt = userRepository.findById(apiKey.getUserId()); + if (userEntityOpt.isEmpty()) { + log.warn("Access denied: User [{}] not found for API key [{}]", apiKey.getUserId(), apiKey.getApiKey()); + throw new ApiKeyInvalidAccessException("User not found for API key"); + } + + var userEntity = userEntityOpt.get(); + + if (!userEntity.getActivated()) { + log.warn("Access denied: User [{}] not activated", userEntity.getLogin()); + throw new ApiKeyInvalidAccessException("User not activated"); + } + + List authorities = userEntity.getAuthorities().stream() + .map(auth -> new SimpleGrantedAuthority(auth.getName())) + .toList(); + + User principal = new User(userEntity.getLogin(), "", authorities); + + return new UsernamePasswordAuthenticationToken(principal, apiKey.getApiKey(), authorities); + } + + public boolean allowAccessToRemoteIp(String allowedIpList, String remoteIp) { + if (allowedIpList == null || allowedIpList.trim().isEmpty()) { + return true; + } + String[] whitelistIps = allowedIpList.split(","); + for (String ip : whitelistIps) { + String allowed = ip.trim(); + if (allowed.isEmpty()) { + continue; + } + if (CIDR_PATTERN.matcher(allowed).matches()) { + try { + SubnetUtils subnetUtils = cidrCache.computeIfAbsent(allowed, key -> { + SubnetUtils su = new SubnetUtils(key); + su.setInclusiveHostCount(true); + return su; + }); + if (subnetUtils.getInfo().isInRange(remoteIp)) { + return true; + } + } catch (IllegalArgumentException e) { + log.error("Invalid CIDR notation: {}", allowed); + } + } else if (allowed.equals(remoteIp)) { + return true; + } + } + return false; + } +} diff --git a/backend/src/main/java/com/park/utmstack/security/jwt/JWTConfigurer.java b/backend/src/main/java/com/park/utmstack/security/jwt/JWTConfigurer.java index e39a211ec..de7ef3b60 100644 --- a/backend/src/main/java/com/park/utmstack/security/jwt/JWTConfigurer.java +++ b/backend/src/main/java/com/park/utmstack/security/jwt/JWTConfigurer.java @@ -7,7 +7,7 @@ public class JWTConfigurer extends SecurityConfigurerAdapter { - private TokenProvider tokenProvider; + private final TokenProvider tokenProvider; public JWTConfigurer(TokenProvider tokenProvider) { this.tokenProvider = tokenProvider; diff --git a/backend/src/main/java/com/park/utmstack/security/jwt/TokenProvider.java b/backend/src/main/java/com/park/utmstack/security/jwt/TokenProvider.java index 246a5dad0..ce2ec4228 100644 --- a/backend/src/main/java/com/park/utmstack/security/jwt/TokenProvider.java +++ b/backend/src/main/java/com/park/utmstack/security/jwt/TokenProvider.java @@ -1,6 +1,7 @@ package com.park.utmstack.security.jwt; +import com.park.utmstack.config.Constants; import com.park.utmstack.security.AuthoritiesConstants; import com.park.utmstack.util.CipherUtil; import io.jsonwebtoken.*; @@ -16,10 +17,12 @@ import org.springframework.stereotype.Component; import tech.jhipster.config.JHipsterProperties; +import javax.servlet.http.HttpServletRequest; import java.security.Key; import java.util.Arrays; import java.util.Collection; import java.util.Date; +import java.util.Optional; import java.util.stream.Collectors; @Component @@ -116,4 +119,15 @@ public boolean validateToken(String authToken) { } return false; } + + public boolean shouldBypassTfa(HttpServletRequest request) { + boolean bypassSwagger = Boolean.parseBoolean(request.getHeader(Constants.TFA_EXEMPTION_HEADER)); + + boolean forceTfaAuth = Boolean.parseBoolean( + Optional.ofNullable(System.getenv(Constants.ENV_TFA_ENABLE)).orElse("true") + ); + + return bypassSwagger || !forceTfaAuth; + } + } diff --git a/backend/src/main/java/com/park/utmstack/security/oauth/CustomOAuth2UserService.java b/backend/src/main/java/com/park/utmstack/security/oauth/CustomOAuth2UserService.java new file mode 100644 index 000000000..4ba6e0a87 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/security/oauth/CustomOAuth2UserService.java @@ -0,0 +1,35 @@ +package com.park.utmstack.security.oauth; + +import com.park.utmstack.domain.idp_provider.CustomOidcUser; +import com.park.utmstack.repository.UserRepository; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; + +public class CustomOAuth2UserService implements OAuth2UserService { + + private final UserRepository userRepository; + + public CustomOAuth2UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + OidcUserService delegate = new OidcUserService(); + OidcUser oidcUser = delegate.loadUser(userRequest); + + String email = oidcUser.getAttribute("email"); + if (email == null) { + throw new OAuth2AuthenticationException("Email not found in OIDC token"); + } + + return userRepository.findOneWithAuthoritiesByEmail(email) + .map(user -> new CustomOidcUser(oidcUser, user)) + .orElseThrow(() -> new OAuth2AuthenticationException("User with email " + email + " is not registered")); + } + +} + diff --git a/backend/src/main/java/com/park/utmstack/security/oauth/OAuth2LoginFailureHandler.java b/backend/src/main/java/com/park/utmstack/security/oauth/OAuth2LoginFailureHandler.java new file mode 100644 index 000000000..0ccf84e6d --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/security/oauth/OAuth2LoginFailureHandler.java @@ -0,0 +1,22 @@ +package com.park.utmstack.security.oauth; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; + +import static com.park.utmstack.config.Constants.FRONT_BASE_URL; + +public class OAuth2LoginFailureHandler implements AuthenticationFailureHandler { + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { + URI redirectUri = UriComponentsBuilder.fromHttpUrl(FRONT_BASE_URL) + .queryParam("error", "oauth2") + .build().toUri(); + response.sendRedirect(redirectUri.toString()); + } +} diff --git a/backend/src/main/java/com/park/utmstack/security/oauth/OAuth2LoginSuccessHandler.java b/backend/src/main/java/com/park/utmstack/security/oauth/OAuth2LoginSuccessHandler.java new file mode 100644 index 000000000..9d735f7c0 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/security/oauth/OAuth2LoginSuccessHandler.java @@ -0,0 +1,45 @@ +package com.park.utmstack.security.oauth; + +import com.park.utmstack.domain.idp_provider.CustomOidcUser; +import com.park.utmstack.security.jwt.TokenProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; + +import static com.park.utmstack.config.Constants.FRONT_BASE_URL; + + +public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler { + + private final TokenProvider tokenProvider; + + public OAuth2LoginSuccessHandler(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { + CustomOidcUser oAuth2User = (CustomOidcUser) authentication.getPrincipal(); + + UsernamePasswordAuthenticationToken auth = + new UsernamePasswordAuthenticationToken(oAuth2User.getName(), null, oAuth2User.getAuthorities()); + + SecurityContextHolder.getContext().setAuthentication(auth); + + String token = tokenProvider.createToken(auth, false, true); + + URI redirectUri = UriComponentsBuilder.fromHttpUrl(FRONT_BASE_URL) + .queryParam("token", token) + .build().toUri(); + + response.sendRedirect(redirectUri.toString()); + } +} + diff --git a/backend/src/main/java/com/park/utmstack/service/MailService.java b/backend/src/main/java/com/park/utmstack/service/MailService.java index 1ad02efff..ba07eaf0f 100644 --- a/backend/src/main/java/com/park/utmstack/service/MailService.java +++ b/backend/src/main/java/com/park/utmstack/service/MailService.java @@ -5,7 +5,7 @@ import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.incident.UtmIncident; import com.park.utmstack.domain.mail_sender.MailConfig; -import com.park.utmstack.domain.shared_types.AlertType; +import com.park.utmstack.domain.shared_types.alert.UtmAlert; import com.park.utmstack.domain.shared_types.LogType; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.mail_sender.BaseMailSender; @@ -250,7 +250,7 @@ public void sendPasswordResetMail(User user) { } @Async - public void sendAlertEmail(List emailsTo, AlertType alert, List relatedLogs) { + public void sendAlertEmail(List emailsTo, UtmAlert alert, List relatedLogs) { final String ctx = CLASS_NAME + ".sendAlertEmail"; try { JavaMailSender javaMailSender = getJavaMailSender(); @@ -289,7 +289,7 @@ public void sendAlertEmail(List emailsTo, AlertType alert, List } @Async - public void sendIncidentEmail(List emailsTo, List alerts, UtmIncident incident) { + public void sendIncidentEmail(List emailsTo, List alerts, UtmIncident incident) { final String ctx = CLASS_NAME + ".sendIncidentEmail"; try { JavaMailSender javaMailSender = getJavaMailSender(); @@ -332,7 +332,7 @@ public void sendIncidentEmail(List emailsTo, List alerts, Utm * @return ByteArrayResource object with attachment to alert mail * @throws Exception In case of any error */ - private ByteArrayResource buildAlertEmailAttachment(Context context, AlertType alert, + private ByteArrayResource buildAlertEmailAttachment(Context context, UtmAlert alert, List relatedLogs) throws Exception { final String ctx = CLASS_NAME + ".buildAlertEmailAttachment"; try { diff --git a/backend/src/main/java/com/park/utmstack/service/UserService.java b/backend/src/main/java/com/park/utmstack/service/UserService.java index 5a40c4286..5dacde192 100644 --- a/backend/src/main/java/com/park/utmstack/service/UserService.java +++ b/backend/src/main/java/com/park/utmstack/service/UserService.java @@ -213,19 +213,16 @@ public Optional updateUser(UserDTO userDTO) { }).map(UserDTO::new); } - public void updateUserTfaSecret(String userLogin, String tfaSecret, String tfaMethod) throws Exception { - final String ctx = CLASS_NAME + ".updateUserTfaSecret"; - try { + public void updateUserTfaSecret(String userLogin, String tfaSecret, String tfaMethod) { User user = userRepository.findOneByLogin(userLogin) - .orElseThrow(() -> new Exception(String.format("User %1$s not found", userLogin))); + .orElseThrow(() -> new NoSuchElementException(String.format("User %1$s not found", userLogin))); user.setTfaMethod(tfaMethod); user.setTfaSecret(tfaSecret); - } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); - } + + userRepository.save(user); } - public void deleteUser(String login) throws Exception { + public void deleteUser(String login) { String ctx = CLASS_NAME + ".deleteUser"; User user = userRepository.findOneByLogin(login) .orElseThrow(() -> new NoSuchElementException(String.format("User %1$s not found", login))); @@ -304,6 +301,7 @@ public List getAuthorities() { public User getCurrentUserLogin() { String userLogin = SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new CurrentUserLoginNotFoundException("No current user login was found")); - return userRepository.findOneWithAuthoritiesByLogin(userLogin).orElseThrow(() -> new CurrentUserLoginNotFoundException(String.format("No user with login %1$s was found", userLogin))); + return userRepository.findOneWithAuthoritiesByLogin(userLogin) + .orElseThrow(() -> new CurrentUserLoginNotFoundException(String.format("No user with login %1$s was found", userLogin))); } } diff --git a/backend/src/main/java/com/park/utmstack/service/UtmAlertService.java b/backend/src/main/java/com/park/utmstack/service/UtmAlertService.java index 3f322b126..51caa3164 100644 --- a/backend/src/main/java/com/park/utmstack/service/UtmAlertService.java +++ b/backend/src/main/java/com/park/utmstack/service/UtmAlertService.java @@ -1,7 +1,7 @@ package com.park.utmstack.service; import com.park.utmstack.domain.chart_builder.types.query.FilterType; -import com.park.utmstack.domain.shared_types.AlertType; +import com.park.utmstack.domain.shared_types.alert.UtmAlert; import com.park.utmstack.domain.shared_types.static_dashboard.CardType; import com.park.utmstack.util.exceptions.DashboardOverviewException; import com.park.utmstack.util.exceptions.ElasticsearchIndexDocumentUpdateException; @@ -26,5 +26,8 @@ void updateStatus(List alertIds, int status, String statusObservation) t void convertToIncident(List eventIds, String incidentName, Integer incidentId, String incidentSource) throws ElasticsearchIndexDocumentUpdateException; - List getAlertsByIds(List ids) throws UtmElasticsearchException; + List getAlertsByIds(List ids) throws UtmElasticsearchException; + + void updateStatusAndTag(List alertIds, int status, String statusObservation) throws UtmElasticsearchException, + IOException, ElasticsearchIndexDocumentUpdateException; } diff --git a/backend/src/main/java/com/park/utmstack/service/UtmAlertTagRuleService.java b/backend/src/main/java/com/park/utmstack/service/UtmAlertTagRuleService.java index ed596130d..ec228bc30 100644 --- a/backend/src/main/java/com/park/utmstack/service/UtmAlertTagRuleService.java +++ b/backend/src/main/java/com/park/utmstack/service/UtmAlertTagRuleService.java @@ -12,10 +12,12 @@ import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.elasticsearch.ElasticsearchService; import com.park.utmstack.service.elasticsearch.SearchUtil; +import com.park.utmstack.service.network_scan.AlertAssetGroupService; import com.park.utmstack.util.AlertUtil; import com.park.utmstack.util.enums.AlertStatus; import com.park.utmstack.util.events.RulesEvaluationEndEvent; import com.park.utmstack.web.rest.vm.AlertTagRuleFilterVM; +import lombok.RequiredArgsConstructor; import org.hibernate.jpa.TypedParameterValue; import org.hibernate.type.BooleanType; import org.hibernate.type.LongType; @@ -38,6 +40,7 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -46,6 +49,7 @@ */ @Service @Transactional +@RequiredArgsConstructor public class UtmAlertTagRuleService { private final Logger log = LoggerFactory.getLogger(UtmAlertTagRuleService.class); @@ -58,22 +62,8 @@ public class UtmAlertTagRuleService { private final AlertPointcut alertPointcut; private final UtmAlertTagService alertTagService; private final ElasticsearchService elasticsearchService; + private final AlertAssetGroupService alertAssetGroupService; - public UtmAlertTagRuleService(UtmAlertTagRuleRepository alertTagRuleRepository, - AlertUtil alertUtil, - ApplicationEventPublisher publisher, - ApplicationEventService eventService, - AlertPointcut alertPointcut, - UtmAlertTagService alertTagService, - ElasticsearchService elasticsearchService) { - this.alertTagRuleRepository = alertTagRuleRepository; - this.alertUtil = alertUtil; - this.publisher = publisher; - this.eventService = eventService; - this.alertPointcut = alertPointcut; - this.alertTagService = alertTagService; - this.elasticsearchService = elasticsearchService; - } /** * Save a utmTagRule. @@ -145,9 +135,12 @@ public void automaticReview() { final String ctx = CLASSNAME + ".automaticReview"; try { // If no new alerts have been received, stop execution - if (alertUtil.countAlertsByStatus(AlertStatus.AUTOMATIC_REVIEW.getCode()) == 0) + if (alertUtil.countAllAlertsByStatus(AlertStatus.AUTOMATIC_REVIEW.getCode()) == 0) return; + // Assigning asset groups to alerts in automatic review + this.assignAssetGroupsToReviewAlerts(); + // Getting all registered rules List tagRules = alertTagRuleRepository.findAll(); @@ -247,4 +240,61 @@ private void applyTagRule(List rules, Instant rulesEvaluationSt } } } + + + private void assignAssetGroupsToReviewAlerts() { + final String ctx = CLASSNAME + ".assignAssetGroupsToReviewAlerts"; + try { + + Map> assetGroups = + alertAssetGroupService.getAssetGroupsMapForAlerts(); + + if (assetGroups.isEmpty()) { + log.debug("{}: No asset-group mappings found", ctx); + return; + } + + StringBuilder scriptBuilder = new StringBuilder(); + scriptBuilder.append("if (ctx._source.containsKey('dataSource') && ctx._source.dataSource != null) {\n"); + + for (Map.Entry> entry : assetGroups.entrySet()) { + String assetName = entry.getKey(); + Long groupId = (Long) entry.getValue().get("id"); + String groupName = (String) entry.getValue().get("name"); + + scriptBuilder.append(String.format( + """ + if (ctx._source.dataSource == '%s') { + ctx._source.assetGroupId = %dL; + ctx._source.assetGroupName = '%s'; + } + """, + assetName.replace("'", "\\'"), // Escapar comillas simples + groupId, + groupName.replace("'", "\\'") + )); + } + + scriptBuilder.append("}"); + String script = scriptBuilder.toString(); + + + List filters = new ArrayList<>(); + filters.add(new FilterType(Constants.alertStatus, OperatorType.IS, + AlertStatus.AUTOMATIC_REVIEW.getCode())); + filters.add(new FilterType("dataSource", OperatorType.IS_NOT, null)); + + Query query = SearchUtil.toQuery(filters); + String indexPattern = Constants.SYS_INDEX_PATTERN.get(SystemIndexPattern.ALERTS); + + elasticsearchService.updateByQuery(query, indexPattern, script); + + log.info("{}: Asset groups assigned to {} alerts", ctx, assetGroups.size()); + + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + eventService.createEvent(msg, ApplicationEventType.ERROR); + log.error(msg, e); + } + } } diff --git a/backend/src/main/java/com/park/utmstack/service/UtmConfigurationParameterService.java b/backend/src/main/java/com/park/utmstack/service/UtmConfigurationParameterService.java index c90e5e6c1..48ceb04ff 100644 --- a/backend/src/main/java/com/park/utmstack/service/UtmConfigurationParameterService.java +++ b/backend/src/main/java/com/park/utmstack/service/UtmConfigurationParameterService.java @@ -162,13 +162,13 @@ public Map getValueMapForDateSetting() throws Exception { } } - public List getConfigParameterBySectionId(long sectionId) throws Exception { + public List getConfigParameterBySectionId(long sectionId) { final String ctx = CLASSNAME + ".getConfigParameterBySectionId"; try { return new ArrayList<>(configParamRepository .findAllBySectionId(sectionId)); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } diff --git a/backend/src/main/java/com/park/utmstack/service/UtmDataInputStatusService.java b/backend/src/main/java/com/park/utmstack/service/UtmDataInputStatusService.java index c1ffb3d94..f4da74953 100644 --- a/backend/src/main/java/com/park/utmstack/service/UtmDataInputStatusService.java +++ b/backend/src/main/java/com/park/utmstack/service/UtmDataInputStatusService.java @@ -10,7 +10,7 @@ import com.park.utmstack.domain.network_scan.UtmNetworkScan; import com.park.utmstack.domain.network_scan.enums.AssetStatus; import com.park.utmstack.domain.network_scan.enums.UpdateLevel; -import com.park.utmstack.domain.shared_types.AlertType; +import com.park.utmstack.domain.shared_types.alert.UtmAlert; import com.park.utmstack.repository.UtmDataInputStatusRepository; import com.park.utmstack.repository.correlation.config.UtmDataTypesRepository; import com.park.utmstack.repository.network_scan.UtmNetworkScanRepository; @@ -24,6 +24,7 @@ import com.park.utmstack.util.enums.AlertStatus; import com.utmstack.opensearch_connector.parsers.TermAggregateParser; import com.utmstack.opensearch_connector.types.BucketAggregation; +import lombok.RequiredArgsConstructor; import org.apache.http.conn.util.InetAddressUtils; import org.opensearch.client.json.JsonData; import org.opensearch.client.opensearch._types.SortOrder; @@ -51,6 +52,7 @@ * Service Implementation for managing UtmDataInputStatus. */ @Service +@RequiredArgsConstructor @Transactional public class UtmDataInputStatusService { @@ -66,22 +68,6 @@ public class UtmDataInputStatusService { private final UtmNetworkScanRepository networkScanRepository; - public UtmDataInputStatusService(UtmDataInputStatusRepository dataInputStatusRepository, - UtmServerModuleService serverModuleService, - ApplicationEventService applicationEventService, - UtmNetworkScanService networkScanService, - ElasticsearchService elasticsearchService, - UtmDataTypesRepository dataTypesRepository, - UtmNetworkScanRepository networkScanRepository) { - this.dataInputStatusRepository = dataInputStatusRepository; - this.serverModuleService = serverModuleService; - this.applicationEventService = applicationEventService; - this.networkScanService = networkScanService; - this.elasticsearchService = elasticsearchService; - this.dataTypesRepository = dataTypesRepository; - this.networkScanRepository = networkScanRepository; - } - /** * Save a utmDataInputStatus. * @@ -340,7 +326,7 @@ public void checkDatasourceDown() { * Create the alert object for datasource down * * @param input: Datasource information - * @return A ${@link AlertType} to index + * @return A ${@link UtmAlert} to index */ private Map createAlertForDatasourceDown(UtmDataInputStatus input) { List cloudTypes = Arrays.asList("aws", "o365", "office365", "azure", "gcp", "google", "nids", "netflow"); diff --git a/backend/src/main/java/com/park/utmstack/service/UtmMenuService.java b/backend/src/main/java/com/park/utmstack/service/UtmMenuService.java index f5dc5dde0..a0531b7c2 100644 --- a/backend/src/main/java/com/park/utmstack/service/UtmMenuService.java +++ b/backend/src/main/java/com/park/utmstack/service/UtmMenuService.java @@ -17,7 +17,7 @@ public interface UtmMenuService { */ UtmMenu save(UtmMenu menu) throws Exception; - List saveAll(List menus) throws Exception; + List saveAll(List menus); /** * Get all UtmMenu. @@ -52,7 +52,7 @@ public interface UtmMenuService { Boolean saveMenuStructure(List menus) throws Exception; - List findAllByModuleNameShort(String nameShort) throws Exception; + List findAllByModuleNameShort(String nameShort); void deleteSysMenusNotIn(@Param("ids") List ids) throws Exception; diff --git a/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseActionTemplateService.java b/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseActionTemplateService.java index ee8b6a192..2180e1e52 100644 --- a/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseActionTemplateService.java +++ b/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseActionTemplateService.java @@ -1,55 +1,12 @@ package com.park.utmstack.service.alert_response_rule; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import com.jayway.jsonpath.Criteria; -import com.jayway.jsonpath.Filter; -import com.jayway.jsonpath.Predicate; -import com.park.utmstack.config.Constants; import com.park.utmstack.domain.alert_response_rule.UtmAlertResponseActionTemplate; -import com.park.utmstack.domain.alert_response_rule.UtmAlertResponseRule; -import com.park.utmstack.domain.alert_response_rule.UtmAlertResponseRuleExecution; -import com.park.utmstack.domain.alert_response_rule.UtmAlertResponseRuleHistory; -import com.park.utmstack.domain.alert_response_rule.enums.RuleExecutionStatus; -import com.park.utmstack.domain.alert_response_rule.enums.RuleNonExecutionCause; -import com.park.utmstack.domain.application_events.enums.ApplicationEventType; -import com.park.utmstack.domain.chart_builder.types.query.FilterType; -import com.park.utmstack.domain.chart_builder.types.query.OperatorType; -import com.park.utmstack.domain.compliance.UtmComplianceStandardSection; -import com.park.utmstack.domain.shared_types.AlertType; import com.park.utmstack.repository.alert_response_rule.UtmAlertResponseActionTemplateRepository; -import com.park.utmstack.repository.alert_response_rule.UtmAlertResponseRuleExecutionRepository; -import com.park.utmstack.repository.alert_response_rule.UtmAlertResponseRuleHistoryRepository; -import com.park.utmstack.repository.alert_response_rule.UtmAlertResponseRuleRepository; -import com.park.utmstack.repository.network_scan.UtmNetworkScanRepository; import com.park.utmstack.service.UtmStackService; -import com.park.utmstack.service.agent_manager.AgentService; -import com.park.utmstack.service.application_events.ApplicationEventService; -import com.park.utmstack.service.dto.UtmAlertResponseActionTemplateDTO; -import com.park.utmstack.service.dto.UtmAlertResponseRuleDTO; -import com.park.utmstack.service.dto.agent_manager.AgentDTO; -import com.park.utmstack.service.dto.agent_manager.AgentStatusEnum; -import com.park.utmstack.service.grpc.CommandResult; -import com.park.utmstack.service.incident_response.UtmIncidentVariableService; -import com.park.utmstack.service.incident_response.grpc_impl.IncidentResponseCommandService; -import com.park.utmstack.util.UtilJson; -import com.park.utmstack.util.exceptions.UtmNotImplementedException; -import io.grpc.stub.StreamObserver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; - -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; @Service @Transactional @@ -72,7 +29,7 @@ public UtmAlertResponseActionTemplateService(UtmAlertResponseActionTemplateRepos public UtmAlertResponseActionTemplate save(UtmAlertResponseActionTemplate alertResponseActionTemplate) { final String ctx = CLASSNAME + ".save"; try { - if (!utmStackService.isInDevelop()) { + if (utmStackService.isInDevelop()) { alertResponseActionTemplate.setId(this.getSystemSequenceNextValue()); alertResponseActionTemplate.setSystemOwner(true); } else { diff --git a/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseRuleQueryService.java b/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseRuleQueryService.java index ed6b8bdb0..1ce211dbd 100644 --- a/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseRuleQueryService.java +++ b/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseRuleQueryService.java @@ -83,6 +83,9 @@ private Specification createSpecification(UtmAlertResponse if (criteria.getLastModifiedDate() != null) { specification = specification.and(buildRangeSpecification(criteria.getLastModifiedDate(), UtmAlertResponseRule_.lastModifiedDate)); } + if (criteria.getSystemOwner() != null) { + specification = specification.and(buildSpecification(criteria.getSystemOwner(), UtmAlertResponseRule_.systemOwner)); + } } return specification; } diff --git a/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseRuleService.java b/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseRuleService.java index 8a982abfe..07b3cdd36 100644 --- a/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseRuleService.java +++ b/backend/src/main/java/com/park/utmstack/service/alert_response_rule/UtmAlertResponseRuleService.java @@ -15,7 +15,7 @@ import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.chart_builder.types.query.FilterType; import com.park.utmstack.domain.chart_builder.types.query.OperatorType; -import com.park.utmstack.domain.shared_types.AlertType; +import com.park.utmstack.domain.shared_types.alert.UtmAlert; import com.park.utmstack.repository.alert_response_rule.UtmAlertResponseActionTemplateRepository; import com.park.utmstack.repository.alert_response_rule.UtmAlertResponseRuleExecutionRepository; import com.park.utmstack.repository.alert_response_rule.UtmAlertResponseRuleHistoryRepository; @@ -32,6 +32,7 @@ import com.park.utmstack.util.UtilJson; import com.park.utmstack.util.exceptions.UtmNotImplementedException; import io.grpc.stub.StreamObserver; +import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Async; @@ -49,6 +50,7 @@ import java.util.stream.Collectors; @Service +@RequiredArgsConstructor @Transactional public class UtmAlertResponseRuleService { @@ -64,46 +66,20 @@ public class UtmAlertResponseRuleService { private final UtmAlertResponseRuleExecutionRepository alertResponseRuleExecutionRepository; private final UtmIncidentVariableService utmIncidentVariableService; private final UtmAlertResponseActionTemplateRepository utmAlertResponseActionTemplateRepository; - private final UtmAlertResponseActionTemplateService utmAlertResponseActionTemplateService; - public UtmAlertResponseRuleService(UtmAlertResponseRuleRepository alertResponseRuleRepository, - UtmAlertResponseRuleHistoryRepository alertResponseRuleHistoryRepository, - UtmNetworkScanRepository networkScanRepository, - ApplicationEventService eventService, - AgentService agentService, - IncidentResponseCommandService incidentResponseCommandService, - UtmAlertResponseRuleExecutionRepository alertResponseRuleExecutionRepository, - UtmIncidentVariableService utmIncidentVariableService, - UtmAlertResponseActionTemplateRepository utmAlertResponseActionTemplateRepository, - UtmAlertResponseActionTemplateService utmAlertResponseActionTemplateService) { - this.alertResponseRuleRepository = alertResponseRuleRepository; - this.alertResponseRuleHistoryRepository = alertResponseRuleHistoryRepository; - this.networkScanRepository = networkScanRepository; - this.eventService = eventService; - this.agentService = agentService; - this.incidentResponseCommandService = incidentResponseCommandService; - this.alertResponseRuleExecutionRepository = alertResponseRuleExecutionRepository; - this.utmIncidentVariableService = utmIncidentVariableService; - this.utmAlertResponseActionTemplateRepository = utmAlertResponseActionTemplateRepository; - this.utmAlertResponseActionTemplateService = utmAlertResponseActionTemplateService; - } - public UtmAlertResponseRule save(UtmAlertResponseRule alertResponseRule) { + + public UtmAlertResponseRule save(UtmAlertResponseRule alertResponseRule, boolean isCreate) { final String ctx = CLASSNAME + ".save"; try { - if (alertResponseRule.getId() != null) { + if (!isCreate) { + String alertRuleId = String.valueOf(alertResponseRule.getId()); UtmAlertResponseRule current = alertResponseRuleRepository.findById(alertResponseRule.getId()) - .orElseThrow(() -> new RuntimeException(String.format("Incident response rule with ID: %1$s not found", alertResponseRule.getId()))); - alertResponseRuleHistoryRepository.save(new UtmAlertResponseRuleHistory(new UtmAlertResponseRuleDTO(current))); - } - - if (alertResponseRule.getUtmAlertResponseActionTemplates() != null) { - for (UtmAlertResponseActionTemplate action : alertResponseRule.getUtmAlertResponseActionTemplates()) { - if (action.getId() == null || !utmAlertResponseActionTemplateRepository.existsById(action.getId())) { - utmAlertResponseActionTemplateService.save(action); - } - } + .orElseThrow(() -> new RuntimeException(String.format("Incident response rule with ID: %1$s not found", alertRuleId))); + this.mergeInto(current, alertResponseRule); + alertResponseRuleHistoryRepository.save(new UtmAlertResponseRuleHistory(new UtmAlertResponseRuleDTO(alertResponseRule))); + alertResponseRule = current; } return alertResponseRuleRepository.save(alertResponseRule); @@ -144,7 +120,7 @@ public Map> resolveFilterValues() { } @Async - public void evaluateRules(List alerts) { + public void evaluateRules(List alerts) { final String ctx = CLASSNAME + ".evaluateRules"; try { if (CollectionUtils.isEmpty(alerts)) @@ -292,7 +268,7 @@ private String buildCommand(String rawCommand, String alertJson) { public void executeRuleCommands() { final String ctx = CLASSNAME + ".executeRuleCommands"; try { - List cmds = alertResponseRuleExecutionRepository.findAllByExecutionStatus(RuleExecutionStatus.PENDING); + List cmds = alertResponseRuleExecutionRepository.findAllRuleByExecutionStatusAndRule_RuleActiveTrue(RuleExecutionStatus.PENDING); if (CollectionUtils.isEmpty(cmds)) return; @@ -344,4 +320,47 @@ public void onCompleted() { log.error(msg); } } + + public void mergeInto(UtmAlertResponseRule target, UtmAlertResponseRule source) { + target.setRuleName(source.getRuleName()); + target.setRuleDescription(source.getRuleDescription()); + target.setRuleCmd(source.getRuleCmd()); + target.setRuleActive(source.getRuleActive()); + target.setAgentPlatform(source.getAgentPlatform()); + target.setDefaultAgent(source.getDefaultAgent()); + target.setExcludedAgents(source.getExcludedAgents()); + target.setRuleConditions(source.getRuleConditions()); + + target.getUtmAlertResponseActionTemplates().clear(); + + if (!source.getUtmAlertResponseActionTemplates().isEmpty()) { + List managedTemplates = source.getUtmAlertResponseActionTemplates() + .stream() + .map(t -> { + if (t.getId() != null) { + UtmAlertResponseActionTemplate existing = utmAlertResponseActionTemplateRepository.findById(t.getId()) + .orElseThrow(() -> new RuntimeException("Template not found: " + t.getId())); + existing.setTitle(t.getTitle()); + existing.setDescription(t.getDescription()); + existing.setCommand(t.getCommand()); + return existing; + } else { + UtmAlertResponseActionTemplate newT = new UtmAlertResponseActionTemplate(); + newT.setTitle(t.getTitle()); + newT.setDescription(t.getDescription()); + newT.setCommand(t.getCommand()); + return utmAlertResponseActionTemplateService.save(newT); + } + }) + .collect(Collectors.toList()); + + target.getUtmAlertResponseActionTemplates().addAll(managedTemplates); + } + } + + public Long getSystemSequenceNextValue() { + return alertResponseRuleRepository.findFirstBySystemOwnerIsTrueOrderByIdDesc() + .map(rule -> rule.getId() + 1) + .orElse(1L); + } } diff --git a/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java new file mode 100644 index 000000000..cbc2784a5 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/api_key/ApiKeyService.java @@ -0,0 +1,195 @@ +package com.park.utmstack.service.api_key; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.domain.api_keys.ApiKeyUsageLog; +import com.park.utmstack.repository.api_key.ApiKeyRepository; +import com.park.utmstack.service.UserService; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; +import com.park.utmstack.service.elasticsearch.OpensearchClientBuilder; +import com.park.utmstack.service.mapper.ApiKeyMapper; +import com.park.utmstack.util.exceptions.ApiKeyExistException; +import com.park.utmstack.util.exceptions.ApiKeyNotFoundException; +import lombok.AllArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.Optional; +import java.util.UUID; + +import static com.park.utmstack.config.Constants.V11_API_ACCESS_LOGS; + +@Service +@AllArgsConstructor +public class ApiKeyService { + + private static final String CLASSNAME = "ApiKeyService"; + private final Logger log = LoggerFactory.getLogger(ApiKeyService.class); + private final ApiKeyRepository apiKeyRepository; + private final ApiKeyMapper apiKeyMapper; + private final OpensearchClientBuilder client; + + + public ApiKeyResponseDTO createApiKey(Long userId,ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".createApiKey"; + try { + apiKeyRepository.findByNameAndUserId(dto.getName(), userId) + .ifPresent(apiKey -> { + throw new ApiKeyExistException("Api key already exists"); + }); + + var apiKey = ApiKey.builder() + .userId(userId) + .name(dto.getName()) + .expiresAt(dto.getExpiresAt()) + .allowedIp(String.join(",", dto.getAllowedIp())) + .createdAt(Instant.now()) + .generatedAt(Instant.now()) + .apiKey(generateRandomKey()) + .build(); + + return apiKeyMapper.toDto(apiKeyRepository.save(apiKey)); + } catch (Exception e) { + throw new ApiKeyExistException(ctx + ": " + e.getMessage()); + } + } + + public String generateApiKey(Long userId, Long apiKeyId) { + final String ctx = CLASSNAME + ".generateApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + + Instant now = Instant.now(); + Instant originalCreated = apiKey.getGeneratedAt() != null ? apiKey.getGeneratedAt() : apiKey.getCreatedAt(); + Instant originalExpires = apiKey.getExpiresAt(); + + Duration duration; + if (originalCreated != null && originalExpires != null && !originalExpires.isBefore(originalCreated)) { + duration = Duration.between(originalCreated, originalExpires); + } else { + duration = Duration.ofDays(7); + } + + String plainKey = generateRandomKey(); + apiKey.setApiKey(plainKey); + apiKey.setGeneratedAt(Instant.now()); + apiKey.setExpiresAt(now.plus(duration)); + apiKeyRepository.save(apiKey); + return plainKey; + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public ApiKeyResponseDTO updateApiKey(Long userId, Long apiKeyId, ApiKeyUpsertDTO dto) { + final String ctx = CLASSNAME + ".updateApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + apiKey.setName(dto.getName()); + if (dto.getAllowedIp() != null) { + apiKey.setAllowedIp(String.join(",", dto.getAllowedIp())); + } else { + apiKey.setAllowedIp(null); + } + apiKey.setExpiresAt(dto.getExpiresAt()); + ApiKey updated = apiKeyRepository.save(apiKey); + return apiKeyMapper.toDto(updated); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public ApiKeyResponseDTO getApiKey(Long userId, Long apiKeyId) { + final String ctx = CLASSNAME + ".getApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + return apiKeyMapper.toDto(apiKey); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public Page listApiKeys(Long userId, Pageable pageable) { + final String ctx = CLASSNAME + ".listApiKeys"; + try { + return apiKeyRepository.findByUserId(userId, pageable).map(apiKeyMapper::toDto); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + + public void deleteApiKey(Long userId, Long apiKeyId) { + final String ctx = CLASSNAME + ".deleteApiKey"; + try { + ApiKey apiKey = apiKeyRepository.findByIdAndUserId(apiKeyId, userId) + .orElseThrow(() -> new ApiKeyNotFoundException("API key not found")); + apiKeyRepository.delete(apiKey); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + private String generateRandomKey() { + final String ctx = CLASSNAME + ".generateRandomKey"; + try { + SecureRandom random = new SecureRandom(); + byte[] keyBytes = new byte[32]; + random.nextBytes(keyBytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(keyBytes); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + @Async + public void logUsage(ApiKeyUsageLog apiKeyUsageLog) { + final String ctx = CLASSNAME + ".logUsage"; + try { + client.getClient().index(V11_API_ACCESS_LOGS, apiKeyUsageLog); + } catch (Exception e) { + log.error(ctx + ": {}", e.getMessage()); + } + } + + public Optional findOneByApiKey(String apiKey) { + return apiKeyRepository.findOneByApiKey(apiKey); + } + + + /*@Scheduled(cron = "0 0 9 * * ?") + public void checkExpiringApiKeys() { + Instant fiveDaysFromNow = Instant.now().plus(5, ChronoUnit.DAYS); + Instant now = Instant.now(); + List expiringKeys = apiKeyRepository.findAllByExpiresAtAfterAndExpiresAtLessThanEqual(now, fiveDaysFromNow); + + if (!expiringKeys.isEmpty()) { + Map> expiringKeysByAccount = expiringKeys.stream() + .collect(Collectors.groupingBy(ApiKey::getUserId)); + + expiringKeysByAccount.forEach((userId, apiKeys) -> { + var principal = userRepository.findByuserIdAndAccountOwnerIsTrue(userId.toString()).orElse(null); + if (principal == null) { + return; + } + mailService.sendKeyExpirationEmail(principal, apiKeys); + + userNotificationService.createAndSendNotification(principal.getUuid(), + NotificationMessageKeyEnum.apiKey_EXPIRATION, + Map.of("names", apiKeys.stream().map(ApiKey::getName).collect(Collectors.joining(",")))); + }); + } + }*/ +} diff --git a/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java b/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java index 83bb23176..450f60d37 100644 --- a/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java +++ b/backend/src/main/java/com/park/utmstack/service/application_events/ApplicationEventService.java @@ -3,24 +3,28 @@ import com.park.utmstack.domain.application_events.enums.ApplicationEventSource; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.application_events.types.ApplicationEvent; +import com.park.utmstack.loggin.LogContextBuilder; import com.park.utmstack.service.elasticsearch.OpensearchClientBuilder; +import lombok.RequiredArgsConstructor; +import net.logstash.logback.argument.StructuredArguments; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.time.Instant; +import java.util.Map; @Service +@RequiredArgsConstructor public class ApplicationEventService { private static final String CLASSNAME = "ApplicationEventService"; private final Logger log = LoggerFactory.getLogger(ApplicationEventService.class); private final OpensearchClientBuilder client; + private final LogContextBuilder logContextBuilder; - public ApplicationEventService(OpensearchClientBuilder client) { - this.client = client; - } /** * Create an application event. Can be an error, warning or info @@ -38,7 +42,12 @@ public void createEvent(String message, ApplicationEventType type) { .build(); client.getClient().index(".utmstack-logs", applicationEvent); } catch (Exception e) { - log.error(ctx + ": " + e.getMessage()); + log.error(ctx + ": {}", e.getMessage()); } } + + public void createEvent(String message, ApplicationEventType type, Map details) { + String msg = String.format("%s: %s", MDC.get("context"), message); + log.info( msg, StructuredArguments.keyValue("args", logContextBuilder.buildArgs(details))); + } } diff --git a/backend/src/main/java/com/park/utmstack/service/application_modules/UtmModuleGroupService.java b/backend/src/main/java/com/park/utmstack/service/application_modules/UtmModuleGroupService.java index 0b5dd79ae..117c64155 100644 --- a/backend/src/main/java/com/park/utmstack/service/application_modules/UtmModuleGroupService.java +++ b/backend/src/main/java/com/park/utmstack/service/application_modules/UtmModuleGroupService.java @@ -1,19 +1,27 @@ package com.park.utmstack.service.application_modules; +import com.park.utmstack.aop.logging.Loggable; +import com.park.utmstack.domain.application_events.enums.ApplicationEventType; +import com.park.utmstack.domain.application_modules.UtmModule; import com.park.utmstack.domain.application_modules.UtmModuleGroup; import com.park.utmstack.repository.UtmModuleGroupRepository; +import com.park.utmstack.service.application_events.ApplicationEventService; +import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import javax.persistence.EntityNotFoundException; import java.util.List; +import java.util.Map; import java.util.Optional; /** * Service Implementation for managing UtmConfigurationGroup. */ @Service +@RequiredArgsConstructor @Transactional public class UtmModuleGroupService { @@ -21,10 +29,9 @@ public class UtmModuleGroupService { private final Logger log = LoggerFactory.getLogger(UtmModuleGroupService.class); private final UtmModuleGroupRepository moduleGroupRepository; + private final UtmModuleService moduleService; + private final ApplicationEventService applicationEventService; - public UtmModuleGroupService(UtmModuleGroupRepository moduleGroupRepository) { - this.moduleGroupRepository = moduleGroupRepository; - } /** * Save a utmConfigurationGroup. @@ -32,6 +39,7 @@ public UtmModuleGroupService(UtmModuleGroupRepository moduleGroupRepository) { * @param utmModuleGroup the entity to save * @return the persisted entity */ + @Loggable public UtmModuleGroup save(UtmModuleGroup utmModuleGroup) { log.debug("Request to save UtmConfigurationGroup : {}", utmModuleGroup); return moduleGroupRepository.save(utmModuleGroup); @@ -66,13 +74,47 @@ public Optional findOne(Long id) { * * @param id the id of the entity */ + public void delete(Long id) { - log.debug("Request to delete UtmConfigurationGroup : {}", id); + final String ctx = CLASSNAME + ".delete"; + long start = System.currentTimeMillis(); + + UtmModuleGroup moduleGroup = this.moduleGroupRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Configuration group not found with ID: " + id)); + + String moduleName = String.valueOf(moduleGroup.getModule().getModuleName()); + Map extra = Map.of( + "ModuleId", moduleGroup.getModule().getId(), + "ModuleName", moduleName, + "GroupId", id + ); + + String attemptMsg = String.format("Initiating deletion of configuration group (ID: %d) for module '%s'", id, moduleName); + applicationEventService.createEvent(attemptMsg, ApplicationEventType.CONFIG_GROUP_DELETE_ATTEMPT, extra); + moduleGroupRepository.deleteById(id); + + long duration = System.currentTimeMillis() - start; + String successMsg = String.format("Configuration group (ID: %d) for module '%s' deleted successfully in %dms", id, moduleName, duration); + applicationEventService.createEvent(successMsg, ApplicationEventType.CONFIG_GROUP_DELETE_SUCCESS, extra); } + public void deleteAllByModuleId(Long id) { + UtmModule module = this.moduleService.findOne(id) + .orElseThrow(() -> new EntityNotFoundException("Module not found with id " + id)); + String attemptMsg = String.format("Attempt to delete configuration keys for module '%s' initiated", module.getModuleName()); + + Map extra = Map.of( + "ModuleId", id, + "ModuleName", module.getModuleName() + ); + + applicationEventService.createEvent(attemptMsg, ApplicationEventType.CONFIG_GROUP_BULK_DELETE_ATTEMPT, extra); moduleGroupRepository.deleteAllByModuleId(id); + + String successMsg = String.format("Configuration keys for module '%s' deleted successfully", module.getModuleName()); + applicationEventService.createEvent(successMsg, ApplicationEventType.CONFIG_GROUP_BULK_DELETE_SUCCESS, extra); } public List findAllByModuleId(Long moduleId) throws Exception { diff --git a/backend/src/main/java/com/park/utmstack/service/application_modules/UtmModuleService.java b/backend/src/main/java/com/park/utmstack/service/application_modules/UtmModuleService.java index e78eeb0c5..4ff889fdb 100644 --- a/backend/src/main/java/com/park/utmstack/service/application_modules/UtmModuleService.java +++ b/backend/src/main/java/com/park/utmstack/service/application_modules/UtmModuleService.java @@ -9,6 +9,7 @@ import com.park.utmstack.repository.application_modules.UtmModuleRepository; import com.park.utmstack.service.UtmMenuService; import com.park.utmstack.event_processor.EventProcessorManagerService; +import com.park.utmstack.service.dto.application_modules.ModuleActivationDTO; import com.park.utmstack.service.index_pattern.UtmIndexPatternService; import com.park.utmstack.service.logstash_filter.UtmLogstashFilterService; import lombok.RequiredArgsConstructor; @@ -22,6 +23,7 @@ import org.springframework.util.CollectionUtils; import java.util.List; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; @@ -47,18 +49,21 @@ public class UtmModuleService { /** * Activate or deactivate the module requested * - * @param nameShort Short name of the module - * @param activationStatus Activation status + * @param moduleActivationDTO The module activation information * @return The current module information - * @throws Exception In case of any error + * @throws NoSuchElementException In case the module definition is not found for the server */ - public UtmModule activateDeactivate(Long serverId, ModuleName nameShort, Boolean activationStatus) throws Exception { + public UtmModule activateDeactivate(ModuleActivationDTO moduleActivationDTO) { final String ctx = CLASSNAME + ".activateDeactivate"; - try { + + long serverId = moduleActivationDTO.getServerId(); + ModuleName nameShort = moduleActivationDTO.getModuleName(); + boolean activationStatus = moduleActivationDTO.getActivationStatus(); + UtmModule module = moduleRepository.findByServerIdAndModuleName(serverId, nameShort); if (Objects.isNull(module)) - throw new Exception(String.format("Definition of the module %1$s not found for the server ID %2$s", nameShort.name(), serverId)); + throw new NoSuchElementException(String.format("Definition of the module %1$s not found for the server ID %2$s", nameShort.name(), serverId)); module.setModuleActive(activationStatus); module = moduleRepository.save(module); @@ -75,12 +80,9 @@ public UtmModule activateDeactivate(Long serverId, ModuleName nameShort, Boolean eventProcessorManagerService.updateModule(detached); return module; - } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); - } } - private void enableDisableModuleMenus(ModuleName nameShort, Boolean activationStatus) throws Exception { + private void enableDisableModuleMenus(ModuleName nameShort, Boolean activationStatus) { final String ctx = CLASSNAME + ".enableDisableModuleMenus"; try { List menus = menuService.findAllByModuleNameShort(nameShort.name()); @@ -96,11 +98,11 @@ private void enableDisableModuleMenus(ModuleName nameShort, Boolean activationSt menus.forEach(menu -> menu.setMenuActive(activationStatus)); menuService.saveAll(menus); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } - private void enableDisableModuleIndexPatterns(ModuleName nameShort, Boolean activationStatus) throws Exception { + private void enableDisableModuleIndexPatterns(ModuleName nameShort, Boolean activationStatus) { final String ctx = CLASSNAME + ".enableDisableModuleIndexPatterns"; try { List patterns = indexPatternService.findAllByPatternModule(nameShort.name()); @@ -116,27 +118,29 @@ private void enableDisableModuleIndexPatterns(ModuleName nameShort, Boolean acti patterns.forEach(pattern -> pattern.setActive(activationStatus)); indexPatternService.saveAll(patterns); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } - private void enableDisableModuleFilter(ModuleName nameShort, Boolean activationStatus) throws Exception { + private void enableDisableModuleFilter(ModuleName nameShort, Boolean activationStatus) { final String ctx = CLASSNAME + ".enableDisableModuleFilter"; try { List filters = logstashFilterService.findAllByModuleName(nameShort.name()); - if (CollectionUtils.isEmpty(filters)) - return; + if (!CollectionUtils.isEmpty(filters)) { + Integer moduleInstancesActives = moduleRepository.countAllByModuleNameAndModuleActiveIsTrue(nameShort); - Integer moduleInstancesActives = moduleRepository.countAllByModuleNameAndModuleActiveIsTrue(nameShort); + if ((!activationStatus && moduleInstancesActives > 0) || (activationStatus && moduleInstancesActives > 1)) + return; - if ((!activationStatus && moduleInstancesActives > 0) || (activationStatus && moduleInstancesActives > 1)) + filters.forEach(filter -> filter.setActive(activationStatus)); + logstashFilterService.saveAll(filters); + } else { return; + } - filters.forEach(filter -> filter.setActive(activationStatus)); - logstashFilterService.saveAll(filters); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } @@ -146,12 +150,12 @@ private void enableDisableModuleFilter(ModuleName nameShort, Boolean activationS * @return A list of string with all module categories * @throws Exception In case of any error */ - public List getModuleCategories(Long serverId) throws Exception { + public List getModuleCategories(Long serverId) { final String ctx = CLASSNAME + ".getModuleCategories"; try { return moduleRepository.findModuleCategories(serverId); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } @@ -180,21 +184,21 @@ public Optional findOne(Long id) { return moduleRepository.findById(id); } - public UtmModule findByServerIdAndModuleName(Long serverId, ModuleName shortName) throws Exception { + public UtmModule findByServerIdAndModuleName(Long serverId, ModuleName shortName) { final String ctx = CLASSNAME + ".findByServerIdAndModuleName"; try { return moduleRepository.findByServerIdAndModuleName(serverId, shortName); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } - public boolean isModuleActive(ModuleName shortName) throws Exception { + public boolean isModuleActive(ModuleName shortName) { final String ctx = CLASSNAME + ".isModuleActive"; try { return moduleRepository.countAllByModuleNameAndModuleActiveIsTrue(shortName) > 0; } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } } diff --git a/backend/src/main/java/com/park/utmstack/service/collectors/CollectorOpsService.java b/backend/src/main/java/com/park/utmstack/service/collectors/CollectorOpsService.java index eb6451087..a18fae336 100644 --- a/backend/src/main/java/com/park/utmstack/service/collectors/CollectorOpsService.java +++ b/backend/src/main/java/com/park/utmstack/service/collectors/CollectorOpsService.java @@ -26,6 +26,7 @@ import com.park.utmstack.security.SecurityUtils; import com.park.utmstack.service.application_modules.UtmModuleGroupService; import com.park.utmstack.service.application_modules.UtmModuleService; +import com.park.utmstack.service.dto.application_modules.ModuleActivationDTO; import com.park.utmstack.service.dto.collectors.CollectorHostnames; import com.park.utmstack.service.dto.collectors.CollectorModuleEnum; import com.park.utmstack.service.dto.collectors.dto.CollectorConfigKeysDTO; @@ -481,12 +482,14 @@ private String paginateAndSort(String query, Pageable pageable) { @Transactional public void deleteCollector(Long id) throws Exception { + Optional collector = utmCollectorRepository.findById(id); + if (collector.isEmpty()) { log.error(String.format("Collector with %1$s not found", id)); throw new RuntimeException(String.format("Collector with %1$s not found", id)); } else if (collector.get().isActive()) { - this.deleteCollector(collector.get().getHostname(), CollectorModuleEnum.AS_400); + this.deleteCollector(collector.get().getHostname(), CollectorModuleEnum.valueOf(collector.get().getModule())); List modules = this.utmModuleGroupRepository.findAllByCollector(id.toString()); if(!modules.isEmpty()){ @@ -495,10 +498,15 @@ public void deleteCollector(Long id) throws Exception { if(module.getModuleActive()){ modules = this.utmModuleGroupRepository.findAllByModuleId(module.getId()) .stream().filter( m -> !m.getCollector().equals(id.toString())) - .collect(Collectors.toList()); + .toList(); + if(modules.isEmpty()){ - this.utmModuleService.activateDeactivate(module.getServerId(), module.getModuleName(), false); + this.utmModuleService.activateDeactivate(ModuleActivationDTO.builder() + .serverId(module.getServerId()) + .moduleName(module.getModuleName()) + .activationStatus(false) + .build()); } } } diff --git a/backend/src/main/java/com/park/utmstack/service/dto/UtmAlertResponseRuleCriteria.java b/backend/src/main/java/com/park/utmstack/service/dto/UtmAlertResponseRuleCriteria.java index afc1954ff..d25dced62 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/UtmAlertResponseRuleCriteria.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/UtmAlertResponseRuleCriteria.java @@ -1,5 +1,7 @@ package com.park.utmstack.service.dto; +import lombok.Getter; +import lombok.Setter; import tech.jhipster.service.filter.BooleanFilter; import tech.jhipster.service.filter.InstantFilter; import tech.jhipster.service.filter.LongFilter; @@ -7,6 +9,8 @@ import java.io.Serializable; +@Getter +@Setter public class UtmAlertResponseRuleCriteria implements Serializable { private static final long serialVersionUID = 1L; @@ -19,69 +23,5 @@ public class UtmAlertResponseRuleCriteria implements Serializable { private StringFilter lastModifiedBy; private InstantFilter createdDate; private InstantFilter lastModifiedDate; - - - public LongFilter getId() { - return id; - } - - public void setId(LongFilter id) { - this.id = id; - } - - public StringFilter getName() { - return name; - } - - public void setName(StringFilter name) { - this.name = name; - } - - public BooleanFilter getActive() { - return active; - } - - public void setActive(BooleanFilter active) { - this.active = active; - } - - public StringFilter getAgentPlatform() { - return agentPlatform; - } - - public void setAgentPlatform(StringFilter agentPlatform) { - this.agentPlatform = agentPlatform; - } - - public StringFilter getCreatedBy() { - return createdBy; - } - - public void setCreatedBy(StringFilter createdBy) { - this.createdBy = createdBy; - } - - public StringFilter getLastModifiedBy() { - return lastModifiedBy; - } - - public void setLastModifiedBy(StringFilter lastModifiedBy) { - this.lastModifiedBy = lastModifiedBy; - } - - public InstantFilter getCreatedDate() { - return createdDate; - } - - public void setCreatedDate(InstantFilter createdDate) { - this.createdDate = createdDate; - } - - public InstantFilter getLastModifiedDate() { - return lastModifiedDate; - } - - public void setLastModifiedDate(InstantFilter lastModifiedDate) { - this.lastModifiedDate = lastModifiedDate; - } + private BooleanFilter systemOwner; } diff --git a/backend/src/main/java/com/park/utmstack/service/dto/UtmAlertResponseRuleDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/UtmAlertResponseRuleDTO.java index f036c2666..7c6cfa8ad 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/UtmAlertResponseRuleDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/UtmAlertResponseRuleDTO.java @@ -3,10 +3,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import com.park.utmstack.domain.alert_response_rule.UtmAlertResponseActionTemplate; import com.park.utmstack.domain.alert_response_rule.UtmAlertResponseRule; import com.park.utmstack.domain.chart_builder.types.query.FilterType; import lombok.Data; import lombok.NoArgsConstructor; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import javax.validation.constraints.*; @@ -60,6 +62,8 @@ public class UtmAlertResponseRuleDTO { private List actions; + private Boolean systemOwner; + public UtmAlertResponseRuleDTO(UtmAlertResponseRule rule) { this.id = rule.getId(); this.name = rule.getRuleName(); @@ -79,6 +83,7 @@ public UtmAlertResponseRuleDTO(UtmAlertResponseRule rule) { this.createdDate = rule.getCreatedDate(); this.lastModifiedBy = rule.getLastModifiedBy(); this.lastModifiedDate = rule.getLastModifiedDate(); + this.systemOwner = rule.getSystemOwner(); if (rule.getUtmAlertResponseActionTemplates() != null) { this.actions = rule.getUtmAlertResponseActionTemplates() @@ -95,4 +100,5 @@ public UtmAlertResponseRuleDTO(UtmAlertResponseRule rule) { } } + } diff --git a/backend/src/main/java/com/park/utmstack/service/dto/alert/ConvertToIncidentRequestBody.java b/backend/src/main/java/com/park/utmstack/service/dto/alert/ConvertToIncidentRequestBody.java new file mode 100644 index 000000000..4f54e071c --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/alert/ConvertToIncidentRequestBody.java @@ -0,0 +1,38 @@ +package com.park.utmstack.service.dto.alert; + +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.util.List; +import java.util.Map; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class ConvertToIncidentRequestBody implements AuditableDTO { + @NotNull + private List eventIds; + @NotNull + @Pattern(regexp = "^[^\"]*$", message = "Double quotes are not allowed") + private String incidentName; + @NotNull + private Integer incidentId; + @NotNull + private String incidentSource; + + @Override + public Map toAuditMap() { + return Map.of( + "eventIds", eventIds, + "incidentName", incidentName, + "incidentId", incidentId, + "incidentSource", incidentSource + ); + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/alert/UpdateAlertStatusRequestBody.java b/backend/src/main/java/com/park/utmstack/service/dto/alert/UpdateAlertStatusRequestBody.java new file mode 100644 index 000000000..8740293c6 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/alert/UpdateAlertStatusRequestBody.java @@ -0,0 +1,32 @@ +package com.park.utmstack.service.dto.alert; + +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class UpdateAlertStatusRequestBody implements AuditableDTO { + @NotNull + private List alertIds; + private String statusObservation; + @NotNull + private int status; + boolean addFalsePositiveTag; + + + @Override + public Map toAuditMap() { + return Map.of( + "alertIds", alertIds, + "statusObservation", statusObservation, + "status", status, + "addFalsePositiveTag", addFalsePositiveTag + ); + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/alert/UpdateAlertTagsRequestBody.java b/backend/src/main/java/com/park/utmstack/service/dto/alert/UpdateAlertTagsRequestBody.java new file mode 100644 index 000000000..898aef6bf --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/alert/UpdateAlertTagsRequestBody.java @@ -0,0 +1,31 @@ +package com.park.utmstack.service.dto.alert; + +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class UpdateAlertTagsRequestBody implements AuditableDTO { + + @NotNull + private List alertIds; + + private List tags; + @NotNull + private Boolean createRule; + + @Override + public Map toAuditMap() { + return Map.of( + "alertIds", alertIds, + "tags", tags, + "createRule", createRule + ); + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java new file mode 100644 index 000000000..abfa4d02d --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyResponseDTO.java @@ -0,0 +1,36 @@ +package com.park.utmstack.service.dto.api_key; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiKeyResponseDTO { + + @Schema(description = "Unique identifier of the API key") + private Long id; + + @Schema(description = "User-friendly API key name") + private String name; + + @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24')") + private List allowedIp; + + @Schema(description = "API key creation timestamp") + private Instant createdAt; + + @Schema(description = "API key expiration timestamp (if applicable)") + private Instant expiresAt; + + @Schema(description = "Generated At") + private Instant generatedAt; +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java new file mode 100644 index 000000000..6345f2032 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/api_key/ApiKeyUpsertDTO.java @@ -0,0 +1,28 @@ +package com.park.utmstack.service.dto.api_key; + +import com.park.utmstack.validation.api_key.ValidIPOrCIDR; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.time.Instant; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiKeyUpsertDTO { + @NotNull + @Schema(description = "API Key name", requiredMode = Schema.RequiredMode.REQUIRED) + private String name; + + @Schema(description = "Allowed IP address or IP range in CIDR notation (e.g., '192.168.1.100' or '192.168.1.0/24'). If null, no IP restrictions are applied.") + private List<@ValidIPOrCIDR String> allowedIp; + + @Schema(description = "Expiration timestamp of the API key") + private Instant expiresAt; +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/application_modules/CheckRequirementsResponse.java b/backend/src/main/java/com/park/utmstack/service/dto/application_modules/CheckRequirementsResponse.java new file mode 100644 index 000000000..c97cfa797 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/application_modules/CheckRequirementsResponse.java @@ -0,0 +1,20 @@ +package com.park.utmstack.service.dto.application_modules; + +import com.park.utmstack.domain.application_modules.enums.ModuleRequirementStatus; +import com.park.utmstack.domain.application_modules.types.ModuleRequirement; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class CheckRequirementsResponse { + private ModuleRequirementStatus status; + private List checks; + +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/application_modules/ModuleActivationDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/application_modules/ModuleActivationDTO.java new file mode 100644 index 000000000..3b836a05f --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/application_modules/ModuleActivationDTO.java @@ -0,0 +1,27 @@ +package com.park.utmstack.service.dto.application_modules; + +import com.park.utmstack.domain.application_modules.enums.ModuleName; +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.*; + +import java.util.Map; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ModuleActivationDTO implements AuditableDTO { + private Long serverId; + private ModuleName moduleName; + private Boolean activationStatus; + + @Override + public Map toAuditMap() { + return Map.of( + "serverId", serverId, + "moduleName", moduleName != null ? moduleName.name() : null, + "activationStatus", activationStatus + ); + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/auditable/AuditableDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/auditable/AuditableDTO.java new file mode 100644 index 000000000..60f04c0bd --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/auditable/AuditableDTO.java @@ -0,0 +1,7 @@ +package com.park.utmstack.service.dto.auditable; + +import java.util.Map; + +public interface AuditableDTO { + Map toAuditMap(); +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/collectors/CollectorModuleEnum.java b/backend/src/main/java/com/park/utmstack/service/dto/collectors/CollectorModuleEnum.java index 8c4bc09a9..14aa6ed2b 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/collectors/CollectorModuleEnum.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/collectors/CollectorModuleEnum.java @@ -1,5 +1,6 @@ package com.park.utmstack.service.dto.collectors; public enum CollectorModuleEnum { - AS_400; + AS_400, + UTMSTACK } diff --git a/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderConfigRequestDto.java b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderConfigRequestDto.java new file mode 100644 index 000000000..3122a970c --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderConfigRequestDto.java @@ -0,0 +1,47 @@ +package com.park.utmstack.service.dto.idp_provider.dto; + +import com.park.utmstack.domain.idp_provider.enums.ProviderType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdentityProviderConfigRequestDto { + + private Long id; + + @NotBlank + private String name; + + @NotNull + private ProviderType providerType; + + @NotBlank + private String clientId; + + private String clientSecret; + + @NotBlank + private String authUri; + + @NotBlank + private String tokenUri; + + @NotBlank + private String redirectUri; + + private String scopes; + + private String allowedDomains; + + private Boolean active; + + private String jwksUri; + + private String userInfoUri; +} \ No newline at end of file diff --git a/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderConfigResponseDto.java b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderConfigResponseDto.java new file mode 100644 index 000000000..8ace931d6 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderConfigResponseDto.java @@ -0,0 +1,26 @@ +package com.park.utmstack.service.dto.idp_provider.dto; + +import com.park.utmstack.domain.idp_provider.enums.ProviderType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdentityProviderConfigResponseDto { + private Long id; + private String name; + private ProviderType providerType; + private String clientId; + private String authUri; + private String tokenUri; + private String redirectUri; + private String scopes; + private String allowedDomains; + private Boolean active; + private String jwksUri; + private String userInfoUri; +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderCreateConfigDto.java b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderCreateConfigDto.java new file mode 100644 index 000000000..950cee136 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderCreateConfigDto.java @@ -0,0 +1,21 @@ +package com.park.utmstack.service.dto.idp_provider.dto; + +import com.park.utmstack.domain.idp_provider.enums.ProviderType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdentityProviderCreateConfigDto extends IdentityProviderConfigRequestDto { + + @NotBlank + private String clientSecret; + +} \ No newline at end of file diff --git a/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderCriteria.java b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderCriteria.java new file mode 100644 index 000000000..880d07a40 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderCriteria.java @@ -0,0 +1,31 @@ +package com.park.utmstack.service.dto.idp_provider.dto; + +import com.park.utmstack.domain.idp_provider.enums.ProviderType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import tech.jhipster.service.filter.*; + +import java.io.Serializable; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class IdentityProviderCriteria implements Serializable { + private static final long serialVersionUID = 1L; + + public static class ProviderTypeFilter extends Filter { } + + private LongFilter id; + private StringFilter name; + private ProviderTypeFilter providerType; + private StringFilter redirectUri; + private StringFilter scopes; + private StringFilter authUri; + private StringFilter tokenUri; + private StringFilter userInfoUri; + private StringFilter jwksUri; + private InstantFilter createdDate; + private InstantFilter lastModifiedDate; + private BooleanFilter active; +} \ No newline at end of file diff --git a/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderMapper.java b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderMapper.java new file mode 100644 index 000000000..ed4b4cfd6 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderMapper.java @@ -0,0 +1,24 @@ +package com.park.utmstack.service.dto.idp_provider.dto; + +import com.park.utmstack.config.Constants; +import com.park.utmstack.domain.idp_provider.IdentityProviderConfig; +import com.park.utmstack.util.CipherUtil; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +import java.util.List; + +@Mapper(componentModel = "spring", imports = {CipherUtil.class, System.class, Constants.class}) +public interface IdentityProviderMapper { + + IdentityProviderConfigResponseDto toDto(IdentityProviderConfig entity); + + @Mapping(target = "clientSecret", + expression = "java(CipherUtil.encrypt(request.getClientSecret(), System.getenv(Constants.ENV_ENCRYPTION_KEY)))") + IdentityProviderConfig toEntity(IdentityProviderConfigRequestDto request); + + List toDtoList(List entities); + + void updateEntityFromRequest(IdentityProviderConfigRequestDto request, @MappingTarget IdentityProviderConfig entity); +} diff --git a/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderSpecification.java b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderSpecification.java new file mode 100644 index 000000000..7de9075ff --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/idp_provider/dto/IdentityProviderSpecification.java @@ -0,0 +1,54 @@ +package com.park.utmstack.service.dto.idp_provider.dto; + +import com.park.utmstack.domain.idp_provider.IdentityProviderConfig; +import org.springframework.data.jpa.domain.Specification; + +import javax.persistence.criteria.Predicate; + +public class IdentityProviderSpecification { + public static Specification build(IdentityProviderCriteria criteria) { + return (root, query, cb) -> { + Predicate predicate = cb.conjunction(); + + if (criteria.getId() != null && criteria.getId().getEquals() != null) { + predicate = cb.and(predicate, cb.equal(root.get("id"), criteria.getId().getEquals())); + } + if (criteria.getName() != null && criteria.getName().getContains() != null) { + predicate = cb.and(predicate, cb.like(root.get("name"), "%" + criteria.getName().getContains() + "%")); + } + if (criteria.getProviderType() != null && criteria.getProviderType().getEquals() != null) { + predicate = cb.and(predicate, cb.equal(root.get("providerType"), criteria.getProviderType().getEquals())); + } + if (criteria.getRedirectUri() != null && criteria.getRedirectUri().getContains() != null) { + predicate = cb.and(predicate, cb.like(root.get("redirectUri"), "%" + criteria.getRedirectUri().getContains() + "%")); + } + if (criteria.getScopes() != null && criteria.getScopes().getContains() != null) { + predicate = cb.and(predicate, cb.like(root.get("scopes"), "%" + criteria.getScopes().getContains() + "%")); + } + if (criteria.getAuthUri() != null && criteria.getAuthUri().getContains() != null) { + predicate = cb.and(predicate, cb.like(root.get("authUri"), "%" + criteria.getAuthUri().getContains() + "%")); + } + if (criteria.getTokenUri() != null && criteria.getTokenUri().getContains() != null) { + predicate = cb.and(predicate, cb.like(root.get("tokenUri"), "%" + criteria.getTokenUri().getContains() + "%")); + } + if (criteria.getUserInfoUri() != null && criteria.getUserInfoUri().getContains() != null) { + predicate = cb.and(predicate, cb.like(root.get("userInfoUri"), "%" + criteria.getUserInfoUri().getContains() + "%")); + } + if (criteria.getJwksUri() != null && criteria.getJwksUri().getContains() != null) { + predicate = cb.and(predicate, cb.like(root.get("jwksUri"), "%" + criteria.getJwksUri().getContains() + "%")); + } + if (criteria.getCreatedDate() != null && criteria.getCreatedDate().getEquals() != null) { + predicate = cb.and(predicate, cb.equal(root.get("createdDate"), criteria.getCreatedDate().getEquals())); + } + if (criteria.getLastModifiedDate() != null && criteria.getLastModifiedDate().getEquals() != null) { + predicate = cb.and(predicate, cb.equal(root.get("lastModifiedDate"), criteria.getLastModifiedDate().getEquals())); + } + if (criteria.getActive() != null && criteria.getActive().getEquals() != null) { + predicate = cb.and(predicate, cb.equal(root.get("active"), criteria.getActive().getEquals())); + } + + return predicate; + }; + } +} + diff --git a/backend/src/main/java/com/park/utmstack/service/dto/incident/AddToIncidentDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/incident/AddToIncidentDTO.java index 2e648f75d..154e58da1 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/incident/AddToIncidentDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/incident/AddToIncidentDTO.java @@ -1,9 +1,17 @@ package com.park.utmstack.service.dto.incident; +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.Getter; +import lombok.Setter; + import javax.validation.constraints.NotNull; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; -public class AddToIncidentDTO { +@Setter +@Getter +public class AddToIncidentDTO implements AuditableDTO { @NotNull public Long incidentId; @NotNull @@ -12,19 +20,15 @@ public class AddToIncidentDTO { public AddToIncidentDTO() { } - public Long getIncidentId() { - return incidentId; - } - - public void setIncidentId(Long incidentId) { - this.incidentId = incidentId; - } - - public List getAlertList() { - return alertList; - } + @Override + public Map toAuditMap() { + List alertIds = alertList.stream() + .map(RelatedIncidentAlertsDTO::getAlertId) + .toList(); - public void setAlertList(List alertList) { - this.alertList = alertList; + return Map.of( + "incidentId", incidentId, + "alertIds", alertIds + ); } } diff --git a/backend/src/main/java/com/park/utmstack/service/dto/incident/NewIncidentDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/incident/NewIncidentDTO.java index 7b4c4b9ef..50d11ed7e 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/incident/NewIncidentDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/incident/NewIncidentDTO.java @@ -1,10 +1,17 @@ package com.park.utmstack.service.dto.incident; +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.Getter; +import lombok.Setter; + import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import java.util.List; +import java.util.Map; -public class NewIncidentDTO { +@Setter +@Getter +public class NewIncidentDTO implements AuditableDTO { @NotNull @Pattern(regexp = "^[^\"]*$", message = "Double quotes are not allowed") public String incidentName; @@ -16,36 +23,16 @@ public class NewIncidentDTO { public NewIncidentDTO() { } - public String getIncidentName() { - return incidentName; - } - - public void setIncidentName(String incidentName) { - this.incidentName = incidentName; - } - - public String getIncidentDescription() { - return incidentDescription; - } - - public void setIncidentDescription(String incidentDescription) { - this.incidentDescription = incidentDescription; - } - - public String getIncidentAssignedTo() { - return incidentAssignedTo; - } - - public void setIncidentAssignedTo(String incidentAssignedTo) { - this.incidentAssignedTo = incidentAssignedTo; - } - - public List getAlertList() { - return alertList; - } + @Override + public Map toAuditMap() { + List alertIds = alertList.stream() + .map(RelatedIncidentAlertsDTO::getAlertId) + .toList(); - public void setAlertList(List alertList) { - this.alertList = alertList; + return Map.of( + "incidentName", incidentName, + "alertIds", alertIds + ); } @Deprecated diff --git a/backend/src/main/java/com/park/utmstack/service/dto/jwt/LoginResponseDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/jwt/LoginResponseDTO.java index d81e732e5..c1a6594c2 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/jwt/LoginResponseDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/jwt/LoginResponseDTO.java @@ -9,8 +9,10 @@ @Builder public class LoginResponseDTO { private boolean success; - private boolean tfaRequired; + private boolean tfaConfigured; + private boolean forceTfa; private String method; private String token; + private long tfaExpiresInSeconds; } diff --git a/backend/src/main/java/com/park/utmstack/service/dto/network_scan/NetworkScanDTO.java b/backend/src/main/java/com/park/utmstack/service/dto/network_scan/NetworkScanDTO.java index 17d561744..da06503f7 100644 --- a/backend/src/main/java/com/park/utmstack/service/dto/network_scan/NetworkScanDTO.java +++ b/backend/src/main/java/com/park/utmstack/service/dto/network_scan/NetworkScanDTO.java @@ -7,12 +7,18 @@ import com.park.utmstack.domain.network_scan.UtmPorts; import com.park.utmstack.domain.network_scan.enums.AssetRegisteredMode; import com.park.utmstack.domain.network_scan.enums.AssetStatus; +import com.park.utmstack.repository.UtmDataInputStatusRepository; +import lombok.Getter; +import lombok.Setter; import org.springframework.util.CollectionUtils; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; +@Getter +@Setter public class NetworkScanDTO { private Long id; private String assetIp; @@ -46,7 +52,7 @@ public class NetworkScanDTO { public NetworkScanDTO() { } - public NetworkScanDTO(UtmNetworkScan scan, boolean details) { + public NetworkScanDTO(UtmNetworkScan scan, boolean details, UtmDataInputStatusRepository utmDataInputStatusRepository) { this.id = scan.getId(); this.assetIp = scan.getAssetIp(); this.assetAddresses = scan.getAssetAddresses(); @@ -71,12 +77,7 @@ public NetworkScanDTO(UtmNetworkScan scan, boolean details) { this.registeredMode = scan.getRegisteredMode(); this.assetAlias = scan.getAssetAlias(); this.isAgent = scan.getIsAgent(); - - List dataInputSourceList = Objects.requireNonNullElse(scan.getDataInputSourceList(), Collections.emptyList()); - List dataInputIpList = Objects.requireNonNullElse(scan.getDataInputIpList(), Collections.emptyList()); - - this.dataInputList = new ArrayList<>(dataInputSourceList); - this.dataInputList.addAll(dataInputIpList); + this.dataInputList = utmDataInputStatusRepository.findByIpOrHostname(scan.getAssetIp(), scan.getAssetName()); if (!CollectionUtils.isEmpty(scan.getMetrics())) scan.getMetrics().forEach(metric -> this.metrics.put(metric.getMetric(), metric.getAmount())); @@ -87,212 +88,6 @@ public NetworkScanDTO(UtmNetworkScan scan, boolean details) { } } - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getAssetIp() { - return assetIp; - } - - public void setAssetIp(String assetIp) { - this.assetIp = assetIp; - } - - public String getAssetAddresses() { - return assetAddresses; - } - - public void setAssetAddresses(String assetAddresses) { - this.assetAddresses = assetAddresses; - } - - public String getAssetMac() { - return assetMac; - } - - public void setAssetMac(String assetMac) { - this.assetMac = assetMac; - } - - public String getAssetOs() { - return assetOs; - } - - public void setAssetOs(String assetOs) { - this.assetOs = assetOs; - } - - public String getAssetOsArch() { - return assetOsArch; - } - - public void setAssetOsArch(String assetOsArch) { - this.assetOsArch = assetOsArch; - } - - public String getAssetOsMajorVersion() { - return assetOsMajorVersion; - } - - public void setAssetOsMajorVersion(String assetOsMajorVersion) { - this.assetOsMajorVersion = assetOsMajorVersion; - } - - public String getAssetOsMinorVersion() { - return assetOsMinorVersion; - } - - public void setAssetOsMinorVersion(String assetOsMinorVersion) { - this.assetOsMinorVersion = assetOsMinorVersion; - } - - public String getAssetOsPlatform() { - return assetOsPlatform; - } - - public void setAssetOsPlatform(String assetOsPlatform) { - this.assetOsPlatform = assetOsPlatform; - } - - public String getAssetOsVersion() { - return assetOsVersion; - } - - public void setAssetOsVersion(String assetOsVersion) { - this.assetOsVersion = assetOsVersion; - } - - public String getAssetName() { - return assetName; - } - - public void setAssetName(String assetName) { - this.assetName = assetName; - } - - public String getAssetAliases() { - return assetAliases; - } - - public void setAssetAliases(String assetAliases) { - this.assetAliases = assetAliases; - } - - public String getAssetAlias() { - return assetAlias; - } - - public void setAssetAlias(String assetAlias) { - this.assetAlias = assetAlias; - } - - - public Boolean getAssetAlive() { - return assetAlive; - } - - public void setAssetAlive(Boolean assetAlive) { - this.assetAlive = assetAlive; - } - - public AssetStatus getAssetStatus() { - return assetStatus; - } - - public void setAssetStatus(AssetStatus assetStatus) { - this.assetStatus = assetStatus; - } - - public Float getAssetSeverityMetric() { - return assetSeverityMetric; - } - - public void setAssetSeverityMetric(Float assetSeverityMetric) { - this.assetSeverityMetric = assetSeverityMetric; - } - - public UtmAssetTypes getAssetType() { - return assetType; - } - - public void setAssetType(UtmAssetTypes assetType) { - this.assetType = assetType; - } - - public String getAssetNotes() { - return assetNotes; - } - - public void setAssetNotes(String assetNotes) { - this.assetNotes = assetNotes; - } - - public Instant getDiscoveredAt() { - return discoveredAt; - } - - public void setDiscoveredAt(Instant discoveredAt) { - this.discoveredAt = discoveredAt; - } - - public Instant getModifiedAt() { - return modifiedAt; - } - - public void setModifiedAt(Instant modifiedAt) { - this.modifiedAt = modifiedAt; - } - - public Set getPorts() { - return ports; - } - - public void setPorts(Set ports) { - this.ports = ports; - } - - public Map getMetrics() { - return metrics; - } - - public String getServerName() { - return serverName; - } - - public void setServerName(String serverName) { - this.serverName = serverName; - } - - public UtmAssetGroup getGroup() { - return group; - } - - public void setGroup(UtmAssetGroup group) { - this.group = group; - } - - public AssetRegisteredMode getRegisteredMode() { - return registeredMode; - } - - - public void setRegisteredMode(AssetRegisteredMode registeredMode) { - this.registeredMode = registeredMode; - } - - public Boolean getAgent() { - return isAgent; - } - - public void setAgent(Boolean agent) { - isAgent = agent; - } - public static class Port { private Integer port; private String tcp; diff --git a/backend/src/main/java/com/park/utmstack/service/dto/tfa/enroll/TfaEnrollRequest.java b/backend/src/main/java/com/park/utmstack/service/dto/tfa/enroll/TfaEnrollRequest.java new file mode 100644 index 000000000..b331422c3 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/dto/tfa/enroll/TfaEnrollRequest.java @@ -0,0 +1,19 @@ +package com.park.utmstack.service.dto.tfa.enroll; + +import com.park.utmstack.domain.tfa.TfaMethod; +import com.park.utmstack.domain.tfa.TfaStage; +import com.park.utmstack.service.dto.tfa.save.TfaSaveRequest; +import com.park.utmstack.service.dto.tfa.verify.TfaVerifyRequest; +import lombok.Data; + +@Data +public class TfaEnrollRequest { + private TfaStage stage; + private TfaMethod method; + private String code; + private boolean enable; + + public TfaVerifyRequest toVerifyRequest() { + return new TfaVerifyRequest(method, code); + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/elasticsearch/ElasticsearchService.java b/backend/src/main/java/com/park/utmstack/service/elasticsearch/ElasticsearchService.java index c07b2a06b..b7d078842 100644 --- a/backend/src/main/java/com/park/utmstack/service/elasticsearch/ElasticsearchService.java +++ b/backend/src/main/java/com/park/utmstack/service/elasticsearch/ElasticsearchService.java @@ -5,6 +5,7 @@ import com.park.utmstack.domain.UtmSpaceNotificationControl; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.chart_builder.types.query.FilterType; +import com.park.utmstack.domain.chart_builder.types.query.OperatorType; import com.park.utmstack.domain.index_pattern.enums.SystemIndexPattern; import com.park.utmstack.repository.UserRepository; import com.park.utmstack.service.MailService; @@ -18,12 +19,13 @@ import com.utmstack.opensearch_connector.exceptions.OpenSearchException; import com.utmstack.opensearch_connector.types.ElasticCluster; import com.utmstack.opensearch_connector.types.IndexSort; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.opensearch.client.json.JsonData; import org.opensearch.client.opensearch._types.SortOrder; import org.opensearch.client.opensearch._types.query_dsl.Query; +import org.opensearch.client.opensearch.cat.CountResponse; import org.opensearch.client.opensearch.cat.indices.IndicesRecord; -import org.opensearch.client.opensearch.core.IndexResponse; -import org.opensearch.client.opensearch.core.SearchRequest; -import org.opensearch.client.opensearch.core.SearchResponse; +import org.opensearch.client.opensearch.core.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.support.PagedListHolder; @@ -327,6 +329,38 @@ public SearchResponse search(List filters, Integer top, Strin } } + public boolean exists(List filters, String indexPattern) { + final String ctx = CLASSNAME + ".exists"; + try { + SearchRequest request = new SearchRequest.Builder() + .index(indexPattern) + .query(SearchUtil.toQuery(filters)) + .size(1) + .build(); + + SearchResponse response = search(request, Object.class); + return response.hits().total().value() > 0; + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } + + public long count(List filters, String indexPattern) { + final String ctx = CLASSNAME + ".count"; + try { + SearchRequest.Builder srb = new SearchRequest.Builder() + .index(indexPattern) + .query(SearchUtil.toQuery(filters)) + .size(0); + + SearchResponse response = search(srb.build(), Object.class); + return response.hits().total().value(); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage(), e); + } + } + + public SearchResponse search(SearchRequest request, Class type) { final String ctx = CLASSNAME + ".search"; try { diff --git a/backend/src/main/java/com/park/utmstack/service/elasticsearch/SearchUtil.java b/backend/src/main/java/com/park/utmstack/service/elasticsearch/SearchUtil.java index 4e2d7cc8a..5d4cadc05 100644 --- a/backend/src/main/java/com/park/utmstack/service/elasticsearch/SearchUtil.java +++ b/backend/src/main/java/com/park/utmstack/service/elasticsearch/SearchUtil.java @@ -31,8 +31,9 @@ public class SearchUtil { public static Query toQuery(List filters) { final String ctx = CLASSNAME + ".toQuery"; try { + boolean hasShouldClauses = false; BoolQuery.Builder bool = new BoolQuery.Builder(); - bool.filter(Query.of(q -> q.matchAll(MatchAllQuery.of(m -> m)))); + /*bool.filter(Query.of(q -> q.matchAll(MatchAllQuery.of(m -> m))));*/ if (!CollectionUtils.isEmpty(filters)) { for (FilterType filter : filters) { @@ -100,9 +101,18 @@ public static Query toQuery(List filters) { case IS_LESS_THAN_OR_EQUALS: buildIsLessThanOrEquals(bool, filter); break; + case IS_ONE_OF_TERMS_OR: + hasShouldClauses = true; + buildIsOneOfOr(bool, filter); + break; } } } + + if (hasShouldClauses) { + bool.minimumShouldMatch("1"); + } + return Query.of(q -> q.bool(bool.build())); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getLocalizedMessage()); @@ -373,6 +383,16 @@ private static void buildIsLessThanOrEquals(BoolQuery.Builder bool, FilterType f } } + private static void buildIsOneOfOr(BoolQuery.Builder bool, FilterType filter) { + List termsValues = ((List) filter.getValue()).stream().map(str -> FieldValue.of((String) str)).toList(); + + Query termsQuery = Query.of(q -> q.terms(t -> t + .field(filter.getField()) + .terms(terms -> terms.value(termsValues)))); + + bool.should(termsQuery); + } + /** * Apply a sort to a elasticsearch query * diff --git a/backend/src/main/java/com/park/utmstack/service/elasticsearch/processor/GroupByFieldProcessor.java b/backend/src/main/java/com/park/utmstack/service/elasticsearch/processor/GroupByFieldProcessor.java index 8282d63dd..aa2d5b615 100644 --- a/backend/src/main/java/com/park/utmstack/service/elasticsearch/processor/GroupByFieldProcessor.java +++ b/backend/src/main/java/com/park/utmstack/service/elasticsearch/processor/GroupByFieldProcessor.java @@ -12,7 +12,6 @@ public class GroupByFieldProcessor implements SearchResultProcessor { @Override public List> process(List> rawResults) { - // Fase 1: Indexaciรณn por ID Map> byId = new LinkedHashMap<>(); for (Map item : rawResults) { Object id = item.get("id"); @@ -22,13 +21,13 @@ public List> process(List> rawResults) { } Map>> childrenByParent = new LinkedHashMap<>(); - Set childIds = new HashSet<>(); // โœ… Rastrear IDs de hijos + Set childIds = new HashSet<>(); for (Map item : rawResults) { Object parentId = item.get("parentId"); if (parentId != null && byId.containsKey(parentId)) { childrenByParent.computeIfAbsent(parentId, k -> new ArrayList<>()).add(item); - childIds.add(item.get("id")); // โœ… Marcar como hijo + childIds.add(item.get("id")); } } diff --git a/backend/src/main/java/com/park/utmstack/service/idp_provider/IdentityProviderService.java b/backend/src/main/java/com/park/utmstack/service/idp_provider/IdentityProviderService.java new file mode 100644 index 000000000..37218b537 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/idp_provider/IdentityProviderService.java @@ -0,0 +1,92 @@ +package com.park.utmstack.service.idp_provider; + + +import com.park.utmstack.domain.idp_provider.IdentityProviderConfig; +import com.park.utmstack.repository.idp_provider.IdentityProviderConfigRepository; +import com.park.utmstack.service.dto.idp_provider.dto.*; +import com.park.utmstack.util.events.ProviderChangedEvent; +import com.park.utmstack.util.exceptions.IdpNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class IdentityProviderService { + + private final IdentityProviderMapper mapper; + private final IdentityProviderConfigRepository repository; + private final ApplicationEventPublisher publisher; + + public List getAllActiveProviders() { + return repository.findAllByActiveTrue(); + } + + public IdentityProviderConfigResponseDto create(IdentityProviderCreateConfigDto dto) { + + IdentityProviderConfig entity = mapper.toEntity(dto); + entity.setCreatedAt(LocalDateTime.now()); + entity.setUpdatedAt(LocalDateTime.now()); + + IdentityProviderConfig saved = repository.save(entity); + publisher.publishEvent(new ProviderChangedEvent(saved)); + return mapper.toDto(saved); + } + + + public IdentityProviderConfigResponseDto update(Long id, IdentityProviderConfigRequestDto dto) { + + IdentityProviderConfig existing = repository.findById(id) + .orElseThrow(() -> new IdpNotFoundException("IdentityProviderConfig not found: " + id)); + + existing.setName(dto.getName()); + existing.setClientId(dto.getClientId()); + + if (dto.getClientSecret() != null) { + existing.setClientSecret(mapper.toEntity(dto).getClientSecret()); + } + + existing.setRedirectUri(dto.getRedirectUri()); + existing.setScopes(dto.getScopes()); + existing.setAllowedDomains(dto.getAllowedDomains()); + existing.setActive(dto.getActive()); + existing.setUpdatedAt(LocalDateTime.now()); + + IdentityProviderConfig updated = repository.save(existing); + publisher.publishEvent(new ProviderChangedEvent(updated)); + return mapper.toDto(updated); + } + + + @Transactional(readOnly = true) + public Page findAll(IdentityProviderCriteria criteria, Pageable pageable) { + Specification spec = IdentityProviderSpecification.build(criteria); + Page result = repository.findAll(spec, pageable); + return result.map(mapper::toDto); + } + + + + @Transactional(readOnly = true) + public Optional findById(Long id) { + return repository.findById(id) + .map(mapper::toDto); + } + + + public void delete(Long id) { + if (!repository.existsById(id)) { + throw new IdpNotFoundException("IdentityProviderConfig not found: " + id); + } + repository.deleteById(id); + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/impl/UtmAlertServiceImpl.java b/backend/src/main/java/com/park/utmstack/service/impl/UtmAlertServiceImpl.java index 64596c91c..a7e6b6da4 100644 --- a/backend/src/main/java/com/park/utmstack/service/impl/UtmAlertServiceImpl.java +++ b/backend/src/main/java/com/park/utmstack/service/impl/UtmAlertServiceImpl.java @@ -8,7 +8,8 @@ import com.park.utmstack.domain.chart_builder.types.query.FilterType; import com.park.utmstack.domain.chart_builder.types.query.OperatorType; import com.park.utmstack.domain.index_pattern.enums.SystemIndexPattern; -import com.park.utmstack.domain.shared_types.AlertType; +import com.park.utmstack.domain.shared_types.ApplicationLayer; +import com.park.utmstack.domain.shared_types.alert.UtmAlert; import com.park.utmstack.domain.shared_types.LogType; import com.park.utmstack.domain.shared_types.static_dashboard.CardType; import com.park.utmstack.repository.UtmAlertLastRepository; @@ -29,6 +30,7 @@ import com.park.utmstack.util.exceptions.UtmElasticsearchException; import com.utmstack.opensearch_connector.parsers.TermAggregateParser; import com.utmstack.opensearch_connector.types.BucketAggregation; +import lombok.RequiredArgsConstructor; import org.apache.commons.text.StringEscapeUtils; import org.opensearch.client.opensearch._types.query_dsl.Query; import org.opensearch.client.opensearch.core.SearchRequest; @@ -52,6 +54,7 @@ import java.util.stream.Collectors; @Service +@RequiredArgsConstructor @Transactional public class UtmAlertServiceImpl implements UtmAlertService { @@ -67,22 +70,7 @@ public class UtmAlertServiceImpl implements UtmAlertService { private final SocAIService socAIService; private final UtmAlertResponseRuleService alertResponseRuleService; - public UtmAlertServiceImpl(MailService mailService, - ApplicationEventService eventService, - AlertPointcut alertPointcut, - UtmAlertLastRepository lastAlertRepository, - ElasticsearchService elasticsearchService, - UtmModuleService moduleService, SocAIService socAIService, - UtmAlertResponseRuleService alertResponseRuleService) { - this.mailService = mailService; - this.eventService = eventService; - this.alertPointcut = alertPointcut; - this.lastAlertRepository = lastAlertRepository; - this.elasticsearchService = elasticsearchService; - this.moduleService = moduleService; - this.socAIService = socAIService; - this.alertResponseRuleService = alertResponseRuleService; - } + @EventListener(RulesEvaluationEndEvent.class) public void checkForNewAlerts() { @@ -103,12 +91,12 @@ public void checkForNewAlerts() { SearchRequest build = srb.build(); - HitsMetadata hitsMetadata = elasticsearchService.search(build, AlertType.class).hits(); + HitsMetadata hitsMetadata = elasticsearchService.search(build, UtmAlert.class).hits(); - if (hitsMetadata.total().value() <= 0) + if (hitsMetadata.total() != null && hitsMetadata.total().value() <= 0) return; - List alerts = hitsMetadata.hits().stream().map(Hit::source).collect(Collectors.toList()); + List alerts = hitsMetadata.hits().stream().map(Hit::source).collect(Collectors.toList()); if (CollectionUtils.isEmpty(alerts)) return; @@ -116,7 +104,7 @@ public void checkForNewAlerts() { initialDate.setLastAlertTimestamp(alerts.get(alerts.size() - 1).getTimestampAsInstant()); lastAlertRepository.save(initialDate); - for (AlertType alert : alerts) { + for (UtmAlert alert : alerts) { List relatedLogs; try { relatedLogs = getRelatedAlerts(alert.getLogs()); @@ -141,7 +129,7 @@ public void checkForNewAlerts() { alertResponseRuleService.evaluateRules(alerts); if (moduleService.isModuleActive(ModuleName.SOC_AI)) - socAIService.requestSocAiProcess(alerts.stream().map(AlertType::getId).collect(Collectors.toList())); + socAIService.requestSocAiProcess(alerts.stream().map(UtmAlert::getId).collect(Collectors.toList())); } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); @@ -177,13 +165,26 @@ private List getRelatedAlerts(List logs) throws UtmElasticsearc public void updateStatus(List alertIds, int status, String statusObservation) throws ElasticsearchIndexDocumentUpdateException { final String ctx = CLASS_NAME + ".updateStatus"; + long start = System.currentTimeMillis(); try { + String alertsIds = String.join(",", alertIds); + Map extra = Map.of( + "alertIds", alertsIds, + "newStatus", status, + "layer", ApplicationLayer.SERVICE.name() + ); + + String attemptMsg = String.format("Attempt to update status to %1$s for alerts with ids: %2$s", + AlertStatus.getByCode(status).getName(), alertsIds); + eventService.createEvent(attemptMsg, ApplicationEventType.ALERT_STATUS_UPDATE_ATTEMPT, extra); + String ruleScript = "ctx._source.status=%1$s;" + "ctx._source.statusLabel='%2$s';" + "ctx._source.statusObservation=\"%3$s\";"; List filters = new ArrayList<>(); - filters.add(new FilterType(Constants.alertIdKeyword, OperatorType.IS_ONE_OF_TERMS, alertIds)); + filters.add(new FilterType(Constants.alertIdKeyword, OperatorType.IS_ONE_OF_TERMS_OR, alertIds)); + filters.add(new FilterType(Constants.alertParentIdKeyword, OperatorType.IS_ONE_OF_TERMS_OR, alertIds)); String script = String.format(ruleScript, status, AlertStatus.getByCode(status).getName(), StringEscapeUtils.escapeJava(statusObservation)); @@ -192,11 +193,52 @@ public void updateStatus(List alertIds, int status, String statusObserva elasticsearchService.updateByQuery(SearchUtil.toQuery(filters), Constants.SYS_INDEX_PATTERN.get(SystemIndexPattern.ALERTS), script); + + long duration = System.currentTimeMillis() - start; + String successMsg = String.format("Status updated to %1$s for alerts with ids: %2$s in %3$s ms", + AlertStatus.getByCode(status).getName(), alertsIds, duration); + eventService.createEvent(successMsg, ApplicationEventType.ALERT_NOTE_UPDATE_SUCCESS, extra); } catch (Exception e) { throw new ElasticsearchIndexDocumentUpdateException(ctx + ": " + e.getMessage()); } } + public void updateStatusAndTag(List alertIds, int status, String statusObservation) { + + updateStatus(alertIds, status, statusObservation); + + if (status == AlertStatus.COMPLETED.getCode()) { + String alertsIds = String.join(",", alertIds); + Map extra = Map.of( + "alertIds", alertsIds, + "newStatus", status + ); + + String attemptMsg = String.format("Attempt to mark alerts as false positive for ids: %s", alertsIds); + eventService.createEvent(attemptMsg, ApplicationEventType.ALERT_STATUS_UPDATE_ATTEMPT, extra); + + // Script para modificar el tag + String script = String.format("if (ctx._source.tags == null) { ctx._source.tags = []; } " + + "if (!ctx._source.tags.contains('%s')) { ctx._source.tags.add('%s'); }", Constants.FALSE_POSITIVE_TAG, Constants.FALSE_POSITIVE_TAG); + + List filters = new ArrayList<>(); + filters.add(new FilterType(Constants.alertIdKeyword, OperatorType.IS_ONE_OF_TERMS_OR, alertIds)); + filters.add(new FilterType(Constants.alertParentIdKeyword, OperatorType.IS_ONE_OF_TERMS_OR, alertIds)); + + elasticsearchService.updateByQuery( + SearchUtil.toQuery(filters), + Constants.SYS_INDEX_PATTERN.get(SystemIndexPattern.ALERTS), + script + ); + + String successMsg = String.format( + "Alerts with ids %s marked as false positive", alertsIds + ); + eventService.createEvent(successMsg, ApplicationEventType.ALERT_NOTE_UPDATE_SUCCESS, extra); + } + } + + @Override public void updateTags(List alertIds, List tags, Boolean createRule) throws ElasticsearchIndexDocumentUpdateException { @@ -287,16 +329,25 @@ public void convertToIncident(List alertIds, String incidentName, Intege Instant incidentCreationDate = Instant.now(); String incidentCreatedBy = SecurityUtils.getCurrentUserLogin().orElse("system"); + String formattedDate = incidentCreationDate.toString(); + + String script = String.format( + "ctx._source.isIncident = true; " + + "if (ctx._source.incidentDetail == null) { " + + "ctx._source.incidentDetail = [:]; " + + "} " + + "ctx._source.incidentDetail.incidentName = '%s'; " + + "ctx._source.incidentDetail.incidentId = '%s'; " + + "ctx._source.incidentDetail.creationDate = '%s'; " + + "ctx._source.incidentDetail.createdBy = '%s'; " + + "ctx._source.incidentDetail.source = '%s';", + incidentName.replace("'", "\\'"), + incidentId, + formattedDate, + incidentCreatedBy, + incidentSource + ); - String script = String.format("ctx._source.isIncident=true;" + - "if(ctx._source.incidentDetail == null) {" + - "ctx._source.incidentDetail = new HashMap();}" + - "ctx._source.incidentDetail.incidentName=\"%1$s\";" + - "ctx._source.incidentDetail.incidentId=\"%2$s\";" + - "ctx._source.incidentDetail.creationDate=\"%3$s\";" + - "ctx._source.incidentDetail.createdBy=\"%4$s\";" + - "ctx._source.incidentDetail.source=\"%5$s\";", - incidentName, incidentId, incidentCreationDate, incidentCreatedBy, incidentSource); List filters = new ArrayList<>(); filters.add(new FilterType(Constants.alertIdKeyword, OperatorType.IS_ONE_OF_TERMS, alertIds)); @@ -311,7 +362,7 @@ public void convertToIncident(List alertIds, String incidentName, Intege } } - public List getAlertsByIds(List alertIds) throws UtmElasticsearchException { + public List getAlertsByIds(List alertIds) throws UtmElasticsearchException { final String ctx = CLASS_NAME + ".getAlertsByIds"; try { if (CollectionUtils.isEmpty(alertIds)) @@ -325,12 +376,12 @@ public List getAlertsByIds(List alertIds) throws UtmElasticse .index(Constants.SYS_INDEX_PATTERN.get(SystemIndexPattern.ALERTS)) .size(Constants.LOG_ANALYZER_TOTAL_RESULTS)); - HitsMetadata hits = elasticsearchService.search(request, AlertType.class).hits(); + HitsMetadata hits = elasticsearchService.search(request, UtmAlert.class).hits(); if (hits.total().value() <= 0) return new ArrayList<>(); - List alerts = hits.hits().stream().map(Hit::source).collect(Collectors.toList()); + List alerts = hits.hits().stream().map(Hit::source).collect(Collectors.toList()); if (CollectionUtils.isEmpty(alerts)) return new ArrayList<>(); diff --git a/backend/src/main/java/com/park/utmstack/service/impl/UtmMenuServiceImpl.java b/backend/src/main/java/com/park/utmstack/service/impl/UtmMenuServiceImpl.java index 1d101d997..95db09341 100644 --- a/backend/src/main/java/com/park/utmstack/service/impl/UtmMenuServiceImpl.java +++ b/backend/src/main/java/com/park/utmstack/service/impl/UtmMenuServiceImpl.java @@ -31,7 +31,7 @@ public UtmMenuServiceImpl(UtmMenuRepository menuRepository, UtmMenuAuthorityServ } @Override - public UtmMenu save(UtmMenu menu) throws Exception { + public UtmMenu save(UtmMenu menu) { final String ctx = CLASS_NAME + ".save"; try { @@ -52,17 +52,17 @@ public UtmMenu save(UtmMenu menu) throws Exception { return menuRepository.save(menu); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } @Override - public List saveAll(List menus) throws Exception { + public List saveAll(List menus) { final String ctx = CLASS_NAME + ".saveAll"; try { return menuRepository.saveAll(menus); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } @@ -91,9 +91,9 @@ public Optional> findMenusByAuthorities(Long parentId, List getMenus(boolean includeModulesMenus) throws Exception { + public List getMenus(boolean includeModulesMenus) { final String ctx = CLASS_NAME + ".getMenus"; try { List parents = menuRepository.findAllByParentIdIsNull(); @@ -140,7 +140,7 @@ public List getMenus(boolean includeModulesMenus) throws Exception { menus.sort(Comparator.comparing(MenuType::getPosition)); return menus; } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } @@ -156,7 +156,7 @@ private List buildAuthorities(Long menuId) { /** * @param menus */ - public Boolean saveMenuStructure(List menus) throws Exception { + public Boolean saveMenuStructure(List menus) { final String ctx = CLASS_NAME + ".saveMenuStructure"; try { @@ -178,36 +178,36 @@ public Boolean saveMenuStructure(List menus) throws Exception { } return true; } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } - private UtmMenu updateMenuStructure(MenuType menu) throws Exception { + private UtmMenu updateMenuStructure(MenuType menu) { final String ctx = CLASS_NAME + ".updateMenuStructure"; try { return save(new UtmMenu(menu)); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } @Override - public List findAllByModuleNameShort(String nameShort) throws Exception { + public List findAllByModuleNameShort(String nameShort) { final String ctx = CLASS_NAME + ".findAllByModuleNameShort"; try { return menuRepository.findAllByModuleNameShort(nameShort); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } @Override - public void deleteSysMenusNotIn(List ids) throws Exception { + public void deleteSysMenusNotIn(List ids) { final String ctx = CLASS_NAME + ".findAllByModuleNameShort"; try { menuRepository.deleteSysMenusNotIn(ids); } catch (Exception e) { - throw new Exception(ctx + ": " + e.getMessage()); + throw new RuntimeException(ctx + ": " + e.getMessage()); } } diff --git a/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java b/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java index 03546f259..642799d8d 100644 --- a/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java +++ b/backend/src/main/java/com/park/utmstack/service/incident/UtmIncidentService.java @@ -6,7 +6,7 @@ import com.park.utmstack.domain.incident.UtmIncidentAlert; import com.park.utmstack.domain.incident.enums.IncidentHistoryActionEnum; import com.park.utmstack.domain.incident.enums.IncidentStatusEnum; -import com.park.utmstack.domain.shared_types.AlertType; +import com.park.utmstack.domain.shared_types.alert.UtmAlert; import com.park.utmstack.repository.incident.UtmIncidentRepository; import com.park.utmstack.service.MailService; import com.park.utmstack.service.UserService; @@ -17,6 +17,7 @@ import com.park.utmstack.service.dto.incident.NewIncidentDTO; import com.park.utmstack.service.dto.incident.RelatedIncidentAlertsDTO; import com.park.utmstack.service.incident.util.ResolveIncidentStatus; +import com.park.utmstack.util.exceptions.IncidentAlertConflictException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -27,10 +28,7 @@ import org.springframework.util.CollectionUtils; import javax.validation.Valid; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; /** @@ -76,7 +74,6 @@ public UtmIncidentService(UtmIncidentRepository utmIncidentRepository, */ public UtmIncident save(UtmIncident utmIncident) { final String ctx = ".save"; - log.debug("Request to save UtmIncident : {}", utmIncident); try { return utmIncidentRepository.save(utmIncident); } catch (Exception e) { @@ -97,8 +94,8 @@ public UtmIncident changeStatus(UtmIncident utmIncident) { try { log.debug("Request to save UtmIncident : {}", utmIncident); String oldIncident = ResolveIncidentStatus.incidentLabel(utmIncidentRepository. - findById(utmIncident.getId()) - .orElseThrow(() -> new RuntimeException("Incident not found"))); + findById(utmIncident.getId()) + .orElseThrow(() -> new RuntimeException("Incident not found"))); UtmIncident incident = utmIncidentRepository.save(utmIncident); List alerts = utmIncidentAlertService.findAllByIncidentId(incident.getId()); @@ -139,32 +136,30 @@ public UtmIncident changeStatus(UtmIncident utmIncident) { */ public UtmIncident createIncident(NewIncidentDTO newIncidentDTO) { final String ctx = CLASSNAME + ".createIncident"; - try { - UtmIncident utmIncident = new UtmIncident(); - utmIncident.setIncidentName(newIncidentDTO.getIncidentName()); - utmIncident.setIncidentDescription(newIncidentDTO.getIncidentDescription()); - utmIncident.setIncidentStatus(IncidentStatusEnum.OPEN); - Integer severity = newIncidentDTO.getAlertList().stream() + + validateAlertsNotAlreadyLinked(newIncidentDTO.getAlertList(), ctx); + + UtmIncident utmIncident = new UtmIncident(); + utmIncident.setIncidentName(newIncidentDTO.getIncidentName()); + utmIncident.setIncidentDescription(newIncidentDTO.getIncidentDescription()); + utmIncident.setIncidentStatus(IncidentStatusEnum.OPEN); + Integer severity = newIncidentDTO.getAlertList().stream() .mapToInt(RelatedIncidentAlertsDTO::getAlertSeverity).max().orElse(0); - utmIncident.setIncidentSeverity(severity); - utmIncident.setIncidentAssignedTo(newIncidentDTO.getIncidentAssignedTo()); - utmIncident.setIncidentCreatedDate(new Date().toInstant()); + utmIncident.setIncidentSeverity(severity); + utmIncident.setIncidentAssignedTo(newIncidentDTO.getIncidentAssignedTo()); + utmIncident.setIncidentCreatedDate(new Date().toInstant()); - UtmIncident savedIncident = utmIncidentRepository.save(utmIncident); + UtmIncident savedIncident = utmIncidentRepository.save(utmIncident); - saveRelatedAlerts(newIncidentDTO.getAlertList(), savedIncident.getId()); + saveRelatedAlerts(newIncidentDTO.getAlertList(), savedIncident.getId()); - sendIncidentsEmail(newIncidentDTO.getAlertList().stream().map(RelatedIncidentAlertsDTO::getAlertId).collect(Collectors.toList()), savedIncident); + sendIncidentsEmail(newIncidentDTO.getAlertList().stream().map(RelatedIncidentAlertsDTO::getAlertId).collect(Collectors.toList()), savedIncident); - String historyMessage = String.format("Incident created with %d alerts", newIncidentDTO.getAlertList().size()); - utmIncidentHistoryService.createHistory(IncidentHistoryActionEnum.INCIDENT_CREATED, savedIncident.getId(), "Incident Created", historyMessage); + String historyMessage = String.format("Incident created with %d alerts", newIncidentDTO.getAlertList().size()); + utmIncidentHistoryService.createHistory(IncidentHistoryActionEnum.INCIDENT_CREATED, savedIncident.getId(), "Incident Created", historyMessage); + + return savedIncident; - return savedIncident; - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - eventService.createEvent(msg, ApplicationEventType.ERROR); - throw new RuntimeException(msg); - } } /** @@ -175,18 +170,29 @@ public UtmIncident createIncident(NewIncidentDTO newIncidentDTO) { */ public UtmIncident addAlertsIncident(@Valid AddToIncidentDTO addToIncidentDTO) { final String ctx = CLASSNAME + ".addAlertsIncident"; - try { - log.debug("Request to add alert to UtmIncident : {}", addToIncidentDTO); - UtmIncident utmIncident = utmIncidentRepository.findById(addToIncidentDTO.getIncidentId()).orElseThrow(() -> new RuntimeException(ctx + ": Incident not found")); - saveRelatedAlerts(addToIncidentDTO.getAlertList(), utmIncident.getId()); - String historyMessage = String.format("New %d alerts added to incident", addToIncidentDTO.getAlertList().size()); - utmIncidentHistoryService.createHistory(IncidentHistoryActionEnum.INCIDENT_ALERT_ADD, utmIncident.getId(), "New alerts added to incident", historyMessage); - return utmIncident; - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - eventService.createEvent(msg, ApplicationEventType.ERROR); - throw new RuntimeException(msg); - } + + log.debug("Request to add alert to UtmIncident : {}", addToIncidentDTO); + + List alertIds = addToIncidentDTO.getAlertList(); + + String alertsIds = alertIds.stream().map(RelatedIncidentAlertsDTO::getAlertId).collect(Collectors.joining(",")); + Map extra = Map.of( + "alertIds", alertsIds, + "source", "service" + ); + String attemptMsg = String.format("Attempt to add %d alerts to incident %d", addToIncidentDTO.getAlertList().size(), addToIncidentDTO.getIncidentId()); + eventService.createEvent(attemptMsg, ApplicationEventType.INCIDENT_ALERT_ADD_ATTEMPT, extra); + + validateAlertsNotAlreadyLinked(addToIncidentDTO.getAlertList(), ctx); + UtmIncident utmIncident = utmIncidentRepository.findById(addToIncidentDTO.getIncidentId()).orElseThrow(() -> new RuntimeException(ctx + ": Incident not found")); + saveRelatedAlerts(addToIncidentDTO.getAlertList(), utmIncident.getId()); + String historyMessage = String.format("New %d alerts added to incident", addToIncidentDTO.getAlertList().size()); + utmIncidentHistoryService.createHistory(IncidentHistoryActionEnum.INCIDENT_ALERT_ADD, utmIncident.getId(), "New alerts added to incident", historyMessage); + + eventService.createEvent(historyMessage, ApplicationEventType.INCIDENT_ALERTS_ADDED, extra); + + return utmIncident; + } @Async @@ -269,13 +275,13 @@ public void delete(Long id) { private void sendIncidentsEmail(List alertIds, UtmIncident utmIncident) { final String ctx = CLASSNAME + ".sendIncidentsEmail"; try { - List alerts = utmAlertService.getAlertsByIds(alertIds); + List alerts = utmAlertService.getAlertsByIds(alertIds); if (CollectionUtils.isEmpty(alerts)) return; String[] addressToNotify = Constants.CFG.get(Constants.PROP_ALERT_ADDRESS_TO_NOTIFY_INCIDENTS) - .replace(" ", "").split(","); + .replace(" ", "").split(","); mailService.sendIncidentEmail(Arrays.asList(addressToNotify), alerts, utmIncident); @@ -284,4 +290,20 @@ private void sendIncidentsEmail(List alertIds, UtmIncident utmIncident) eventService.createEvent(msg, ApplicationEventType.ERROR); } } + + private void validateAlertsNotAlreadyLinked(List alertList, String ctx) { + + List alertIds = alertList.stream() + .map(RelatedIncidentAlertsDTO::getAlertId) + .collect(Collectors.toList()); + + List alertsFound = utmIncidentAlertService.existsAnyAlert(alertIds); + + if (!alertsFound.isEmpty()) { + String alertIdsList = String.join(", ", alertIds); + String msg = "Some alerts are already linked to another incident. Alert IDs: " + alertIdsList + ". Check the related incidents for more details."; + + throw new IncidentAlertConflictException(ctx + ": " + msg); + } + } } diff --git a/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java b/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java new file mode 100644 index 000000000..0439c563b --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/mapper/ApiKeyMapper.java @@ -0,0 +1,31 @@ +package com.park.utmstack.service.mapper; + +import com.park.utmstack.domain.api_keys.ApiKey; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import org.mapstruct.Mapper; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Optional; +import java.util.stream.Collectors; + +@Mapper(componentModel = "spring") +public class ApiKeyMapper { + + public ApiKeyResponseDTO toDto(ApiKey apiKey){ + return ApiKeyResponseDTO.builder() + .id(apiKey.getId()) + .name(apiKey.getName()) + .createdAt(apiKey.getCreatedAt()) + .expiresAt(apiKey.getExpiresAt()) + .allowedIp( + Optional.ofNullable(apiKey.getAllowedIp()) + .map(s -> Arrays.stream(s.split(",")) + .map(String::trim) + .filter(str -> !str.isEmpty()) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()) + ) + .build(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/service/network_scan/AlertAssetGroupService.java b/backend/src/main/java/com/park/utmstack/service/network_scan/AlertAssetGroupService.java new file mode 100644 index 000000000..da4784da0 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/service/network_scan/AlertAssetGroupService.java @@ -0,0 +1,53 @@ +package com.park.utmstack.service.network_scan; + +import com.park.utmstack.domain.network_scan.UtmAssetGroup; +import com.park.utmstack.repository.network_scan.UtmAssetGroupRepository; +import com.park.utmstack.repository.network_scan.UtmNetworkScanRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Slf4j +public class AlertAssetGroupService { + + private static final String CLASSNAME = "AlertAssetGroupService"; + + private final UtmNetworkScanRepository networkScanRepository; + + + @Transactional(readOnly = true) + public Map> getAssetGroupsMapForAlerts() { + final String ctx = CLASSNAME + ".getAssetGroupsMapForAlerts"; + + try { + List results = networkScanRepository.findAllAssetGroupMappings(); + + Map> assetGroupsMap = new HashMap<>(); + + for (Object[] row : results) { + String assetName = (String) row[0]; + Long groupId = (Long) row[1]; + String groupName = (String) row[2]; + + Map groupInfo = new HashMap<>(); + groupInfo.put("id", groupId); + groupInfo.put("name", groupName); + + assetGroupsMap.put(assetName, groupInfo); + } + + return assetGroupsMap; + + } catch (Exception e) { + log.error("{}: Error retrieving asset groups map: {}", ctx, e.getMessage()); + return new HashMap<>(); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/park/utmstack/service/network_scan/UtmNetworkScanService.java b/backend/src/main/java/com/park/utmstack/service/network_scan/UtmNetworkScanService.java index af77b243d..8848f85f9 100644 --- a/backend/src/main/java/com/park/utmstack/service/network_scan/UtmNetworkScanService.java +++ b/backend/src/main/java/com/park/utmstack/service/network_scan/UtmNetworkScanService.java @@ -16,6 +16,7 @@ import com.park.utmstack.util.PdfUtil; import com.park.utmstack.util.UtilPagination; import com.park.utmstack.web.rest.errors.AgentNotfoundException; +import lombok.RequiredArgsConstructor; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.domain.Page; @@ -35,6 +36,7 @@ * Service Implementation for managing UtmNetworkScan. */ @Service +@RequiredArgsConstructor @Transactional public class UtmNetworkScanService { @@ -51,22 +53,6 @@ public class UtmNetworkScanService { private final UtmDataInputStatusRepository utmDataInputStatusRepository; private final AgentGrpcService agentGrpcService; - public UtmNetworkScanService(UtmNetworkScanRepository networkScanRepository, - UtmAssetMetricsRepository assetMetricsRepository, - EntityManager em, PdfUtil pdfUtil, - UtmPortsService portsService, UtmImagesService imagesService, - UtmDataInputStatusRepository utmDataInputStatusRepository, - AgentGrpcService agentGrpcService) { - this.networkScanRepository = networkScanRepository; - this.assetMetricsRepository = assetMetricsRepository; - this.em = em; - this.pdfUtil = pdfUtil; - this.portsService = portsService; - this.imagesService = imagesService; - this.utmDataInputStatusRepository = utmDataInputStatusRepository; - this.agentGrpcService = agentGrpcService; - } - public void saveOrUpdateCustomAsset(NetworkScanDTO assetDto) throws Exception { final String ctx = CLASSNAME + ".saveOrUpdateCustomAsset"; try { @@ -170,7 +156,7 @@ public Optional searchDetails(Long id) throws Exception { try { return networkScanRepository.findById(id).map(m -> { m.setMetrics(assetMetricsRepository.findAllByAssetName(m.getAssetName())); - return new NetworkScanDTO(m, true); + return new NetworkScanDTO(m, true, utmDataInputStatusRepository); }); } catch (Exception e) { throw new Exception(ctx + ": " + e.getMessage()); @@ -189,7 +175,7 @@ public Page searchByFilters(NetworkScanFilter f, Pageable p) thr final String ctx = CLASSNAME + ".searchByFilters"; try { Page page = filter(f, p); - return page.map(m -> new NetworkScanDTO(m, false)); + return page.map(m -> new NetworkScanDTO(m, false, utmDataInputStatusRepository)); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); } diff --git a/backend/src/main/java/com/park/utmstack/service/overview/OverviewService.java b/backend/src/main/java/com/park/utmstack/service/overview/OverviewService.java index a805630e2..4c76ae2ab 100644 --- a/backend/src/main/java/com/park/utmstack/service/overview/OverviewService.java +++ b/backend/src/main/java/com/park/utmstack/service/overview/OverviewService.java @@ -295,6 +295,7 @@ private List getDefaultFilters(List dateRange){ List filters = new ArrayList<>(); filters.add(new FilterType(Constants.alertStatus, OperatorType.IS_NOT, AlertStatus.AUTOMATIC_REVIEW.getCode())); filters.add(new FilterType(Constants.alertTags, OperatorType.IS_NOT, Constants.FALSE_POSITIVE_TAG)); + filters.add(new FilterType(Constants.alertParentIdKeyword, OperatorType.DOES_NOT_EXIST, null)); if(!CollectionUtils.isEmpty(dateRange)){ filters.add(new FilterType(Constants.timestamp, OperatorType.IS_BETWEEN, dateRange)); diff --git a/backend/src/main/java/com/park/utmstack/service/reports/CustomReportService.java b/backend/src/main/java/com/park/utmstack/service/reports/CustomReportService.java index 865a8c075..879c01da0 100644 --- a/backend/src/main/java/com/park/utmstack/service/reports/CustomReportService.java +++ b/backend/src/main/java/com/park/utmstack/service/reports/CustomReportService.java @@ -6,7 +6,7 @@ import com.park.utmstack.domain.index_pattern.enums.SystemIndexPattern; import com.park.utmstack.domain.network_scan.NetworkScanFilter; import com.park.utmstack.domain.reports.types.IncidentType; -import com.park.utmstack.domain.shared_types.AlertType; +import com.park.utmstack.domain.shared_types.alert.UtmAlert; import com.park.utmstack.domain.shared_types.enums.ImageShortName; import com.park.utmstack.service.UtmImagesService; import com.park.utmstack.service.elasticsearch.ElasticsearchService; @@ -75,12 +75,12 @@ public Optional buildThreatActivityForAlerts(Instant from .query(SearchUtil.toQuery(filters)); SearchUtil.applyPaginationAndSort(srb, page, top); - HitsMetadata hits = elasticsearchService.search(srb.build(), AlertType.class).hits(); + HitsMetadata hits = elasticsearchService.search(srb.build(), UtmAlert.class).hits(); if (hits.total().value() <= 0) return Optional.empty(); - List alerts = hits.hits().stream().map(Hit::source).collect(Collectors.toList()); + List alerts = hits.hits().stream().map(Hit::source).collect(Collectors.toList()); Map vars = new HashMap<>(); vars.put("alerts", alerts); @@ -106,28 +106,28 @@ public Optional buildThreatActivityForIncidents(Instant f .query(SearchUtil.toQuery(filters)); SearchUtil.applyPaginationAndSort(srb, page, top); - HitsMetadata hits = elasticsearchService.search(srb.build(), AlertType.class).hits(); + HitsMetadata hits = elasticsearchService.search(srb.build(), UtmAlert.class).hits(); if (hits.total().value() <= 0) return Optional.empty(); - List incidentDocs = hits.hits().stream().map(Hit::source).collect(Collectors.toList()); + List incidentDocs = hits.hits().stream().map(Hit::source).collect(Collectors.toList()); List incidents = new ArrayList<>(); - for (AlertType incident : incidentDocs) { + for (UtmAlert incident : incidentDocs) { IncidentType incidentType = new IncidentType(); incidentType.setIncident(incident); String src = "", dest = ""; - if (!Objects.isNull(incident.getSource())) - src = StringUtils.hasText(incident.getSource().getHost()) - ? incident.getSource().getHost() : incident.getSource().getIp(); + if (!Objects.isNull(incident.getAdversary())) + src = StringUtils.hasText(incident.getAdversary().getHost()) + ? incident.getAdversary().getHost() : incident.getAdversary().getIp(); - if (!Objects.isNull(incident.getDestination())) - dest = StringUtils.hasText(incident.getDestination().getHost()) - ? incident.getDestination().getHost() : incident.getDestination().getIp(); + if (!Objects.isNull(incident.getTarget())) + dest = StringUtils.hasText(incident.getTarget().getHost()) + ? incident.getTarget().getHost() : incident.getTarget().getIp(); if (StringUtils.hasText(src)) incidentType.setSrcResponses(incidentJobService.findAllByAgent(src)); diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/EmailTfaService.java b/backend/src/main/java/com/park/utmstack/service/tfa/EmailTfaService.java index 23d172a72..aa3b9a892 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/EmailTfaService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/EmailTfaService.java @@ -1,5 +1,6 @@ package com.park.utmstack.service.tfa; +import com.park.utmstack.aop.logging.Loggable; import com.park.utmstack.config.Constants; import com.park.utmstack.domain.User; import com.park.utmstack.domain.tfa.TfaMethod; @@ -9,6 +10,7 @@ import com.park.utmstack.service.dto.tfa.init.Delivery; import com.park.utmstack.service.dto.tfa.init.TfaInitResponse; import com.park.utmstack.service.dto.tfa.verify.TfaVerifyResponse; +import com.park.utmstack.util.exceptions.TooManyRequestsException; import com.park.utmstack.util.exceptions.UtmMailException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -32,6 +34,7 @@ public TfaMethod getMethod() { } @Override + @Loggable public TfaInitResponse initiateSetup(User user) { final String ctx = CLASSNAME + ".initiateSetup"; try { @@ -40,7 +43,7 @@ public TfaInitResponse initiateSetup(User user) { String secret = tfaService.generateSecret(); String code = tfaService.generateCode(secret); - long expiresAt = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS) * 10 * 1000; + long expiresAt = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30); TfaSetupState state = new TfaSetupState(secret, expiresAt); cache.storeState(user.getLogin(), TfaMethod.EMAIL, state); @@ -56,6 +59,7 @@ public TfaInitResponse initiateSetup(User user) { @Override + @Loggable public TfaVerifyResponse verifyCode(User user, String code) { TfaSetupState tfaSetupState = cache.getState(user.getLogin(), TfaMethod.EMAIL) @@ -73,7 +77,7 @@ public TfaVerifyResponse verifyCode(User user, String code) { } @Override - public void persistConfiguration(User user) throws Exception { + public void persistConfiguration(User user) { String secret = cache.getState(user.getLogin(), TfaMethod.EMAIL) .orElseThrow(() -> new IllegalStateException("No TFA setup found for user: " + user.getLogin())) .getSecret(); @@ -86,11 +90,33 @@ public void generateChallenge(User user) { String secret = user.getTfaSecret(); String code = tfaService.generateCode(secret); - TfaSetupState state = new TfaSetupState(secret, System.currentTimeMillis() + Constants.EXPIRES_IN_SECONDS * 1000 * 10); + TfaSetupState state = new TfaSetupState(secret, System.currentTimeMillis() + (Constants.EXPIRES_IN_SECONDS_EMAIL * 4) * 1000 * 10); cache.storeState(user.getLogin(), TfaMethod.EMAIL, state); mailService.sendTfaVerificationCode(user, code); + } + + @Override + public void regenerateChallenge(User user) { + + TfaSetupState state = cache.getState(user.getLogin(), TfaMethod.EMAIL) + .orElseThrow(() -> new IllegalStateException("No TFA setup found for user: " + user.getLogin())); + + if (!state.canRequestChallenge()){ + throw new TooManyRequestsException("Challenge request too soon. Please wait " + state.getCooldownRemainingSeconds() + " seconds."); + } + state.setExpiresAt(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS_EMAIL)); + state.markChallengeRequested(); + + mailService.sendTfaVerificationCode(user, tfaService.generateCode(state.getSecret())); + cache.storeState(user.getLogin(), TfaMethod.EMAIL, state); + + } + + @Override + public long expirationTimeSeconds() { + return Constants.EXPIRES_IN_SECONDS_EMAIL; } } diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/EmailTotpService.java b/backend/src/main/java/com/park/utmstack/service/tfa/EmailTotpService.java index fa1abd795..1e4b358a5 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/EmailTotpService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/EmailTotpService.java @@ -9,7 +9,7 @@ import org.springframework.stereotype.Service; import org.springframework.util.Assert; -import static com.park.utmstack.config.Constants.EXPIRES_IN_SECONDS; +import static com.park.utmstack.config.Constants.EXPIRES_IN_SECONDS_EMAIL; @Service public class EmailTotpService { @@ -38,7 +38,7 @@ public String generateCode(String secret) { try { Assert.hasText(secret, "Secret value is missing"); CodeGenerator codeGenerator = new DefaultCodeGenerator(); - return codeGenerator.generate(secret, Math.floorDiv(timeProvider.getTime(), EXPIRES_IN_SECONDS)); + return codeGenerator.generate(secret, Math.floorDiv(timeProvider.getTime(), EXPIRES_IN_SECONDS_EMAIL)); } catch (Exception e) { throw new RuntimeException(ctx + ": " + e.getMessage()); } @@ -57,7 +57,7 @@ public boolean validateCode(String secret, String code) { Assert.hasText(code, "Code value is missing"); DefaultCodeVerifier verifier = new DefaultCodeVerifier(new DefaultCodeGenerator(), timeProvider); - verifier.setTimePeriod(EXPIRES_IN_SECONDS); + verifier.setTimePeriod(EXPIRES_IN_SECONDS_EMAIL); verifier.setAllowedTimePeriodDiscrepancy(1); return verifier.isValidCode(secret, code); diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java b/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java index a9e40bf35..d93ed9eeb 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/TfaMethodService.java @@ -12,9 +12,13 @@ public interface TfaMethodService { TfaVerifyResponse verifyCode(User use, String code); - void persistConfiguration(User use) throws Exception; + void persistConfiguration(User use); void generateChallenge(User user); + void regenerateChallenge(User user); + + long expirationTimeSeconds(); + } diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java b/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java index a0aabd6b1..8fd6868f0 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/TfaService.java @@ -36,18 +36,28 @@ public TfaVerifyResponse verifyCode(User user, TfaVerifyRequest request) { return selected.verifyCode(user, request.getCode()); } - public void persistConfiguration(TfaMethod method) throws Exception { + public void persistConfiguration(TfaMethod method) { User user = userService.getCurrentUserLogin(); TfaMethodService selected = getMethodService(method); selected.persistConfiguration(user); } - public void generateChallenge(User user) throws Exception { + public long generateChallenge(User user) { TfaMethod method = TfaMethod.valueOf(user.getTfaMethod()); TfaMethodService selected = getMethodService(method); selected.generateChallenge(user); + + return selected.expirationTimeSeconds(); + } + + public void regenerateChallenge(User user) { + + TfaMethod method = TfaMethod.valueOf(user.getTfaMethod()); + + TfaMethodService selected = getMethodService(method); + selected.regenerateChallenge(user); } } diff --git a/backend/src/main/java/com/park/utmstack/service/tfa/TotpTfaService.java b/backend/src/main/java/com/park/utmstack/service/tfa/TotpTfaService.java index 163ca054e..6ce138f1f 100644 --- a/backend/src/main/java/com/park/utmstack/service/tfa/TotpTfaService.java +++ b/backend/src/main/java/com/park/utmstack/service/tfa/TotpTfaService.java @@ -4,6 +4,7 @@ import com.google.zxing.MultiFormatWriter; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; +import com.park.utmstack.aop.logging.Loggable; import com.park.utmstack.config.Constants; import com.park.utmstack.domain.User; import com.park.utmstack.domain.tfa.TfaMethod; @@ -12,6 +13,7 @@ import com.park.utmstack.service.dto.tfa.init.Delivery; import com.park.utmstack.service.dto.tfa.init.TfaInitResponse; import com.park.utmstack.service.dto.tfa.verify.TfaVerifyResponse; +import com.park.utmstack.util.exceptions.TooManyRequestsException; import com.warrenstrange.googleauth.GoogleAuthenticator; import org.springframework.stereotype.Service; @@ -44,7 +46,7 @@ public TfaMethod getMethod() { @Override public TfaInitResponse initiateSetup(User user) { String secret = authenticator.createCredentials().getKey(); - long expiresAt = System.currentTimeMillis() + Constants.EXPIRES_IN_SECONDS * 10 * 1000; + long expiresAt = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS_TOTP * 10); TfaSetupState state = new TfaSetupState(secret, expiresAt); cache.storeState(user.getLogin(), TfaMethod.TOTP, state); @@ -54,7 +56,7 @@ public TfaInitResponse initiateSetup(User user) { String qrBase64 = generateQrBase64(uri); Delivery delivery = new Delivery(TfaMethod.TOTP, qrBase64); - return new TfaInitResponse("pending", delivery, Constants.EXPIRES_IN_SECONDS * 10); + return new TfaInitResponse("pending", delivery, Constants.EXPIRES_IN_SECONDS_TOTP * 10); } @Override @@ -74,7 +76,7 @@ public TfaVerifyResponse verifyCode(User user, String code) { @Override - public void persistConfiguration(User user) throws Exception { + public void persistConfiguration(User user) { String secret = cache.getState(user.getLogin(), TfaMethod.TOTP) .orElseThrow(() -> new IllegalStateException("No TFA setup found for user: " + user.getLogin())) .getSecret(); @@ -86,7 +88,23 @@ public void persistConfiguration(User user) throws Exception { public void generateChallenge(User user) { cache.clear(user.getLogin(), TfaMethod.TOTP); String secret = user.getTfaSecret(); - TfaSetupState state = new TfaSetupState(secret, System.currentTimeMillis() + (Constants.EXPIRES_IN_SECONDS + 10) * 1000); + TfaSetupState state = new TfaSetupState(secret, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS_TOTP)); + cache.storeState(user.getLogin(), TfaMethod.TOTP, state); + } + + @Override + public void regenerateChallenge(User user) { + + TfaSetupState state = cache.getState(user.getLogin(), TfaMethod.TOTP) + .orElseThrow(() -> new IllegalStateException("No TFA setup found for user: " + user.getLogin())); + + if (!state.canRequestChallenge()){ + throw new TooManyRequestsException("Challenge request too soon. Please wait " + state.getCooldownRemainingSeconds() + " seconds."); + } + + state.setExpiresAt(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(Constants.EXPIRES_IN_SECONDS_TOTP)); + state.markChallengeRequested(); + cache.storeState(user.getLogin(), TfaMethod.TOTP, state); } @@ -103,6 +121,11 @@ private String generateQrBase64(String uri) { } } + @Override + public long expirationTimeSeconds() { + return Constants.EXPIRES_IN_SECONDS_TOTP; + } + } diff --git a/backend/src/main/java/com/park/utmstack/util/AlertUtil.java b/backend/src/main/java/com/park/utmstack/util/AlertUtil.java index fa7c407c1..c85b164ae 100644 --- a/backend/src/main/java/com/park/utmstack/util/AlertUtil.java +++ b/backend/src/main/java/com/park/utmstack/util/AlertUtil.java @@ -34,6 +34,7 @@ public Long countAlertsByStatus(int status) { List filters = new ArrayList<>(); filters.add(new FilterType(Constants.alertStatus, OperatorType.IS, status)); filters.add(new FilterType(Constants.alertTags, OperatorType.DOES_NOT_CONTAIN, Constants.FALSE_POSITIVE_TAG)); + filters.add(new FilterType(Constants.alertParentIdKeyword, OperatorType.DOES_NOT_EXIST, null)); SearchRequest.Builder srb = new SearchRequest.Builder(); srb.query(SearchUtil.toQuery(filters)) @@ -47,4 +48,28 @@ public Long countAlertsByStatus(int status) { throw new RuntimeException(ctx + ": " + e.getMessage()); } } + + public Long countAllAlertsByStatus(int status) { + final String ctx = CLASSNAME + ".countAlertsByStatus"; + final String AGG_NAME = "count_open_alerts"; + try { + if (!elasticsearchService.indexExist(Constants.SYS_INDEX_PATTERN.get(SystemIndexPattern.ALERTS))) + return 0L; + + List filters = new ArrayList<>(); + filters.add(new FilterType(Constants.alertStatus, OperatorType.IS, status)); + filters.add(new FilterType(Constants.alertTags, OperatorType.DOES_NOT_CONTAIN, Constants.FALSE_POSITIVE_TAG)); + + SearchRequest.Builder srb = new SearchRequest.Builder(); + srb.query(SearchUtil.toQuery(filters)) + .index(Constants.SYS_INDEX_PATTERN.get(SystemIndexPattern.ALERTS)) + .aggregations(AGG_NAME, a -> a.valueCount(c -> c.field(Constants.alertStatus))) + .size(0); + + SearchResponse response = elasticsearchService.search(srb.build(), Object.class); + return (long) response.aggregations().get(AGG_NAME).valueCount().value(); + } catch (Exception e) { + throw new RuntimeException(ctx + ": " + e.getMessage()); + } + } } diff --git a/backend/src/main/java/com/park/utmstack/util/RequestContextUtils.java b/backend/src/main/java/com/park/utmstack/util/RequestContextUtils.java new file mode 100644 index 000000000..5e11eddaa --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/RequestContextUtils.java @@ -0,0 +1,14 @@ +package com.park.utmstack.util; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.util.Optional; + +public class RequestContextUtils { + public static Optional getCurrentRequest() { + ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return Optional.ofNullable(attrs).map(ServletRequestAttributes::getRequest); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/UtilResponse.java b/backend/src/main/java/com/park/utmstack/util/ResponseUtil.java similarity index 88% rename from backend/src/main/java/com/park/utmstack/util/UtilResponse.java rename to backend/src/main/java/com/park/utmstack/util/ResponseUtil.java index d5d1862be..3111272ec 100644 --- a/backend/src/main/java/com/park/utmstack/util/UtilResponse.java +++ b/backend/src/main/java/com/park/utmstack/util/ResponseUtil.java @@ -4,7 +4,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -public class UtilResponse { +public class ResponseUtil { public static ResponseEntity buildErrorResponse(HttpStatus errorCode, String msg) { return ResponseEntity.status(errorCode) .headers(HeaderUtil.createFailureAlert("", "", @@ -32,6 +32,10 @@ public static ResponseEntity buildPreconditionFailedResponse(String msg) return buildErrorResponse(HttpStatus.PRECONDITION_FAILED, msg); } + public static ResponseEntity buildTooManyRequestResponse(String msg) { + return buildErrorResponse(HttpStatus.TOO_MANY_REQUESTS, msg); + } + public static ResponseEntity buildNotFoundResponse(String msg) { return buildErrorResponse(HttpStatus.NOT_FOUND, msg); } diff --git a/backend/src/main/java/com/park/utmstack/util/events/ProviderChangedEvent.java b/backend/src/main/java/com/park/utmstack/util/events/ProviderChangedEvent.java new file mode 100644 index 000000000..e1a07aeea --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/events/ProviderChangedEvent.java @@ -0,0 +1,9 @@ +package com.park.utmstack.util.events; + +import org.springframework.context.ApplicationEvent; + +public class ProviderChangedEvent extends ApplicationEvent { + public ProviderChangedEvent(Object source) { + super(source); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java new file mode 100644 index 000000000..20577f508 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyExistException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class ApiKeyExistException extends RuntimeException { + public ApiKeyExistException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java new file mode 100644 index 000000000..c5a13ead4 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyInvalidAccessException.java @@ -0,0 +1,9 @@ +package com.park.utmstack.util.exceptions; + +import org.springframework.security.core.AuthenticationException; + +public class ApiKeyInvalidAccessException extends AuthenticationException { + public ApiKeyInvalidAccessException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java new file mode 100644 index 000000000..173d8f442 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ApiKeyNotFoundException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class ApiKeyNotFoundException extends RuntimeException { + public ApiKeyNotFoundException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/ElasticsearchIndexDocumentUpdateException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/ElasticsearchIndexDocumentUpdateException.java index 195ef2af7..61cad6d0a 100644 --- a/backend/src/main/java/com/park/utmstack/util/exceptions/ElasticsearchIndexDocumentUpdateException.java +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/ElasticsearchIndexDocumentUpdateException.java @@ -1,7 +1,7 @@ package com.park.utmstack.util.exceptions; -public class ElasticsearchIndexDocumentUpdateException extends Exception { +public class ElasticsearchIndexDocumentUpdateException extends RuntimeException { public ElasticsearchIndexDocumentUpdateException(String message) { super(message); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/IdpNotFoundException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/IdpNotFoundException.java new file mode 100644 index 000000000..cb6a04ad5 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/IdpNotFoundException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class IdpNotFoundException extends RuntimeException { + public IdpNotFoundException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/IncidentAlertConflictException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/IncidentAlertConflictException.java new file mode 100644 index 000000000..c5aa47586 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/IncidentAlertConflictException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class IncidentAlertConflictException extends RuntimeException { + public IncidentAlertConflictException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/InvalidIdpConfigException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/InvalidIdpConfigException.java new file mode 100644 index 000000000..d6fd26766 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/InvalidIdpConfigException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class InvalidIdpConfigException extends RuntimeException { + public InvalidIdpConfigException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/InvalidTfaStageException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/InvalidTfaStageException.java new file mode 100644 index 000000000..207554ca9 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/InvalidTfaStageException.java @@ -0,0 +1,8 @@ +package com.park.utmstack.util.exceptions; + +public class InvalidTfaStageException extends RuntimeException { + public InvalidTfaStageException(String message) { + super(message); + } +} + diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/NoAlertsProvidedException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/NoAlertsProvidedException.java new file mode 100644 index 000000000..9f8c7b22e --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/NoAlertsProvidedException.java @@ -0,0 +1,7 @@ +package com.park.utmstack.util.exceptions; + +public class NoAlertsProvidedException extends RuntimeException { + public NoAlertsProvidedException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/TfaVerificationException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/TfaVerificationException.java new file mode 100644 index 000000000..68e73cc58 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/TfaVerificationException.java @@ -0,0 +1,8 @@ +package com.park.utmstack.util.exceptions; + +public class TfaVerificationException extends RuntimeException { + public TfaVerificationException(String message) { + super(message); + } +} + diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/TooManyRequestsException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/TooManyRequestsException.java new file mode 100644 index 000000000..0ca4da9fc --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/TooManyRequestsException.java @@ -0,0 +1,8 @@ +package com.park.utmstack.util.exceptions; + +public class TooManyRequestsException extends RuntimeException { + public TooManyRequestsException(String message) { + super(message); + } +} + diff --git a/backend/src/main/java/com/park/utmstack/util/exceptions/UtmElasticsearchException.java b/backend/src/main/java/com/park/utmstack/util/exceptions/UtmElasticsearchException.java index 9f9488ee0..693014d73 100644 --- a/backend/src/main/java/com/park/utmstack/util/exceptions/UtmElasticsearchException.java +++ b/backend/src/main/java/com/park/utmstack/util/exceptions/UtmElasticsearchException.java @@ -1,6 +1,6 @@ package com.park.utmstack.util.exceptions; -public class UtmElasticsearchException extends Exception { +public class UtmElasticsearchException extends RuntimeException { public UtmElasticsearchException(String message) { super(message); } diff --git a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java new file mode 100644 index 000000000..55dfe9593 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDR.java @@ -0,0 +1,23 @@ +package com.park.utmstack.validation.api_key; + + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Documented +@Constraint(validatedBy = ValidIPOrCIDRValidator.class) +@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}) +@Retention(RUNTIME) +public @interface ValidIPOrCIDR { + String message() default "Invalid IP address or CIDR notation"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java new file mode 100644 index 000000000..8324094f8 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/validation/api_key/ValidIPOrCIDRValidator.java @@ -0,0 +1,37 @@ +package com.park.utmstack.validation.api_key; + + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.regex.Pattern; + +public class ValidIPOrCIDRValidator implements ConstraintValidator { + + private static final Pattern IPV4_PATTERN = Pattern.compile( + "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)$" + ); + + private static final Pattern IPV4_CIDR_PATTERN = Pattern.compile( + "^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)/(\\d|[1-2]\\d|3[0-2])$" + ); + private static final Pattern IPV6_PATTERN = Pattern.compile( + "^(?:[\\da-fA-F]{1,4}:){7}[\\da-fA-F]{1,4}$" + ); + + private static final Pattern IPV6_CIDR_PATTERN = Pattern.compile( + "^(?:[\\da-fA-F]{1,4}:){7}[\\da-fA-F]{1,4}/(\\d|[1-9]\\d|1[01]\\d|12[0-8])$" + ); + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + // Allow null or empty values; use @NotNull/@NotEmpty to enforce non-null if needed. + if (value == null || value.trim().isEmpty()) { + return true; + } + String trimmed = value.trim(); + if (IPV4_PATTERN.matcher(trimmed).matches() || IPV4_CIDR_PATTERN.matcher(trimmed).matches()) { + return true; + } + return IPV6_PATTERN.matcher(trimmed).matches() || IPV6_CIDR_PATTERN.matcher(trimmed).matches(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java b/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java index 7be824025..0744540ef 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/UserJWTController.java @@ -1,31 +1,28 @@ package com.park.utmstack.web.rest; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.park.utmstack.aop.logging.AuditEvent; import com.park.utmstack.config.Constants; -import com.park.utmstack.domain.Authority; import com.park.utmstack.domain.User; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.federation_service.UtmFederationServiceClient; -import com.park.utmstack.domain.tfa.TfaMethod; +import com.park.utmstack.loggin.LogContextBuilder; import com.park.utmstack.repository.federation_service.UtmFederationServiceClientRepository; import com.park.utmstack.security.TooMuchLoginAttemptsException; import com.park.utmstack.security.jwt.JWTFilter; import com.park.utmstack.security.jwt.TokenProvider; -import com.park.utmstack.service.MailService; import com.park.utmstack.service.UserService; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.dto.jwt.JWTToken; import com.park.utmstack.service.dto.jwt.LoginResponseDTO; import com.park.utmstack.service.login_attempts.LoginAttemptService; -import com.park.utmstack.service.tfa.EmailTotpService; import com.park.utmstack.service.tfa.TfaService; import com.park.utmstack.util.CipherUtil; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.util.exceptions.InvalidConnectionKeyException; import com.park.utmstack.web.rest.util.HeaderUtil; import com.park.utmstack.web.rest.vm.LoginVM; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -33,27 +30,28 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Base64Utils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; -import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; /** * Controller to authenticate users. */ @RestController +@RequiredArgsConstructor +@Slf4j @RequestMapping("/api") public class UserJWTController { private static final String CLASSNAME = "UserJWTController"; - private static final String TFA_METHOD = "Tfa-Method"; - private final Logger log = LoggerFactory.getLogger(UserJWTController.class); private final TokenProvider tokenProvider; private final AuthenticationManager authenticationManager; @@ -62,78 +60,81 @@ public class UserJWTController { private final LoginAttemptService loginAttemptService; private final UtmFederationServiceClientRepository fsClientRepository; private final TfaService tfaService; - - public UserJWTController(TokenProvider tokenProvider, - AuthenticationManager authenticationManager, - ApplicationEventService applicationEventService, - UserService userService, - LoginAttemptService loginAttemptService, - UtmFederationServiceClientRepository fsClientRepository, TfaService tfaService) { - this.tokenProvider = tokenProvider; - this.authenticationManager = authenticationManager; - this.applicationEventService = applicationEventService; - this.userService = userService; - this.loginAttemptService = loginAttemptService; - this.fsClientRepository = fsClientRepository; - this.tfaService = tfaService; - } - + private final LogContextBuilder logContextBuilder; + private final UserDetailsService userDetailsService; + private final PasswordEncoder passwordEncoder; + + @AuditEvent( + attemptType = ApplicationEventType.AUTH_ATTEMPT, + attemptMessage = "Authentication attempt registered", + successType = ApplicationEventType.UNDEFINED, + successMessage = "" + ) @PostMapping("/authenticate") - public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM) { - final String ctx = CLASSNAME + ".authorize"; - try { - if (loginAttemptService.isBlocked()) - throw new TooMuchLoginAttemptsException(String.format("Client IP %1$s blocked due to too many failed login attempts", loginAttemptService.getClientIP())); + public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, HttpServletRequest request) { - boolean isTfaEnabled = Boolean.parseBoolean(Constants.CFG.get(Constants.PROP_TFA_ENABLE)); + if (loginAttemptService.isBlocked()) { + String ip = loginAttemptService.getClientIP(); + throw new TooMuchLoginAttemptsException(String.format("Authentication blocked: IP %s exceeded login attempt threshold", ip)); + } - UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword()); + boolean isAuth = this.tokenProvider.shouldBypassTfa(request); + boolean isTfaEnabled = Boolean.parseBoolean(Constants.CFG.get(Constants.PROP_TFA_ENABLE)); - Authentication authentication = this.authenticationManager.authenticate(authenticationToken); - SecurityContextHolder.getContext().setAuthentication(authentication); - String tempToken = tokenProvider.createToken(authentication, false, false); + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword()); - User user = userService.getUserWithAuthoritiesByLogin(loginVM.getUsername()) - .orElseThrow(() -> new BadCredentialsException("User " + loginVM.getUsername() + " not found")); + Authentication authentication = authenticationManager.authenticate(authenticationToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + + String token = tokenProvider.createToken(authentication, false, isAuth); + + User user = userService.getUserWithAuthoritiesByLogin(loginVM.getUsername()) + .orElseThrow(() -> new BadCredentialsException("Authentication failed: user '" + loginVM.getUsername() + "' not found")); + + boolean isTfaSetup = isTfaEnabled && user.getTfaMethod() != null && !user.getTfaMethod().isEmpty() && !isAuth; + Map args = logContextBuilder.buildArgs(request); + Long tfaExpiresInSeconds = 0L; + + if (isTfaSetup) { + tfaExpiresInSeconds = tfaService.generateChallenge(user); + + args.put("tfaMethod", user.getTfaMethod()); + applicationEventService.createEvent( + "TFA challenge issued for user '" + user.getLogin() + "' via method '" + user.getTfaMethod() + "'", + ApplicationEventType.TFA_CODE_SENT, + args + ); + } else { + applicationEventService.createEvent( + "Login successfully completed for user '" + user.getLogin() + "'", + ApplicationEventType.AUTH_SUCCESS, + args + ); + } - if (isTfaEnabled && (user.getTfaMethod() != null && !user.getTfaMethod().isEmpty())) { - tfaService.generateChallenge(user); - } + LoginResponseDTO response = LoginResponseDTO.builder() + .token(token) + .method(user.getTfaMethod()) + .success(true) + .tfaConfigured(isTfaSetup) + .forceTfa(!isAuth) + .tfaExpiresInSeconds(tfaExpiresInSeconds) + .build(); - return new ResponseEntity<>( LoginResponseDTO.builder() - .token(tempToken) - .method(user.getTfaMethod()) - .success(true) - .tfaRequired(isTfaEnabled) - .build(), HttpStatus.OK); - } catch (BadCredentialsException e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildUnauthorizedResponse(msg); - } catch (TooMuchLoginAttemptsException e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildLockedResponse(msg); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildInternalServerErrorResponse(msg); - } + return ResponseEntity.ok(response); } + @GetMapping("/check-credentials") public ResponseEntity checkPassword(@Valid @RequestParam String password, @RequestParam String checkUUID) { final String ctx = CLASSNAME + ".checkPassword"; try { User user = userService.getCurrentUserLogin(); - UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(user.getLogin(), password); - Authentication authentication = this.authenticationManager.authenticate(authenticationToken); - if (authentication.isAuthenticated()) { + + UserDetails userDetails = userDetailsService.loadUserByUsername(user.getLogin()); + + if (passwordEncoder.matches(password, userDetails.getPassword())) { return new ResponseEntity<>(checkUUID, HttpStatus.OK); } else { return new ResponseEntity<>(checkUUID, HttpStatus.BAD_REQUEST); @@ -143,7 +144,7 @@ public ResponseEntity checkPassword(@Valid @RequestParam String password log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert("", "", msg)).body(null); + HeaderUtil.createFailureAlert("", "", msg)).body(null); } } @@ -155,7 +156,7 @@ public ResponseEntity authorizeFederationServiceManager(@Valid @Reques throw new InvalidConnectionKeyException("It's needed to provide a connection key"); UtmFederationServiceClient fsToken = fsClientRepository.findByFsClientToken(token) - .orElseThrow(() -> new InvalidConnectionKeyException("Unrecognized connection key")); + .orElseThrow(() -> new InvalidConnectionKeyException("Unrecognized connection key")); String[] tokenInfo = new String(Base64Utils.decodeFromUrlSafeString(fsToken.getFsClientToken())).split("\\|"); @@ -166,7 +167,7 @@ public ResponseEntity authorizeFederationServiceManager(@Valid @Reques throw new InvalidConnectionKeyException("Connection key is corrupt, unrecognized instance");*/ UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(Constants.FS_USER, CipherUtil.decrypt(tokenInfo[1], System.getenv(Constants.ENV_ENCRYPTION_KEY))); + new UsernamePasswordAuthenticationToken(Constants.FS_USER, CipherUtil.decrypt(tokenInfo[1], System.getenv(Constants.ENV_ENCRYPTION_KEY))); Authentication authentication = this.authenticationManager.authenticate(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authentication); @@ -181,12 +182,12 @@ public ResponseEntity authorizeFederationServiceManager(@Valid @Reques String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildBadRequestResponse(msg); + return ResponseUtil.buildBadRequestResponse(msg); } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildInternalServerErrorResponse(msg); + return ResponseUtil.buildInternalServerErrorResponse(msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/UtmAlertResource.java b/backend/src/main/java/com/park/utmstack/web/rest/UtmAlertResource.java index 6c14e09d8..d4b217b1e 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/UtmAlertResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/UtmAlertResource.java @@ -1,11 +1,17 @@ package com.park.utmstack.web.rest; +import com.park.utmstack.aop.logging.AuditEvent; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.service.UtmAlertService; import com.park.utmstack.service.application_events.ApplicationEventService; +import com.park.utmstack.service.dto.alert.ConvertToIncidentRequestBody; +import com.park.utmstack.service.dto.alert.UpdateAlertStatusRequestBody; +import com.park.utmstack.service.dto.alert.UpdateAlertTagsRequestBody; import com.park.utmstack.util.AlertUtil; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.enums.AlertStatus; import com.park.utmstack.web.rest.util.HeaderUtil; +import lombok.Data; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -15,6 +21,7 @@ import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; +import java.io.IOException; import java.util.List; /** @@ -40,63 +47,61 @@ public UtmAlertResource(UtmAlertService utmAlertService, } @PostMapping("/utm-alerts/status") - public ResponseEntity updateAlertStatus(@RequestBody UpdateAlertStatusRequestBody rq) { + @AuditEvent( + attemptType = ApplicationEventType.ALERT_UPDATE_ATTEMPT, + attemptMessage = "Attempt to update alert status initiated", + successType = ApplicationEventType.ALERT_UPDATE_SUCCESS, + successMessage = "Alert status updated successfully" + ) + public ResponseEntity updateAlertStatus(@RequestBody UpdateAlertStatusRequestBody rq) throws IOException { final String ctx = CLASSNAME + ".updateAlertStatus"; - try { - utmAlertService.updateStatus(rq.getAlertIds(), rq.getStatus(), rq.getStatusObservation()); - return ResponseEntity.ok().build(); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert("", "", msg)).body(null); + if (rq.getStatus() == AlertStatus.COMPLETED.getCode() && rq.isAddFalsePositiveTag()) { + utmAlertService.updateStatusAndTag(rq.getAlertIds(), rq.getStatus(), rq.getStatusObservation()); } + utmAlertService.updateStatus(rq.getAlertIds(), rq.getStatus(), rq.getStatusObservation()); + + return ResponseEntity.ok().build(); } @PostMapping("/utm-alerts/notes") + @AuditEvent( + attemptType = ApplicationEventType.ALERT_NOTE_UPDATE_ATTEMPT, + attemptMessage = "Attempt to update alert notes initiated", + successType = ApplicationEventType.ALERT_NOTE_UPDATE_SUCCESS, + successMessage = "Alert notes updated successfully" + ) public ResponseEntity updateAlertNotes(@RequestBody(required = false) String notes, @RequestParam String alertId) { final String ctx = CLASSNAME + ".updateAlertNotes"; - try { - utmAlertService.updateNotes(alertId, notes); - return ResponseEntity.ok().build(); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert("", "", msg)).body(null); - } + utmAlertService.updateNotes(alertId, notes); + return ResponseEntity.ok().build(); } @PostMapping("/utm-alerts/tags") + @AuditEvent( + attemptType = ApplicationEventType.ALERT_TAG_UPDATE_ATTEMPT, + attemptMessage = "Attempt to update alert tags initiated", + successType = ApplicationEventType.ALERT_TAG_UPDATE_SUCCESS, + successMessage = "Alert tags updated successfully" + ) public ResponseEntity updateAlertTags(@RequestBody @Valid UpdateAlertTagsRequestBody body) { final String ctx = CLASSNAME + ".updateAlertTags"; - try { - utmAlertService.updateTags(body.getAlertIds(), body.getTags(), body.isCreateRule()); - return ResponseEntity.ok().build(); - } catch (Exception ex) { - String msg = ctx + ": " + ex.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert("", "", msg)).body(null); - } + utmAlertService.updateTags(body.getAlertIds(), body.getTags(), body.getCreateRule()); + return ResponseEntity.ok().build(); } @PostMapping("/utm-alerts/convert-to-incident") + @AuditEvent( + attemptType = ApplicationEventType.ALERT_CONVERT_TO_INCIDENT_ATTEMPT, + attemptMessage = "Attempt to convert alerts to incident initiated", + successType = ApplicationEventType.ALERT_CONVERT_TO_INCIDENT_SUCCESS, + successMessage = "Alerts converted to incident successfully" + ) public ResponseEntity convertToIncident(@RequestBody @Valid ConvertToIncidentRequestBody body) { final String ctx = CLASSNAME + ".convertToIncident"; - try { - utmAlertService.convertToIncident(body.getEventIds(), body.getIncidentName(),body.getIncidentId(), body.getIncidentSource()); - return ResponseEntity.ok().build(); - } catch (Exception ex) { - String msg = ctx + ": " + ex.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert("", "", msg)).body(null); - } + + utmAlertService.convertToIncident(body.getEventIds(), body.getIncidentName(),body.getIncidentId(), body.getIncidentSource()); + return ResponseEntity.ok().build(); + } @GetMapping("/utm-alerts/count-open-alerts") @@ -108,138 +113,7 @@ public ResponseEntity countOpenAlerts() { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); - } - } - - public static class UpdateAlertTagsRequestBody { - @NotNull - private List alertIds; - private List tags; - @NotNull - private Boolean createRule; - - public List getAlertIds() { - return alertIds; - } - - public void setAlertIds(List alertIds) { - this.alertIds = alertIds; - } - - public List getTags() { - return tags; - } - - public void setTags(List tags) { - this.tags = tags; - } - - public Boolean isCreateRule() { - return createRule; - } - - public void setCreateRule(Boolean createRule) { - this.createRule = createRule; - } - } - - public static class UpdateAlertStatusRequestBody { - @NotNull - private List alertIds; - private String statusObservation; - @NotNull - private int status; - - public List getAlertIds() { - return alertIds; - } - - public void setAlertIds(List alertIds) { - this.alertIds = alertIds; - } - - public String getStatusObservation() { - return statusObservation; - } - - public void setStatusObservation(String statusObservation) { - this.statusObservation = statusObservation; - } - - public int getStatus() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - } - - public static class UpdateAlertSolutionRequestBody { - @NotNull - private String alertName; - @NotNull - private String solution; - - public String getAlertName() { - return alertName; - } - - public void setAlertName(String alertName) { - this.alertName = alertName; - } - - public String getSolution() { - return solution; - } - - public void setSolution(String solution) { - this.solution = solution; - } - } - - public static class ConvertToIncidentRequestBody { - @NotNull - private List eventIds; - @NotNull - @Pattern(regexp = "^[^\"]*$", message = "Double quotes are not allowed") - private String incidentName; - @NotNull - private Integer incidentId; - @NotNull - private String incidentSource; - - public List getEventIds() { - return eventIds; - } - - public void setEventIds(List eventIds) { - this.eventIds = eventIds; - } - - public String getIncidentName() { - return incidentName; - } - - public void setIncidentName(String incidentName) { - this.incidentName = incidentName; - } - - public Integer getIncidentId() { - return incidentId; - } - - public void setIncidentId(Integer incidentId) { - this.incidentId = incidentId; - } - - public String getIncidentSource() { - return incidentSource; - } - - public void setIncidentSource(String incidentSource) { - this.incidentSource = incidentSource; + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/UtmConfigurationParameterResource.java b/backend/src/main/java/com/park/utmstack/web/rest/UtmConfigurationParameterResource.java index 5f16bd876..5360dbc7d 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/UtmConfigurationParameterResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/UtmConfigurationParameterResource.java @@ -9,7 +9,7 @@ import com.park.utmstack.service.dto.UtmConfigurationParameterCriteria; import com.park.utmstack.service.mail_config.MailConfigService; import com.park.utmstack.service.validators.email.EmailValidatorService; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.util.exceptions.UtmMailException; import com.park.utmstack.web.rest.util.PaginationUtil; import org.slf4j.Logger; @@ -25,7 +25,6 @@ import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.*; -import tech.jhipster.web.util.ResponseUtil; import javax.mail.MessagingException; import javax.validation.Valid; @@ -85,7 +84,7 @@ public ResponseEntity updateConfigurationParameters(@Valid @RequestBody Li String msg = String.format("Validation failed for field %s.", parameter.getConfParamShort()); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildPreconditionFailedResponse(msg); + return ResponseUtil.buildPreconditionFailedResponse(msg); } } } @@ -95,17 +94,17 @@ public ResponseEntity updateConfigurationParameters(@Valid @RequestBody Li String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildPreconditionFailedResponse(msg); + return ResponseUtil.buildPreconditionFailedResponse(msg); } catch (IllegalArgumentException e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildBadRequestResponse(msg); + return ResponseUtil.buildBadRequestResponse(msg); } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildInternalServerErrorResponse(msg); + return ResponseUtil.buildInternalServerErrorResponse(msg); } } @@ -135,7 +134,7 @@ public ResponseEntity> getAllUtmConfigurationPar public ResponseEntity getUtmConfigurationParameter(@PathVariable Long id) { log.debug("REST request to get UtmConfigurationParameter : {}", id); Optional utmConfigurationParameter = utmConfigurationParameterService.findOne(id); - return ResponseUtil.wrapOrNotFound(utmConfigurationParameter); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(utmConfigurationParameter); } @PostMapping ("/checkEmailConfiguration") @@ -148,12 +147,12 @@ public ResponseEntity checkEmailConfiguration(@Valid @RequestBody List> getConfigurationSections(@P final String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildInternalServerErrorResponse(msg); + return ResponseUtil.buildInternalServerErrorResponse(msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/UtmImagesResource.java b/backend/src/main/java/com/park/utmstack/web/rest/UtmImagesResource.java index d45720ba5..3deddc470 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/UtmImagesResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/UtmImagesResource.java @@ -6,14 +6,13 @@ import com.park.utmstack.repository.UtmImagesRepository; import com.park.utmstack.service.UtmImagesService; import com.park.utmstack.service.application_events.ApplicationEventService; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.util.HeaderUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import tech.jhipster.web.util.ResponseUtil; import javax.validation.Valid; import java.util.List; @@ -50,7 +49,7 @@ public ResponseEntity updateImage(@Valid @RequestBody UtmImages image Optional imageOpt = imagesRepository.findById(image.getShortName()); if (imageOpt.isEmpty()) - return UtilResponse.buildBadRequestResponse("Image short name not recognized: " + image.getShortName()); + return ResponseUtil.buildBadRequestResponse("Image short name not recognized: " + image.getShortName()); UtmImages img = imageOpt.get(); img.setUserImg(image.getUserImg()); @@ -59,7 +58,7 @@ public ResponseEntity updateImage(@Valid @RequestBody UtmImages image String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildInternalServerErrorResponse(msg); + return ResponseUtil.buildInternalServerErrorResponse(msg); } } @@ -82,7 +81,7 @@ public ResponseEntity getImage(@PathVariable ImageShortName shortName final String ctx = CLASSNAME + ".getImage"; try { Optional utmImages = utmImagesService.findOne(shortName); - return ResponseUtil.wrapOrNotFound(utmImages); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(utmImages); } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); diff --git a/backend/src/main/java/com/park/utmstack/web/rest/UtmStackResource.java b/backend/src/main/java/com/park/utmstack/web/rest/UtmStackResource.java index 4c3f03825..c13feb28e 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/UtmStackResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/UtmStackResource.java @@ -2,15 +2,12 @@ import com.park.utmstack.config.Constants; -import com.park.utmstack.domain.UtmConfigurationParameter; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; -import com.park.utmstack.domain.mail_sender.MailConfig; import com.park.utmstack.service.UtmConfigurationParameterService; import com.park.utmstack.service.UtmStackService; import com.park.utmstack.service.application_events.ApplicationEventService; -import com.park.utmstack.service.mail_config.MailConfigService; import com.park.utmstack.util.CipherUtil; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.actuate.info.InfoEndpoint; @@ -19,9 +16,6 @@ import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; -import javax.mail.MessagingException; -import javax.validation.Valid; -import java.util.List; import java.util.Map; /** @@ -64,7 +58,7 @@ public ResponseEntity> dateFormat() { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -78,7 +72,7 @@ public ResponseEntity healthCheck() { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -91,7 +85,7 @@ public ResponseEntity isInDevelop() { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -106,7 +100,7 @@ public ResponseEntity encrypt(@RequestBody String str) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/agent_manager/AgentManagerResource.java b/backend/src/main/java/com/park/utmstack/web/rest/agent_manager/AgentManagerResource.java index 42ef11832..c0c28f94d 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/agent_manager/AgentManagerResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/agent_manager/AgentManagerResource.java @@ -6,7 +6,7 @@ import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.dto.agent_manager.*; import com.park.utmstack.service.incident_response.UtmIncidentVariableService; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.application_modules.UtmModuleResource; import com.park.utmstack.web.rest.util.HeaderUtil; import com.park.utmstack.web.rest.vm.AgentRequestVM; @@ -64,7 +64,7 @@ public ResponseEntity> listAgents( String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -88,7 +88,7 @@ public ResponseEntity> listAgentsWithCommands() { String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -105,7 +105,7 @@ public ResponseEntity getAgentByHostname( String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -140,7 +140,7 @@ public ResponseEntity> listAgentCommands( String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/alert_response_rule/UtmAlertResponseRuleExecutionResource.java b/backend/src/main/java/com/park/utmstack/web/rest/alert_response_rule/UtmAlertResponseRuleExecutionResource.java index 23b8207ec..ebf85b2f4 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/alert_response_rule/UtmAlertResponseRuleExecutionResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/alert_response_rule/UtmAlertResponseRuleExecutionResource.java @@ -5,7 +5,7 @@ import com.park.utmstack.service.alert_response_rule.UtmAlertResponseRuleExecutionQueryService; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.dto.UtmAlertResponseRuleExecutionCriteria; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.util.PaginationUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,7 +46,7 @@ public ResponseEntity> getAllAlertResponseRu String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildInternalServerErrorResponse(msg); + return ResponseUtil.buildInternalServerErrorResponse(msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/alert_response_rule/UtmAlertResponseRuleResource.java b/backend/src/main/java/com/park/utmstack/web/rest/alert_response_rule/UtmAlertResponseRuleResource.java index a2a69bb67..5c6939c04 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/alert_response_rule/UtmAlertResponseRuleResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/alert_response_rule/UtmAlertResponseRuleResource.java @@ -3,6 +3,7 @@ import com.park.utmstack.domain.alert_response_rule.UtmAlertResponseActionTemplate; import com.park.utmstack.domain.alert_response_rule.UtmAlertResponseRule; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; +import com.park.utmstack.service.UtmStackService; import com.park.utmstack.service.alert_response_rule.UtmAlertResponseActionTemplateQueryService; import com.park.utmstack.service.alert_response_rule.UtmAlertResponseRuleQueryService; import com.park.utmstack.service.alert_response_rule.UtmAlertResponseRuleService; @@ -11,8 +12,9 @@ import com.park.utmstack.service.dto.UtmAlertResponseActionTemplateDTO; import com.park.utmstack.service.dto.UtmAlertResponseRuleCriteria; import com.park.utmstack.service.dto.UtmAlertResponseRuleDTO; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.util.PaginationUtil; +import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springdoc.api.annotations.ParameterObject; @@ -30,6 +32,7 @@ @RestController @RequestMapping("/api") +@RequiredArgsConstructor public class UtmAlertResponseRuleResource { private static final String CLASSNAME = "UtmAlertResponseRuleResource"; @@ -38,17 +41,11 @@ public class UtmAlertResponseRuleResource { private final UtmAlertResponseRuleService alertResponseRuleService; private final UtmAlertResponseRuleQueryService alertResponseRuleQueryService; private final ApplicationEventService eventService; + private final UtmStackService utmStackService; private final UtmAlertResponseActionTemplateQueryService utmAlertResponseActionTemplateQueryService; - public UtmAlertResponseRuleResource(UtmAlertResponseRuleService alertResponseRuleService, - UtmAlertResponseRuleQueryService alertResponseRuleQueryService, - ApplicationEventService eventService, UtmAlertResponseActionTemplateQueryService utmAlertResponseActionTemplateQueryService) { - this.alertResponseRuleService = alertResponseRuleService; - this.alertResponseRuleQueryService = alertResponseRuleQueryService; - this.eventService = eventService; - this.utmAlertResponseActionTemplateQueryService = utmAlertResponseActionTemplateQueryService; - } + @PostMapping("/utm-alert-response-rules") public ResponseEntity createAlertResponseRule(@Valid @RequestBody UtmAlertResponseRuleDTO dto) { @@ -58,14 +55,22 @@ public ResponseEntity createAlertResponseRule(@Valid @R String msg = ctx + ": A new rule cannot already have an ID"; log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } - return ResponseEntity.ok(new UtmAlertResponseRuleDTO(alertResponseRuleService.save(new UtmAlertResponseRule(dto)))); + + if (utmStackService.isInDevelop()) { + dto.setId(alertResponseRuleService.getSystemSequenceNextValue()); + dto.setSystemOwner(true); + } else { + dto.setSystemOwner(false); + } + + return ResponseEntity.ok(new UtmAlertResponseRuleDTO(alertResponseRuleService.save(new UtmAlertResponseRule(dto), true))); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -77,14 +82,14 @@ public ResponseEntity updateAlertResponseRule(@Valid @R String msg = ctx + ": The rule you are trying to update does not have a valid ID"; log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } - return ResponseEntity.ok(new UtmAlertResponseRuleDTO(alertResponseRuleService.save(new UtmAlertResponseRule(dto)))); + return ResponseEntity.ok(new UtmAlertResponseRuleDTO(alertResponseRuleService.save(new UtmAlertResponseRule(dto), false))); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -100,7 +105,7 @@ public ResponseEntity> getAllAlertResponseRules(@P String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -117,7 +122,7 @@ public ResponseEntity> getAllAlertRespon String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -130,7 +135,7 @@ public ResponseEntity getAlertResponseRule(@PathVariabl String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -143,7 +148,7 @@ public ResponseEntity>> resolveFilterValues() { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java new file mode 100644 index 000000000..aa6b2052a --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/web/rest/api_key/ApiKeyResource.java @@ -0,0 +1,184 @@ +package com.park.utmstack.web.rest.api_key; + + +import com.park.utmstack.domain.chart_builder.types.query.FilterType; +import com.park.utmstack.security.AuthoritiesConstants; +import com.park.utmstack.service.UserService; +import com.park.utmstack.service.api_key.ApiKeyService; +import com.park.utmstack.service.dto.api_key.ApiKeyResponseDTO; +import com.park.utmstack.service.dto.api_key.ApiKeyUpsertDTO; +import com.park.utmstack.service.elasticsearch.ElasticsearchService; +import com.park.utmstack.util.UtilPagination; +import com.park.utmstack.web.rest.elasticsearch.ElasticsearchResource; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.opensearch.client.opensearch.core.SearchResponse; +import org.opensearch.client.opensearch.core.search.Hit; +import org.opensearch.client.opensearch.core.search.HitsMetadata; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import tech.jhipster.web.util.PaginationUtil; + +import java.util.*; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/api-keys") +@PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.USER + "\")") +@Slf4j +@AllArgsConstructor +@Hidden +public class ApiKeyResource { + + private final ApiKeyService apiKeyService; + private final ElasticsearchService elasticsearchService; + private final UserService userService; + + @Operation(summary = "Create API key", + description = "Creates a new API key record using the provided settings. The plain text key is not generated at creation.") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "API key created successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "409", description = "API key already exists", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PostMapping + public ResponseEntity createApiKey(@RequestBody ApiKeyUpsertDTO dto) { + Long userId = userService.getCurrentUserLogin().getId(); + ApiKeyResponseDTO responseDTO = apiKeyService.createApiKey(userId, dto); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO); + } + + @Operation(summary = "Generate a new API key", + description = "Generates (or renews) a new random API key for the specified API key record. The plain text key is returned only once.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key generated successfully", + content = @Content(schema = @Schema(type = "string"))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PostMapping("/{id}/generate") + public ResponseEntity generateApiKey(@PathVariable("id") Long apiKeyId) { + Long userId = userService.getCurrentUserLogin().getId(); + String plainKey = apiKeyService.generateApiKey(userId, apiKeyId); + return ResponseEntity.ok(plainKey); + } + + @Operation(summary = "Retrieve API key", + description = "Retrieves the API key details for the specified API key record.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key retrieved successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @GetMapping("/{id}") + public ResponseEntity getApiKey(@PathVariable("id") Long apiKeyId) { + Long userId = userService.getCurrentUserLogin().getId(); + ApiKeyResponseDTO responseDTO = apiKeyService.getApiKey(userId, apiKeyId); + return ResponseEntity.ok(responseDTO); + } + + @Operation(summary = "List API keys", + description = "Retrieves the API key list.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key retrieved successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @GetMapping + public ResponseEntity> listApiKeys(@ParameterObject Pageable pageable) { + Long userId = userService.getCurrentUserLogin().getId(); + Page page = apiKeyService.listApiKeys(userId,pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } + + @Operation(summary = "Update API key", + description = "Updates mutable fields (name, allowed IPs, expiration) for the specified API key record.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "API key updated successfully", + content = @Content(schema = @Schema(implementation = ApiKeyResponseDTO.class))), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @PutMapping("/{id}") + public ResponseEntity updateApiKey(@PathVariable("id") Long apiKeyId, + @RequestBody ApiKeyUpsertDTO dto) { + + Long userId = userService.getCurrentUserLogin().getId(); + ApiKeyResponseDTO responseDTO = apiKeyService.updateApiKey(userId, apiKeyId, dto); + return ResponseEntity.ok(responseDTO); + + } + + @Operation(summary = "Delete API key", + description = "Deletes the specified API key record for the authenticated user.") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "API key deleted successfully", content = @Content), + @ApiResponse(responseCode = "404", description = "API key not found", content = @Content), + @ApiResponse(responseCode = "500", description = "Internal server error", + content = @Content, headers = { + @Header(name = "X-App-Error", description = "Technical error details") + }) + }) + @DeleteMapping("/{id}") + public ResponseEntity deleteApiKey(@PathVariable("id") Long apiKeyId) { + + Long userId = userService.getCurrentUserLogin().getId(); + apiKeyService.deleteApiKey(userId, apiKeyId); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/usage") + public ResponseEntity> search(@RequestBody(required = false) List filters, + @RequestParam Integer top, @RequestParam String indexPattern, + @RequestParam(required = false, defaultValue = "false") boolean includeChildren, + Pageable pageable) { + + SearchResponse searchResponse = elasticsearchService.search(filters, top, indexPattern, + pageable, Map.class); + + if (Objects.isNull(searchResponse) || Objects.isNull(searchResponse.hits()) || searchResponse.hits().total().value() == 0) + return ResponseEntity.ok(Collections.emptyList()); + + HitsMetadata hits = searchResponse.hits(); + HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), + pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); + + return ResponseEntity.ok().headers(headers).body(hits.hits().stream() + .map(Hit::source).collect(Collectors.toList())); + + } +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleGroupConfigurationResource.java b/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleGroupConfigurationResource.java index a1f7772ed..8a7026bbd 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleGroupConfigurationResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleGroupConfigurationResource.java @@ -1,5 +1,6 @@ package com.park.utmstack.web.rest.application_modules; +import com.park.utmstack.aop.logging.AuditEvent; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.application_modules.UtmModuleGroupConfiguration; import com.park.utmstack.service.application_events.ApplicationEventService; @@ -29,6 +30,12 @@ public class UtmModuleGroupConfigurationResource { @PutMapping("/module-group-configurations/update") + @AuditEvent( + attemptType = ApplicationEventType.CONFIG_UPDATE_ATTEMPT, + attemptMessage = "Attempt to update configuration keys initiated", + successType = ApplicationEventType.CONFIG_UPDATE_SUCCESS, + successMessage = "Configuration keys updated successfully" + ) public ResponseEntity updateConfiguration(@Valid @RequestBody GroupConfigurationDTO body) { final String ctx = CLASSNAME + ".updateConfiguration"; try { diff --git a/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleGroupResource.java b/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleGroupResource.java index 3755de8d0..9f10dbe3c 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleGroupResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleGroupResource.java @@ -1,5 +1,6 @@ package com.park.utmstack.web.rest.application_modules; +import com.park.utmstack.aop.logging.AuditEvent; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.application_modules.UtmModule; import com.park.utmstack.domain.application_modules.UtmModuleGroup; @@ -58,6 +59,12 @@ public UtmModuleGroupResource(UtmModuleGroupService moduleGroupService, } @PostMapping("/utm-configuration-groups") + @AuditEvent( + attemptType = ApplicationEventType.CONFIG_GROUP_CREATE_ATTEMPT, + attemptMessage = "Attempt to create configuration group initiated", + successType = ApplicationEventType.CONFIG_GROUP_CREATE_SUCCESS, + successMessage = "Configuration group created successfully" + ) public ResponseEntity createConfigurationGroup(@Valid @RequestBody ModuleGroupVM moduleGroupVM) throws URISyntaxException { final String ctx = CLASSNAME + ".createConfigurationGroup"; try { @@ -105,26 +112,19 @@ public ResponseEntity createConfigurationGroup(@Valid @RequestBo * @throws URISyntaxException if the Location URI syntax is incorrect */ @PutMapping("/utm-configuration-groups") - public ResponseEntity updateUtmConfigurationGroup(@Valid @RequestBody UtmModuleGroup utmModuleGroup) throws URISyntaxException { + @AuditEvent( + attemptType = ApplicationEventType.CONFIG_GROUP_UPDATE_ATTEMPT, + attemptMessage = "Attempt to update configuration group initiated", + successType = ApplicationEventType.CONFIG_GROUP_UPDATE_SUCCESS, + successMessage = "Configuration group updated successfully" + ) + public ResponseEntity updateUtmConfigurationGroup(@Valid @RequestBody UtmModuleGroup utmModuleGroup) { final String ctx = CLASSNAME + ".updateUtmConfigurationGroup"; - try { - if (utmModuleGroup.getId() == null) - throw new Exception("Can't update the configuration group because ID is null"); - UtmModuleGroup result = moduleGroupService.save(utmModuleGroup); - return ResponseEntity.ok(result); - } catch (DataIntegrityViolationException e) { - String msg = ctx + ": " + e.getMostSpecificCause().getMessage().replaceAll("\n", ""); - log.error(msg); - eventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert("", "", msg)).body(null); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - eventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert("", "", msg)).body(null); - } + + if (utmModuleGroup.getId() == null) + throw new RuntimeException("Can't update the configuration group because ID is null"); + UtmModuleGroup result = moduleGroupService.save(utmModuleGroup); + return ResponseEntity.ok(result); } /** @@ -149,45 +149,39 @@ public ResponseEntity> getModuleGroups(@RequestParam Long m @GetMapping("/utm-configuration-groups/{groupId}") public ResponseEntity getConfigurationGroup(@PathVariable Long groupId) { final String ctx = CLASSNAME + ".getConfigurationGroups"; - try { - Optional group = moduleGroupService.findOne(groupId); - return ResponseUtil.wrapOrNotFound(group); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - eventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert("", "", msg)).body(null); - } + + Optional group = moduleGroupService.findOne(groupId); + return ResponseUtil.wrapOrNotFound(group); + } @DeleteMapping("/utm-configuration-groups/delete-single-module-group") + @AuditEvent( + attemptType = ApplicationEventType.CONFIG_GROUP_DELETE_ATTEMPT, + attemptMessage = "Attempt to delete single configuration group initiated", + successType = ApplicationEventType.CONFIG_GROUP_DELETE_SUCCESS, + successMessage = "Configuration group deleted successfully" + ) public ResponseEntity deleteSingleModuleGroup(@RequestParam Long groupId) { final String ctx = CLASSNAME + ".deleteSingleModuleGroup"; - try { - moduleGroupService.delete(groupId); - return ResponseEntity.ok().build(); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - eventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert("", "", msg)).body(null); - } + + moduleGroupService.delete(groupId); + return ResponseEntity.ok().build(); + } @DeleteMapping("/utm-configuration-groups/delete-all-module-groups") + @AuditEvent( + attemptType = ApplicationEventType.CONFIG_GROUP_BULK_DELETE_ATTEMPT, + attemptMessage = "Attempt to delete all configuration groups for module initiated", + successType = ApplicationEventType.CONFIG_GROUP_BULK_DELETE_SUCCESS, + successMessage = "All configuration groups for module deleted successfully" + ) public ResponseEntity deleteAllModuleGroups(@RequestParam Long moduleId) { final String ctx = CLASSNAME + ".deleteAllModuleGroups"; - try { - moduleGroupService.deleteAllByModuleId(moduleId); - return ResponseEntity.ok().build(); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - eventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert("", "", msg)).body(null); - } + + moduleGroupService.deleteAllByModuleId(moduleId); + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleResource.java b/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleResource.java index 868d18800..6870a60c3 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/application_modules/UtmModuleResource.java @@ -1,5 +1,6 @@ package com.park.utmstack.web.rest.application_modules; +import com.park.utmstack.aop.logging.AuditEvent; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.application_modules.UtmModule; import com.park.utmstack.domain.application_modules.enums.ModuleName; @@ -12,11 +13,15 @@ import com.park.utmstack.service.application_modules.UtmModuleQueryService; import com.park.utmstack.service.application_modules.UtmModuleService; import com.park.utmstack.event_processor.EventProcessorManagerService; +import com.park.utmstack.service.dto.application_modules.CheckRequirementsResponse; +import com.park.utmstack.service.dto.application_modules.ModuleActivationDTO; import com.park.utmstack.service.dto.application_modules.ModuleDTO; import com.park.utmstack.service.dto.application_modules.UtmModuleCriteria; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.util.PaginationUtil; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -48,19 +53,21 @@ public class UtmModuleResource { + @AuditEvent( + attemptType = ApplicationEventType.MODULE_ACTIVATION_ATTEMPT, + attemptMessage = "Attempt to activate/deactivate module initiated", + successType = ApplicationEventType.MODULE_ACTIVATION_SUCCESS, + successMessage = "Module activated/deactivated successfully" + ) @PutMapping("/utm-modules/activateDeactivate") public ResponseEntity activateDeactivate(@RequestParam Long serverId, @RequestParam ModuleName nameShort, @RequestParam Boolean activationStatus) { - final String ctx = CLASSNAME + ".activateDeactivate"; - try { - return ResponseEntity.ok(moduleService.activateDeactivate(serverId, nameShort, activationStatus)); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); - } + return ResponseEntity.ok(moduleService.activateDeactivate(ModuleActivationDTO.builder() + .serverId(serverId) + .moduleName(nameShort) + .activationStatus(activationStatus) + .build())); } /** @@ -81,7 +88,7 @@ public ResponseEntity> getAllUtmModules(UtmModuleCriteria criter String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -94,7 +101,7 @@ public ResponseEntity getModuleById(@PathVariable Long id) { String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -108,7 +115,7 @@ public ResponseEntity getModuleDetails(@RequestParam Long serverId, String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -123,7 +130,7 @@ public ResponseEntity getModuleDetailsDecrypted(@RequestParam ModuleN String msg = ctx + ": You must provide the header used to communicate internally with this resource"; log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } return ResponseEntity.ok(module); @@ -131,7 +138,7 @@ public ResponseEntity getModuleDetailsDecrypted(@RequestParam ModuleN String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -158,7 +165,7 @@ public ResponseEntity checkRequirements(@RequestParam String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -171,7 +178,7 @@ public ResponseEntity> getModuleCategories(@RequestParam(required = String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -184,28 +191,7 @@ public ResponseEntity isActive(@RequestParam ModuleName moduleName) { String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); - } - } - - public static class CheckRequirementsResponse { - private ModuleRequirementStatus status; - private List checks; - - public ModuleRequirementStatus getStatus() { - return status; - } - - public void setStatus(ModuleRequirementStatus status) { - this.status = status; - } - - public List getChecks() { - return checks; - } - - public void setChecks(List checks) { - this.checks = checks; + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/chart_builder/UtmVisualizationResource.java b/backend/src/main/java/com/park/utmstack/web/rest/chart_builder/UtmVisualizationResource.java index 100bbd705..d34e067ef 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/chart_builder/UtmVisualizationResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/chart_builder/UtmVisualizationResource.java @@ -10,7 +10,7 @@ import com.park.utmstack.service.chart_builder.UtmVisualizationService; import com.park.utmstack.service.dto.chart_builder.UtmVisualizationCriteria; import com.park.utmstack.service.elasticsearch.ElasticsearchService; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.util.chart_builder.elasticsearch_dsl.requests.RequestDsl; import com.park.utmstack.util.chart_builder.elasticsearch_dsl.responses.ResponseParser; import com.park.utmstack.util.chart_builder.elasticsearch_dsl.responses.ResponseParserFactory; @@ -31,7 +31,6 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; -import tech.jhipster.web.util.ResponseUtil; import javax.validation.Valid; import java.net.URI; @@ -238,7 +237,7 @@ public ResponseEntity getUtmVisualization(@PathVariable Long i final String ctx = CLASSNAME + ".getUtmVisualization"; try { Optional utmVisualization = visualizationService.findOne(id); - return ResponseUtil.wrapOrNotFound(utmVisualization); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(utmVisualization); } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); @@ -306,7 +305,7 @@ public ResponseEntity> run(@RequestBody UtmVisualization visualization, String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/collectors/UtmCollectorResource.java b/backend/src/main/java/com/park/utmstack/web/rest/collectors/UtmCollectorResource.java index 84b2f3d0c..fd962beae 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/collectors/UtmCollectorResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/collectors/UtmCollectorResource.java @@ -19,7 +19,7 @@ import com.park.utmstack.service.dto.collectors.dto.ErrorResponse; import com.park.utmstack.service.dto.collectors.dto.ListCollectorsResponseDTO; import com.park.utmstack.service.dto.network_scan.AssetGroupDTO; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.errors.BadRequestAlertException; import com.park.utmstack.web.rest.errors.InternalServerErrorException; import com.park.utmstack.web.rest.network_scan.UtmNetworkScanResource; @@ -35,7 +35,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; @@ -143,12 +142,12 @@ public ResponseEntity listCollectorsByModule(@Request String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (CollectorServiceGrpcException e) { String msg = ctx + ": UtmCollector manager is not available or was an error getting the collector list. " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_GATEWAY, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_GATEWAY, msg); } } @@ -180,12 +179,12 @@ public ResponseEntity listCollectorHostNames(@RequestParam(r String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (CollectorServiceGrpcException e) { String msg = ctx + ": UtmCollector manager is not available or the parameters are wrong, please check." + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_GATEWAY, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_GATEWAY, msg); } } @@ -208,12 +207,12 @@ public ResponseEntity listCollectorByHostNameAndModul String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (CollectorServiceGrpcException e) { String msg = ctx + ": UtmCollector manager is not available or was an error getting configuration. " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_GATEWAY, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_GATEWAY, msg); } } @@ -271,7 +270,6 @@ public ResponseEntity> searchByFilters(@ParameterObject Netwo collectorService.listCollector(ListRequest.newBuilder() .setPageNumber(0) .setPageSize(1000000) - .setSearchQuery("module.Is=" + CollectorModuleEnum.AS_400) .setSortBy("") .build()); Page page = this.utmCollectorService.searchByFilters(filters, pageable); @@ -364,7 +362,7 @@ private ErrorResponse getError(Exception e, CollectorConfig cacheConfig) { private ResponseEntity logAndResponse(ErrorResponse error) { log.error(error.getMessage()); applicationEventService.createEvent(error.getMessage(), ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(error.getStatus(), error.getMessage()); + return ResponseUtil.buildErrorResponse(error.getStatus(), error.getMessage()); } private void upsert(CollectorConfigKeysDTO collectorConfig) throws Exception { diff --git a/backend/src/main/java/com/park/utmstack/web/rest/compliance/UtmComplianceReportScheduleResource.java b/backend/src/main/java/com/park/utmstack/web/rest/compliance/UtmComplianceReportScheduleResource.java index 8f2eeee7d..c67e66573 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/compliance/UtmComplianceReportScheduleResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/compliance/UtmComplianceReportScheduleResource.java @@ -6,7 +6,7 @@ import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.compliance.UtmComplianceReportScheduleService; import com.park.utmstack.service.dto.compliance.UtmComplianceReportScheduleCriteria; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.errors.BadRequestAlertException; import java.net.URISyntaxException; @@ -23,7 +23,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; -import tech.jhipster.web.util.ResponseUtil; import javax.validation.Valid; @@ -82,7 +81,7 @@ public ResponseEntity createUtmComplianceReportSche String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -119,7 +118,7 @@ public ResponseEntity updateUtmComplianceReportSche String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -140,7 +139,7 @@ public ResponseEntity> getAllUtmComplianceRepo String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -155,12 +154,12 @@ public ResponseEntity getUtmComplianceReportSchedul final String ctx = CLASSNAME + ".getUtmComplianceReportScheduleById"; try { log.debug("REST request to get a UtmComplianceReportSchedule by id"); - return ResponseUtil.wrapOrNotFound(utmComplianceReportScheduleService.findOne(id)); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(utmComplianceReportScheduleService.findOne(id)); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -181,7 +180,7 @@ public ResponseEntity deleteUtmComplianceReportSchedule(@PathVariable Long String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/compliance/config/UtmComplianceReportConfigResource.java b/backend/src/main/java/com/park/utmstack/web/rest/compliance/config/UtmComplianceReportConfigResource.java index 6c0de0b6a..acf3b2e63 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/compliance/config/UtmComplianceReportConfigResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/compliance/config/UtmComplianceReportConfigResource.java @@ -10,7 +10,7 @@ import com.park.utmstack.service.compliance.config.UtmComplianceStandardSectionService; import com.park.utmstack.service.compliance.config.UtmComplianceStandardService; import com.park.utmstack.service.dto.compliance.UtmComplianceReportConfigCriteria; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.util.HeaderUtil; import com.park.utmstack.web.rest.util.PaginationUtil; import org.slf4j.Logger; @@ -21,7 +21,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import tech.jhipster.web.util.ResponseUtil; import javax.validation.Valid; import javax.validation.constraints.NotNull; @@ -143,7 +142,7 @@ public ResponseEntity getComplianceReportConfig(@Path dashboardVisualizationService.findAllByIdDashboard(report.getDashboardId()).ifPresent(report::setDashboard); return ResponseEntity.ok(report); } - return ResponseUtil.wrapOrNotFound(standard); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(standard); } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); @@ -201,7 +200,7 @@ public ResponseEntity importReports(@Valid @RequestBody ImportReportsBody String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmDataTypesResource.java b/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmDataTypesResource.java index 578fcf5cb..02ad3c5f8 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmDataTypesResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmDataTypesResource.java @@ -4,8 +4,7 @@ import com.park.utmstack.domain.correlation.config.UtmDataTypes; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.correlation.config.UtmDataTypesService; -import com.park.utmstack.util.UtilResponse; -import com.park.utmstack.web.rest.errors.BadRequestAlertException; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.util.HeaderUtil; import com.park.utmstack.web.rest.util.PaginationUtil; import io.undertow.util.BadRequestException; @@ -17,7 +16,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import tech.jhipster.web.util.ResponseUtil; import javax.validation.Valid; import java.util.List; @@ -55,12 +53,12 @@ public ResponseEntity addDataType(@Valid @RequestBody UtmDataTypes dataTyp String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -80,12 +78,12 @@ public ResponseEntity updateDataTypes(@Valid @RequestBody UtmDataTypes dat String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -102,7 +100,7 @@ public ResponseEntity updateDataTypesList(@Valid @RequestBody List removeDataTypes(@PathVariable Long id) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -163,6 +161,6 @@ public ResponseEntity> getAllDataTypes(@RequestParam(required public ResponseEntity getDataType(@PathVariable Long id) { log.debug("REST request to get UtmDataTypes : {}", id); Optional datatype = dataTypesService.findOne(id); - return ResponseUtil.wrapOrNotFound(datatype); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(datatype); } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmRegexPatternResource.java b/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmRegexPatternResource.java index 38cbf1bdb..5528f8699 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmRegexPatternResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmRegexPatternResource.java @@ -4,7 +4,7 @@ import com.park.utmstack.domain.correlation.config.UtmRegexPattern; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.correlation.config.UtmRegexPatternService; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.util.HeaderUtil; import com.park.utmstack.web.rest.util.PaginationUtil; import io.undertow.util.BadRequestException; @@ -16,7 +16,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import tech.jhipster.web.util.ResponseUtil; import javax.validation.Valid; import java.util.List; @@ -54,12 +53,12 @@ public ResponseEntity addRegexPattern(@Valid @RequestBody UtmRegexPattern String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -79,12 +78,12 @@ public ResponseEntity updateRegexPattern(@Valid @RequestBody UtmRegexPatte String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -104,12 +103,12 @@ public ResponseEntity removeRegexPattern(@PathVariable Long id) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -145,6 +144,6 @@ public ResponseEntity> getAllRegexPatterns(@RequestParam(r public ResponseEntity getRegexPattern(@PathVariable Long id) { log.debug("REST request to get UtmRegexPattern : {}", id); Optional pattern = regexPatternService.findOne(id); - return ResponseUtil.wrapOrNotFound(pattern); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(pattern); } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmTenantConfigResource.java b/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmTenantConfigResource.java index a59cee9bc..4ea70a96c 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmTenantConfigResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/correlation/config/UtmTenantConfigResource.java @@ -4,7 +4,7 @@ import com.park.utmstack.domain.correlation.config.UtmTenantConfig; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.correlation.config.UtmTenantConfigService; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.util.HeaderUtil; import com.park.utmstack.web.rest.util.PaginationUtil; import io.undertow.util.BadRequestException; @@ -16,7 +16,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import tech.jhipster.web.util.ResponseUtil; import javax.validation.Valid; import java.util.List; @@ -54,12 +53,12 @@ public ResponseEntity addTenantConfig(@Valid @RequestBody UtmTenantConfig String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -79,12 +78,12 @@ public ResponseEntity updateTenantConfig(@Valid @RequestBody UtmTenantConf String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -104,12 +103,12 @@ public ResponseEntity removeTenantConfig(@PathVariable Long id) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -145,6 +144,6 @@ public ResponseEntity> getAllTenantConfig(@RequestParam(re public ResponseEntity getTenantConfig(@PathVariable Long id) { log.debug("REST request to get UtmTenantConfig : {}", id); Optional tenantConfig = tenantConfigService.findOne(id); - return ResponseUtil.wrapOrNotFound(tenantConfig); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(tenantConfig); } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/correlation/rules/UtmCorrelationRulesResource.java b/backend/src/main/java/com/park/utmstack/web/rest/correlation/rules/UtmCorrelationRulesResource.java index 19a73ed7b..63e313c67 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/correlation/rules/UtmCorrelationRulesResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/correlation/rules/UtmCorrelationRulesResource.java @@ -10,7 +10,7 @@ import com.park.utmstack.service.dto.correlation.UtmCorrelationRulesDTO; import com.park.utmstack.service.dto.correlation.UtmCorrelationRulesMapper; import com.park.utmstack.service.dto.correlation.validators.CorrelationRuleValidator; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.errors.BadRequestAlertException; import com.park.utmstack.web.rest.util.HeaderUtil; import com.park.utmstack.web.rest.util.PaginationUtil; @@ -24,9 +24,7 @@ import org.springframework.http.HttpStatus; import org.springframework.web.bind.WebDataBinder; import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; -import tech.jhipster.web.util.ResponseUtil; import javax.persistence.EntityNotFoundException; import javax.validation.Valid; @@ -93,12 +91,12 @@ public ResponseEntity addCorrelationRule(@Valid @RequestBody UtmCorrelatio String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -119,12 +117,12 @@ public ResponseEntity activateOrDeactivateCorrelationRule(@RequestParam Lo String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -147,17 +145,17 @@ public ResponseEntity updateCorrelationRule(@Valid @RequestBody UtmCorrela String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (EntityNotFoundException e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.NOT_FOUND, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.NOT_FOUND, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -207,9 +205,9 @@ public ResponseEntity getRule(@PathVariable Long id) { Optional utmCorrelationRule = rulesService.findOne(id); if (utmCorrelationRule.isPresent()) { UtmCorrelationRulesDTO dto = utmCorrelationRulesMapper.toDto(utmCorrelationRule.get()); - return ResponseUtil.wrapOrNotFound(Optional.of(dto)); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(Optional.of(dto)); } else { - return ResponseUtil.wrapOrNotFound(Optional.empty()); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(Optional.empty()); } } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); @@ -236,12 +234,12 @@ public ResponseEntity removeCorrelationRule(@PathVariable Long id) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/elasticsearch/ElasticsearchResource.java b/backend/src/main/java/com/park/utmstack/web/rest/elasticsearch/ElasticsearchResource.java index 39edd2f57..646e3adf0 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/elasticsearch/ElasticsearchResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/elasticsearch/ElasticsearchResource.java @@ -2,6 +2,7 @@ import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.chart_builder.types.query.FilterType; +import com.park.utmstack.domain.chart_builder.types.query.OperatorType; import com.park.utmstack.domain.shared_types.CsvExportingParams; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.elasticsearch.ElasticsearchService; @@ -9,7 +10,7 @@ import com.park.utmstack.service.elasticsearch.processor.SearchResultProcessor; import com.park.utmstack.util.UtilCsv; import com.park.utmstack.util.UtilPagination; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.util.chart_builder.IndexPropertyType; import com.park.utmstack.util.chart_builder.IndexType; import com.park.utmstack.util.exceptions.OpenSearchIndexNotFoundException; @@ -64,7 +65,7 @@ public ResponseEntity> getFieldValues(@RequestParam String keyword, String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -78,7 +79,7 @@ public ResponseEntity> getFieldValuesWithCount(@Valid @Request String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -97,12 +98,12 @@ public ResponseEntity> getIndexProperties(@RequestParam String msg = ctx + ": " + e.getMessage(); log.info(msg); applicationEventService.createEvent(msg, ApplicationEventType.INFO); - return UtilResponse.buildNotFoundResponse(msg); + return ResponseUtil.buildNotFoundResponse(msg); } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildInternalServerErrorResponse(msg); + return ResponseUtil.buildInternalServerErrorResponse(msg); } } @@ -118,7 +119,7 @@ public ResponseEntity> getAllIndexes(@RequestParam(defaultValue String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -144,9 +145,9 @@ public ResponseEntity deleteIndex(@RequestBody List indexes) { } @PostMapping("/search") - public ResponseEntity>> search(@RequestBody(required = false) List filters, + public ResponseEntity> search(@RequestBody(required = false) List filters, @RequestParam Integer top, @RequestParam String indexPattern, - @RequestParam(required = false) String groupByField, + @RequestParam(required = false, defaultValue = "false") boolean includeChildren, Pageable pageable) { final String ctx = CLASSNAME + ".search"; try { @@ -160,20 +161,30 @@ public ResponseEntity>> search(@RequestBody(required = HttpHeaders headers = UtilPagination.generatePaginationHttpHeaders(Math.min(hits.total().value(), top), pageable.getPageNumber(), pageable.getPageSize(), "/api/elasticsearch/search"); - - List> flatResults = hits.hits().stream() - .map(hit -> (Map)hit.source()) - .collect(Collectors.toList()); - - SearchResultProcessor processor = searchProcessorRegistry.resolve(groupByField); - List> processed = processor.process(flatResults); - - return ResponseEntity.ok().headers(headers).body(processed); + List results = hits.hits().stream() + .map(Hit::source) + .toList(); + + if (includeChildren) { + results.forEach(d -> { + Object id = d.get("id"); + if (id != null) { + long countEchoes = elasticsearchService.count( + List.of(new FilterType("parentId", OperatorType.IS, id.toString())), + indexPattern + ); + d.put("hasChildren", countEchoes > 0); + d.put("echoes", countEchoes); + } + }); + } + + return ResponseEntity.ok().headers(headers).body(results); } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -195,7 +206,7 @@ public ResponseEntity searchToCsv(@RequestBody @Valid CsvExportingParams p String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -220,7 +231,7 @@ public ResponseEntity> genericSearch(@Valid @RequestBody GenericSearch String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -234,7 +245,22 @@ public ResponseEntity getClusterStatus() { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + } + } + + @PostMapping("/count") + public ResponseEntity count(@RequestBody(required = false) List filters, + @RequestParam String indexPattern, + Pageable pageable) { + final String ctx = CLASSNAME + ".count"; + try { + return ResponseEntity.ok().body(elasticsearchService.exists(filters, indexPattern)); + } catch (Exception e) { + String msg = ctx + ": " + e.getMessage(); + log.error(msg); + applicationEventService.createEvent(msg, ApplicationEventType.ERROR); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/idp_provider/IdentityProviderConfigResource.java b/backend/src/main/java/com/park/utmstack/web/rest/idp_provider/IdentityProviderConfigResource.java new file mode 100644 index 000000000..d2cb4e456 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/web/rest/idp_provider/IdentityProviderConfigResource.java @@ -0,0 +1,68 @@ +package com.park.utmstack.web.rest.idp_provider; + + +import com.park.utmstack.service.dto.idp_provider.dto.IdentityProviderConfigRequestDto; +import com.park.utmstack.service.dto.idp_provider.dto.IdentityProviderConfigResponseDto; +import com.park.utmstack.service.dto.idp_provider.dto.IdentityProviderCreateConfigDto; +import com.park.utmstack.service.dto.idp_provider.dto.IdentityProviderCriteria; +import com.park.utmstack.service.idp_provider.IdentityProviderService; +import com.park.utmstack.web.rest.util.PaginationUtil; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.net.URI; +import java.util.List; +import java.util.Optional; + +@RestController +@RequestMapping("/api/identity-providers") +@RequiredArgsConstructor +@Hidden +public class IdentityProviderConfigResource { + + private final IdentityProviderService service; + + @PostMapping + public ResponseEntity create(@RequestBody @Valid IdentityProviderCreateConfigDto dto) { + IdentityProviderConfigResponseDto result = service.create(dto); + return ResponseEntity + .created(URI.create("/api/identity-providers/" + result.getId())) + .body(result); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, + @RequestBody @Valid IdentityProviderConfigRequestDto dto) { + IdentityProviderConfigResponseDto result = service.update(id, dto); + return ResponseEntity.ok(result); + } + + + @GetMapping + public ResponseEntity> getAll(IdentityProviderCriteria criteria, Pageable pageable) { + + Page page = service.findAll(criteria, pageable); + + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/utm-providers"); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable Long id) { + Optional dtoOpt = service.findById(id); + return dtoOpt.map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + service.delete(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/idp_provider/IdentityProviderResource.java b/backend/src/main/java/com/park/utmstack/web/rest/idp_provider/IdentityProviderResource.java new file mode 100644 index 000000000..aaf8aaee7 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/web/rest/idp_provider/IdentityProviderResource.java @@ -0,0 +1,38 @@ +package com.park.utmstack.web.rest.idp_provider; + + +import com.park.utmstack.domain.UtmDataInputStatus; +import com.park.utmstack.service.dto.idp_provider.dto.IdentityProviderConfigResponseDto; +import com.park.utmstack.service.dto.idp_provider.dto.IdentityProviderCriteria; +import com.park.utmstack.service.idp_provider.IdentityProviderService; +import com.park.utmstack.web.rest.util.PaginationUtil; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/utm-providers") +@RequiredArgsConstructor +@Hidden +public class IdentityProviderResource { + + private final IdentityProviderService service; + + + @GetMapping + public ResponseEntity> getAll(IdentityProviderCriteria criteria, Pageable pageable) { + + Page page = service.findAll(criteria, pageable); + + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/utm-providers"); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } + + +} diff --git a/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java b/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java index 470e48fed..f7b02d189 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/incident/UtmIncidentResource.java @@ -2,60 +2,44 @@ import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.incident.UtmIncident; -import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.dto.incident.*; -import com.park.utmstack.service.incident.UtmIncidentAlertService; import com.park.utmstack.service.incident.UtmIncidentQueryService; import com.park.utmstack.service.incident.UtmIncidentService; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.exceptions.NoAlertsProvidedException; import com.park.utmstack.web.rest.errors.BadRequestAlertException; import com.park.utmstack.web.rest.util.HeaderUtil; import com.park.utmstack.web.rest.util.PaginationUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; -import tech.jhipster.web.util.ResponseUtil; +import com.park.utmstack.aop.logging.AuditEvent; import javax.validation.Valid; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; /** * REST controller for managing UtmIncident. */ @RestController +@RequiredArgsConstructor +@Slf4j @RequestMapping("/api") public class UtmIncidentResource { - private final String CLASS_NAME = "UtmIncidentResource"; - private final Logger log = LoggerFactory.getLogger(UtmIncidentResource.class); private static final String ENTITY_NAME = "utmIncident"; private final UtmIncidentService utmIncidentService; - private final UtmIncidentAlertService utmIncidentAlertService; - private final UtmIncidentQueryService utmIncidentQueryService; - private final ApplicationEventService applicationEventService; - - public UtmIncidentResource(UtmIncidentService utmIncidentService, - UtmIncidentAlertService utmIncidentAlertService, - UtmIncidentQueryService utmIncidentQueryService, - ApplicationEventService applicationEventService) { - this.utmIncidentService = utmIncidentService; - this.utmIncidentAlertService = utmIncidentAlertService; - this.utmIncidentQueryService = utmIncidentQueryService; - this.applicationEventService = applicationEventService; - } + /** * Creates a new incident based on the provided details. @@ -75,38 +59,21 @@ public UtmIncidentResource(UtmIncidentService utmIncidentService, * @throws IllegalArgumentException if the input data is invalid. */ @PostMapping("/utm-incidents") + @AuditEvent( + attemptType = ApplicationEventType.INCIDENT_CREATION_ATTEMPT, + attemptMessage = "Attempt to create a new incident initiated", + successType = ApplicationEventType.INCIDENT_CREATION_SUCCESS, + successMessage = "Incident created successfully" + ) public ResponseEntity createUtmIncident(@Valid @RequestBody NewIncidentDTO newIncidentDTO) { final String ctx = ".createUtmIncident"; - try { - if (CollectionUtils.isEmpty(newIncidentDTO.getAlertList())) { - String msg = ctx + ": A new incident has to have at least one alert related"; - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.BAD_REQUEST, msg); - } - - List alertIds = newIncidentDTO.getAlertList().stream() - .map(RelatedIncidentAlertsDTO::getAlertId) - .collect(Collectors.toList()); - - List alertsFound = utmIncidentAlertService.existsAnyAlert(alertIds); - - if (!alertsFound.isEmpty()) { - String alertIdsList = String.join(", ", alertIds); - String msg = "Some alerts are already linked to another incident. Alert IDs: " + alertIdsList + ". Check the related incidents for more details."; - log.error(msg); - applicationEventService.createEvent(ctx + ": " + msg , ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.CONFLICT, utmIncidentAlertService.formatAlertMessage(alertsFound)); - } - - - return ResponseEntity.ok(utmIncidentService.createIncident(newIncidentDTO)); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + + if (CollectionUtils.isEmpty(newIncidentDTO.getAlertList())) { + String msg = ctx + ": A new incident has to have at least one alert related"; + throw new NoAlertsProvidedException(ctx + ": " + msg); } + + return ResponseEntity.ok(utmIncidentService.createIncident(newIncidentDTO)); } /** @@ -124,36 +91,22 @@ public ResponseEntity createUtmIncident(@Valid @RequestBody NewInci * @throws URISyntaxException if the Location URI syntax is incorrect */ @PostMapping("/utm-incidents/add-alerts") + @AuditEvent( + attemptType = ApplicationEventType.INCIDENT_ALERT_ADD_ATTEMPT, + attemptMessage = "Attempt to add alerts to incident initiated", + successType = ApplicationEventType.INCIDENT_ALERT_ADD_SUCCESS, + successMessage = "Alerts added to incident successfully" + ) public ResponseEntity addAlertsToUtmIncident(@Valid @RequestBody AddToIncidentDTO addToIncidentDTO) throws URISyntaxException { - final String ctx = ".addAlertsToUtmIncident"; - try { - log.debug("REST request to save UtmIncident : {}", addToIncidentDTO); - if (CollectionUtils.isEmpty(addToIncidentDTO.getAlertList())) { - throw new BadRequestAlertException("Add utmIncident cannot already have an empty related alerts", ENTITY_NAME, "alertList"); - } - List alertIds = addToIncidentDTO.getAlertList().stream() - .map(RelatedIncidentAlertsDTO::getAlertId) - .collect(Collectors.toList()); - - List alertsFound = utmIncidentAlertService.existsAnyAlert(alertIds); - - if (!alertsFound.isEmpty()) { - String alertIdsList = String.join(", ", alertIds); - String msg = "Some alerts are already linked to another incident. Alert IDs: " + alertIdsList + ". Check the related incidents for more details."; - log.error(msg); - applicationEventService.createEvent(ctx + ": " + msg , ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.CONFLICT, utmIncidentAlertService.formatAlertMessage(alertsFound)); - } - UtmIncident result = utmIncidentService.addAlertsIncident(addToIncidentDTO); - return ResponseEntity.created(new URI("/api/utm-incidents/add-alerts" + result.getId())) + + if (CollectionUtils.isEmpty(addToIncidentDTO.getAlertList())) { + throw new NoAlertsProvidedException("Add utmIncident cannot already have an empty related alerts"); + } + + UtmIncident result = utmIncidentService.addAlertsIncident(addToIncidentDTO); + return ResponseEntity.created(new URI("/api/utm-incidents/add-alerts" + result.getId())) .headers(HeaderUtil.createEntityCreationAlert(ENTITY_NAME, result.getId().toString())) .body(result); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers(HeaderUtil.createFailureAlert(CLASS_NAME, null, msg)).body(null); - } } /** @@ -166,23 +119,22 @@ public ResponseEntity addAlertsToUtmIncident(@Valid @RequestBody Ad * @throws URISyntaxException if the Location URI syntax is incorrect */ @PutMapping("/utm-incidents/change-status") - public ResponseEntity updateUtmIncident(@Valid @RequestBody UtmIncident utmIncident) throws URISyntaxException { - final String ctx = ".updateUtmIncident"; - try { - log.debug("REST request to update UtmIncident : {}", utmIncident); - if (utmIncident.getId() == null) { - throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); - } - UtmIncident result = utmIncidentService.changeStatus(utmIncident); - return ResponseEntity.ok() + @AuditEvent( + attemptType = ApplicationEventType.INCIDENT_UPDATE_ATTEMPT, + attemptMessage = "Attempt to update incident status initiated", + successType = ApplicationEventType.INCIDENT_UPDATE_SUCCESS, + successMessage = "Incident status updated successfully" + ) + public ResponseEntity updateUtmIncident(@Valid @RequestBody UtmIncident utmIncident) { + + if (utmIncident.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + UtmIncident result = utmIncidentService.changeStatus(utmIncident); + + return ResponseEntity.ok() .headers(HeaderUtil.createEntityUpdateAlert(ENTITY_NAME, utmIncident.getId().toString())) .body(result); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers(HeaderUtil.createFailureAlert(CLASS_NAME, null, msg)).body(null); - } } /** @@ -194,18 +146,10 @@ public ResponseEntity updateUtmIncident(@Valid @RequestBody UtmInci */ @GetMapping("/utm-incidents") public ResponseEntity> getAllUtmIncidents(UtmIncidentCriteria criteria, Pageable pageable) { - final String ctx = ".getAllUtmIncidents"; - try { - log.debug("REST request to get UtmIncidents by criteria: {}", criteria); - Page page = utmIncidentQueryService.findByCriteria(criteria, pageable); - HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/utm-incidents"); - return ResponseEntity.ok().headers(headers).body(page.getContent()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers(HeaderUtil.createFailureAlert(CLASS_NAME, null, msg)).body(null); - } + + Page page = utmIncidentQueryService.findByCriteria(criteria, pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/utm-incidents"); + return ResponseEntity.ok().headers(headers).body(page.getContent()); } /** @@ -215,15 +159,7 @@ public ResponseEntity> getAllUtmIncidents(UtmIncidentCriteria */ @GetMapping("/utm-incidents/users-assigned") public ResponseEntity> getAllUserAssigned() { - final String ctx = ".getAllUserAssigned"; - try { - return ResponseEntity.ok().body(utmIncidentQueryService.getAllUsersAssigned()); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers(HeaderUtil.createFailureAlert(CLASS_NAME, null, msg)).body(null); - } + return ResponseEntity.ok().body(utmIncidentQueryService.getAllUsersAssigned()); } /** @@ -234,16 +170,9 @@ public ResponseEntity> getAllUserAssigned() { */ @GetMapping("/utm-incidents/{id}") public ResponseEntity getUtmIncident(@PathVariable Long id) { - final String ctx = ".getUtmIncident"; - try { - log.debug("REST request to get UtmIncident : {}", id); - Optional utmIncident = utmIncidentService.findOne(id); - return ResponseUtil.wrapOrNotFound(utmIncident); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers(HeaderUtil.createFailureAlert(CLASS_NAME, null, msg)).body(null); - } + + Optional utmIncident = utmIncidentService.findOne(id); + return tech.jhipster.web.util.ResponseUtil.wrapOrNotFound(utmIncident); + } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/index_policy/IndexPolicyResource.java b/backend/src/main/java/com/park/utmstack/web/rest/index_policy/IndexPolicyResource.java index 73cba8d3f..06adc0cbf 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/index_policy/IndexPolicyResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/index_policy/IndexPolicyResource.java @@ -5,7 +5,7 @@ import com.park.utmstack.domain.index_policy.*; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.index_policy.IndexPolicyService; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -54,7 +54,7 @@ public ResponseEntity getPolicy() { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -69,7 +69,7 @@ public ResponseEntity updateIndexPolicy(@Valid String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/logstash_filter/UtmLogstashFilterResource.java b/backend/src/main/java/com/park/utmstack/web/rest/logstash_filter/UtmFilterResource.java similarity index 92% rename from backend/src/main/java/com/park/utmstack/web/rest/logstash_filter/UtmLogstashFilterResource.java rename to backend/src/main/java/com/park/utmstack/web/rest/logstash_filter/UtmFilterResource.java index f2d63cb20..b43929aa7 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/logstash_filter/UtmLogstashFilterResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/logstash_filter/UtmFilterResource.java @@ -32,10 +32,10 @@ * REST controller for managing UtmLogstashFilter. */ @RestController -@RequestMapping("/api") -public class UtmLogstashFilterResource { +@RequestMapping("/api/utm-filters") +public class UtmFilterResource { - private final Logger log = LoggerFactory.getLogger(UtmLogstashFilterResource.class); + private final Logger log = LoggerFactory.getLogger(UtmFilterResource.class); private static final String CLASSNAME = "UtmLogstashFilterResource"; @@ -45,9 +45,9 @@ public class UtmLogstashFilterResource { private final UtmLogstashPipelineService pipelineService; private final ApplicationEventService applicationEventService; - public UtmLogstashFilterResource(UtmLogstashFilterService utmLogstashFilterService, - UtmLogstashFilterQueryService logstashFilterQueryService, - UtmGroupLogstashPipelineFiltersService groupLogstashPipelineFiltersService, UtmLogstashPipelineService pipelineService, ApplicationEventService applicationEventService) { + public UtmFilterResource(UtmLogstashFilterService utmLogstashFilterService, + UtmLogstashFilterQueryService logstashFilterQueryService, + UtmGroupLogstashPipelineFiltersService groupLogstashPipelineFiltersService, UtmLogstashPipelineService pipelineService, ApplicationEventService applicationEventService) { this.logstashFilterService = utmLogstashFilterService; this.logstashFilterQueryService = logstashFilterQueryService; this.groupLogstashPipelineFiltersService = groupLogstashPipelineFiltersService; @@ -61,7 +61,7 @@ public UtmLogstashFilterResource(UtmLogstashFilterService utmLogstashFilterServi * @param logstashFilter the utmLogstashFilter to create * @return the ResponseEntity with status 201 (Created) and with body the new utmLogstashFilter, or with status 400 (Bad Request) if the utmLogstashFilter has already an ID */ - @PostMapping("/logstash-filters") + @PostMapping public ResponseEntity createLogstashFilter(@Valid @RequestBody UtmLogstashFilter logstashFilter, @RequestParam Long pipelineId) { final String ctx = CLASSNAME + ".createLogstashFilter"; @@ -104,7 +104,7 @@ public ResponseEntity createLogstashFilter(@Valid @RequestBod * or with status 400 (Bad Request) if the utmLogstashFilter is not valid, * or with status 500 (Internal Server Error) if the utmLogstashFilter couldn't be updated */ - @PutMapping("/logstash-filters") + @PutMapping public ResponseEntity updateLogstashFilter(@Valid @RequestBody UtmLogstashFilter logstashFilter) { final String ctx = CLASSNAME + ".updateLogstashFilter"; try { @@ -132,7 +132,7 @@ public ResponseEntity updateLogstashFilter(@Valid @RequestBod * @param pageable the pagination information * @return the ResponseEntity with status 200 (OK) and the list of utmLogstashFilters in body */ - @GetMapping("/logstash-filters") + @GetMapping public ResponseEntity> getAllLogstashFilters(UtmLogstashFilterCriteria criteria, Pageable pageable) { final String ctx = CLASSNAME + ".getAllLogstashFilters"; try { @@ -154,7 +154,7 @@ public ResponseEntity> getAllLogstashFilters(UtmLogstash * @param pipelineId the pipeline Id to search for * @return the ResponseEntity with status 200 (OK) and the list of {@link UtmLogstashFilter} in body */ - @GetMapping("/logstash-filters/by-pipelineid") + @GetMapping("/by-pipelineid") public ResponseEntity> filtersByPipelineId(@RequestParam Long pipelineId) { final String ctx = CLASSNAME + ".filtersByPipelineId"; try { @@ -175,7 +175,7 @@ public ResponseEntity> filtersByPipelineId(@RequestParam * @param id the id of the utmLogstashFilter to retrieve * @return the ResponseEntity with status 200 (OK) and with body the utmLogstashFilter, or with status 404 (Not Found) */ - @GetMapping("/logstash-filters/{id}") + @GetMapping("/{id}") public ResponseEntity getLogstashFilter(@PathVariable Long id) { final String ctx = CLASSNAME + ".getLogstashFilter"; try { @@ -196,7 +196,7 @@ public ResponseEntity getLogstashFilter(@PathVariable Long id * @param id the id of the utmLogstashFilter to delete * @return the ResponseEntity with status 200 (OK) */ - @DeleteMapping("/logstash-filters/{id}") + @DeleteMapping("/{id}") public ResponseEntity deleteLogstashFilter(@PathVariable Long id) { final String ctx = CLASSNAME + ".deleteLogstashFilter"; try { diff --git a/backend/src/main/java/com/park/utmstack/web/rest/logstash_pipeline/UtmLogstashPipelineResource.java b/backend/src/main/java/com/park/utmstack/web/rest/logstash_pipeline/UtmLogstashPipelineResource.java index 2aef240ee..85d172b86 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/logstash_pipeline/UtmLogstashPipelineResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/logstash_pipeline/UtmLogstashPipelineResource.java @@ -8,7 +8,7 @@ import com.park.utmstack.service.dto.logstash_pipeline.UtmLogstashPipelineDTO; import com.park.utmstack.service.logstash_pipeline.UtmLogstashPipelineService; import com.park.utmstack.service.logstash_pipeline.response.ApiStatsResponse; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.errors.BadRequestAlertException; import com.park.utmstack.web.rest.util.HeaderUtil; import com.park.utmstack.web.rest.vm.UtmLogstashPipelineVM; @@ -76,7 +76,7 @@ public ResponseEntity> getAllActivePipelines( String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -96,12 +96,12 @@ public ResponseEntity getLogstashStats() { String msg = ctx + ": Logstash server can't be reached, may be it's down, check the message -> " + ex.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } catch (Exception e) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -130,7 +130,7 @@ public ResponseEntity getUtmLogstashPipeline(@PathVariabl String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -155,7 +155,7 @@ public ResponseEntity validatePipelines(@RequestBody UtmLogstash String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -182,7 +182,7 @@ public ResponseEntity deleteUtmLogstashPipeline(@PathVariable Long id) { String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/network_scan/UtmNetworkScanResource.java b/backend/src/main/java/com/park/utmstack/web/rest/network_scan/UtmNetworkScanResource.java index b4437bd3a..ee8294059 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/network_scan/UtmNetworkScanResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/network_scan/UtmNetworkScanResource.java @@ -4,13 +4,12 @@ import com.park.utmstack.domain.network_scan.NetworkScanFilter; import com.park.utmstack.domain.network_scan.Property; import com.park.utmstack.domain.network_scan.UtmNetworkScan; -import com.park.utmstack.domain.network_scan.enums.PropertyFilter; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.dto.network_scan.NetworkScanDTO; import com.park.utmstack.service.dto.network_scan.UtmNetworkScanCriteria; import com.park.utmstack.service.network_scan.UtmNetworkScanQueryService; import com.park.utmstack.service.network_scan.UtmNetworkScanService; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.errors.BadRequestAlertException; import com.park.utmstack.web.rest.util.HeaderUtil; import com.park.utmstack.web.rest.util.PaginationUtil; @@ -284,7 +283,7 @@ public ResponseEntity> getAgentsOsPlatform() { String msg = ctx + ": " + e.getMessage(); log.error(msg); eventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/overview/OverviewResource.java b/backend/src/main/java/com/park/utmstack/web/rest/overview/OverviewResource.java index 6f6888dec..0c8fe9ebe 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/overview/OverviewResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/overview/OverviewResource.java @@ -70,6 +70,7 @@ public ResponseEntity> countAlertsByStatus(@RequestParam String f filters.add(new FilterType(Constants.timestamp, OperatorType.IS_BETWEEN, Arrays.asList(from, to))); filters.add(new FilterType(Constants.alertStatus, OperatorType.IS_NOT, 1)); filters.add(new FilterType(Constants.alertTags, OperatorType.IS_NOT, Constants.FALSE_POSITIVE_TAG)); + filters.add(new FilterType(Constants.alertParentIdKeyword, OperatorType.DOES_NOT_EXIST, null)); return ResponseEntity.ok(alertService.countAlertsByStatus(filters)); } catch (Exception e) { diff --git a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java new file mode 100644 index 000000000..99139e4d9 --- /dev/null +++ b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaEnrollmentResource.java @@ -0,0 +1,107 @@ +package com.park.utmstack.web.rest.tfa; + +import com.park.utmstack.config.Constants; +import com.park.utmstack.domain.Authority; +import com.park.utmstack.domain.User; +import com.park.utmstack.domain.UtmConfigurationParameter; +import com.park.utmstack.security.jwt.TokenProvider; +import com.park.utmstack.service.UserService; +import com.park.utmstack.service.UtmConfigurationParameterService; +import com.park.utmstack.service.dto.jwt.LoginResponseDTO; +import com.park.utmstack.service.dto.tfa.enroll.TfaEnrollRequest; +import com.park.utmstack.service.dto.tfa.init.TfaInitResponse; +import com.park.utmstack.service.dto.tfa.save.TfaSaveRequest; +import com.park.utmstack.service.dto.tfa.verify.TfaVerifyResponse; +import com.park.utmstack.service.tfa.TfaService; +import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.exceptions.InvalidTfaStageException; +import io.swagger.v3.oas.annotations.Hidden; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.park.utmstack.config.Constants.PROP_TFA_METHOD; + +@RestController +@RequiredArgsConstructor +@Slf4j +@Hidden +@RequestMapping("api/enrollment/tfa") +public class TfaEnrollmentResource { + + private static final String CLASSNAME = "TfaEnrollmentController"; + + private final UserService userService; + private final TfaService tfaService; + private final UtmConfigurationParameterService utmConfigurationParameterService; + private final TokenProvider tokenProvider; + + + @PostMapping + public ResponseEntity enrollTfa(@RequestBody TfaEnrollRequest request) { + User user = userService.getCurrentUserLogin(); + + return switch (request.getStage()) { + case INIT -> { + TfaInitResponse initResponse = tfaService.initiateSetup(user, request.getMethod()); + yield ResponseEntity.ok(initResponse); + } + case VERIFY -> { + TfaVerifyResponse verifyResponse = tfaService.verifyCode(user, request.toVerifyRequest()); + yield ResponseEntity.ok(verifyResponse); + } + case COMPLETE -> { + List tfaParams = utmConfigurationParameterService + .getConfigParameterBySectionId(Constants.TFA_SETTING_ID); + + for (UtmConfigurationParameter param : tfaParams) { + switch (param.getConfParamShort()) { + case PROP_TFA_METHOD: + param.setConfParamValue(String.valueOf(request.getMethod())); + break; + case Constants.PROP_TFA_ENABLE: + param.setConfParamValue(String.valueOf(request.isEnable())); + break; + } + } + + tfaService.persistConfiguration(request.getMethod()); + utmConfigurationParameterService.saveAllConfigParams(tfaParams); + List authorities = user.getAuthorities().stream() + .map(Authority::getName) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + org.springframework.security.core.userdetails.User principal = + new org.springframework.security.core.userdetails.User(user.getLogin(), "", authorities); + + UsernamePasswordAuthenticationToken fullAuth = + new UsernamePasswordAuthenticationToken(principal, "", authorities); + + + String fullToken = tokenProvider.createToken(fullAuth, false, true ); + + LoginResponseDTO response = LoginResponseDTO.builder() + .token(fullToken) + .method(user.getTfaMethod()) + .success(true) + .tfaConfigured(true) + .forceTfa(true) + .build(); + + yield ResponseEntity.ok(response); + } + default -> throw new InvalidTfaStageException("Invalid TFA stage: " + request.getStage()); + }; + } +} + diff --git a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaController.java b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaResource.java similarity index 67% rename from backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaController.java rename to backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaResource.java index e77ff02a4..03d612f7f 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaController.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/tfa/TfaResource.java @@ -1,11 +1,13 @@ package com.park.utmstack.web.rest.tfa; +import com.park.utmstack.aop.logging.AuditEvent; import com.park.utmstack.config.Constants; import com.park.utmstack.domain.Authority; import com.park.utmstack.domain.User; import com.park.utmstack.domain.UtmConfigurationParameter; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.domain.tfa.TfaMethod; +import com.park.utmstack.loggin.LogContextBuilder; import com.park.utmstack.security.jwt.JWTFilter; import com.park.utmstack.security.jwt.TokenProvider; import com.park.utmstack.service.UserService; @@ -18,21 +20,20 @@ import com.park.utmstack.service.dto.tfa.verify.TfaVerifyRequest; import com.park.utmstack.service.dto.tfa.verify.TfaVerifyResponse; import com.park.utmstack.service.tfa.TfaService; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; +import com.park.utmstack.util.exceptions.TfaVerificationException; import com.park.utmstack.util.exceptions.UtmMailException; -import com.park.utmstack.web.rest.util.HeaderUtil; +import io.swagger.v3.oas.annotations.Hidden; import lombok.RequiredArgsConstructor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.web.bind.annotation.*; -import tech.jhipster.web.util.ResponseUtil; +import javax.servlet.http.HttpServletRequest; import java.util.List; import java.util.stream.Collectors; @@ -40,10 +41,11 @@ @RestController @RequiredArgsConstructor +@Slf4j +@Hidden @RequestMapping("api/tfa") -public class TfaController { +public class TfaResource { - private final Logger log = LoggerFactory.getLogger(TfaController.class); private static final String CLASSNAME = "TfaController"; private final TfaService tfaService; @@ -51,6 +53,7 @@ public class TfaController { private final ApplicationEventService applicationEventService; private final UtmConfigurationParameterService utmConfigurationParameterService; private final TokenProvider tokenProvider; + private final LogContextBuilder logContextBuilder; @PostMapping("/init") public ResponseEntity initTfa(@RequestBody TfaInitRequest request) { @@ -63,7 +66,7 @@ public ResponseEntity initTfa(@RequestBody TfaInitRequest reque String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildInternalServerErrorResponse(msg); + return ResponseUtil.buildInternalServerErrorResponse(msg); } } @@ -75,25 +78,26 @@ public ResponseEntity verifyTfa(@RequestBody TfaVerifyRequest User user = userService.getCurrentUserLogin(); TfaVerifyResponse response = tfaService.verifyCode(user, request); return ResponseEntity.ok(response); + } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); - return UtilResponse.buildInternalServerErrorResponse(msg); + return ResponseUtil.buildInternalServerErrorResponse(msg); } } - @GetMapping("/generate-challenge") + @GetMapping("/refresh") public ResponseEntity generateChallenge() { final String ctx = CLASSNAME + ".generateChallenge"; try { User user = userService.getCurrentUserLogin(); - tfaService.generateChallenge(user); + tfaService.regenerateChallenge(user); return ResponseEntity.ok().build(); } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildInternalServerErrorResponse(msg); + return ResponseUtil.buildInternalServerErrorResponse(msg); } } @@ -124,54 +128,52 @@ public ResponseEntity completeTfa(@RequestBody TfaSaveRequest request) { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildPreconditionFailedResponse(msg); + return ResponseUtil.buildPreconditionFailedResponse(msg); } catch (IllegalArgumentException e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildBadRequestResponse(msg); + return ResponseUtil.buildBadRequestResponse(msg); } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildInternalServerErrorResponse(msg); + return ResponseUtil.buildInternalServerErrorResponse(msg); } } - @PostMapping("/verifyCode") - public ResponseEntity verifyCode(@RequestBody String code) { + @AuditEvent( + attemptType = ApplicationEventType.TFA_CODE_VERIFY_ATTEMPT, + attemptMessage = "Verification attempt for second-factor authentication", + successType = ApplicationEventType.AUTH_SUCCESS, + successMessage = "Login successfully completed" + ) + @PostMapping("/verify-code") + public ResponseEntity verifyCode(@RequestBody String code, HttpServletRequest request) { final String ctx = CLASSNAME + ".verifyCode"; - try { - User user = userService.getCurrentUserLogin(); - TfaMethod method = TfaMethod.valueOf(user.getTfaMethod()); - TfaVerifyRequest request = new TfaVerifyRequest(method, code); - TfaVerifyResponse response = tfaService.verifyCode(user, request); + User user = userService.getCurrentUserLogin(); + TfaMethod method = TfaMethod.valueOf(user.getTfaMethod()); + TfaVerifyRequest tfaVerifyRequest = new TfaVerifyRequest(method, code); + TfaVerifyResponse response = tfaService.verifyCode(user, tfaVerifyRequest); - if (!response.isValid()){ - return ResponseEntity.status(HttpStatus.PRECONDITION_FAILED) - .headers(HeaderUtil.createFailureAlert("", "", response.getMessage())) - .body(null); - } + if (!response.isValid()) { + throw new TfaVerificationException("TFA invalid for user '" + user.getLogin() + "': " + response.getMessage()); + } - List authorities = user.getAuthorities().stream().map(Authority::getName) - .map(SimpleGrantedAuthority::new).collect(Collectors.toList()); + List authorities = user.getAuthorities().stream().map(Authority::getName) + .map(SimpleGrantedAuthority::new).collect(Collectors.toList()); - org.springframework.security.core.userdetails.User principal = new org.springframework.security.core.userdetails.User(user.getLogin(), "", authorities); + org.springframework.security.core.userdetails.User principal = new org.springframework.security.core.userdetails.User(user.getLogin(), "", authorities); - UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(principal, "", authorities); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(principal, "", authorities); - String jwt = tokenProvider.createToken(authentication, true, true); + String jwt = tokenProvider.createToken(authentication, true, true); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(JWTFilter.AUTHORIZATION_HEADER, "Bearer " + jwt); + + return new ResponseEntity<>(new JWTToken(jwt, true), httpHeaders, HttpStatus.OK); - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add(JWTFilter.AUTHORIZATION_HEADER, "Bearer " + jwt); - return new ResponseEntity<>(new JWTToken(jwt, true), httpHeaders, HttpStatus.OK); - } catch (Exception e) { - String msg = ctx + ": " + e.getMessage(); - log.error(msg); - applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).headers( - HeaderUtil.createFailureAlert("", "", msg)).body(null); - } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/user_auditor/UtmAuditorUsersResource.java b/backend/src/main/java/com/park/utmstack/web/rest/user_auditor/UtmAuditorUsersResource.java index 08ab745fa..858457d88 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/user_auditor/UtmAuditorUsersResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/user_auditor/UtmAuditorUsersResource.java @@ -2,7 +2,7 @@ import com.park.utmstack.config.Constants; import com.park.utmstack.service.dto.user_auditor.UtmAuditorUsersDTO; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.user_auditor.dto.MicroserviceRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -89,7 +89,7 @@ public ResponseEntity> getUtmAuditorUsers(@RequestParam String sid, } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } @@ -135,7 +135,7 @@ public ResponseEntity> getAllUtmAuditorUsers(Pageable p } catch (Exception e) { String msg = ctx + ": " + e.getMessage(); log.error(msg); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/util/HeaderUtil.java b/backend/src/main/java/com/park/utmstack/web/rest/util/HeaderUtil.java index 72f13e786..861165a16 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/util/HeaderUtil.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/util/HeaderUtil.java @@ -36,7 +36,7 @@ public static HttpHeaders createEntityDeletionAlert(String entityName, String pa } public static HttpHeaders createFailureAlert(String entityName, String errorKey, String defaultMessage) { - log.error("Entity processing failed, {}", defaultMessage); + /* log.error("Entity processing failed, {}", defaultMessage);*/ HttpHeaders headers = new HttpHeaders(); headers.add("X-" + APPLICATION_NAME + "-error", defaultMessage); headers.add("X-" + APPLICATION_NAME + "-params", entityName); diff --git a/backend/src/main/java/com/park/utmstack/web/rest/util/PdfGeneratorResource.java b/backend/src/main/java/com/park/utmstack/web/rest/util/PdfGeneratorResource.java index 676db5cd4..85ebab44a 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/util/PdfGeneratorResource.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/util/PdfGeneratorResource.java @@ -1,11 +1,10 @@ package com.park.utmstack.web.rest.util; -import com.park.utmstack.config.Constants; import com.park.utmstack.domain.application_events.enums.ApplicationEventType; import com.park.utmstack.security.jwt.JWTFilter; import com.park.utmstack.service.application_events.ApplicationEventService; import com.park.utmstack.service.util.PdfService; -import com.park.utmstack.util.UtilResponse; +import com.park.utmstack.util.ResponseUtil; import com.park.utmstack.web.rest.errors.BadRequestAlertException; import javassist.NotFoundException; import org.slf4j.Logger; @@ -76,7 +75,7 @@ public ResponseEntity getPdfReportInBytes(@RequestParam String url, String msg = ctx + ": " + e.getLocalizedMessage(); log.error(msg); applicationEventService.createEvent(msg, ApplicationEventType.ERROR); - return UtilResponse.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); + return ResponseUtil.buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, msg); } } } diff --git a/backend/src/main/java/com/park/utmstack/web/rest/vm/LoginVM.java b/backend/src/main/java/com/park/utmstack/web/rest/vm/LoginVM.java index 74c59fd49..bf41765a5 100644 --- a/backend/src/main/java/com/park/utmstack/web/rest/vm/LoginVM.java +++ b/backend/src/main/java/com/park/utmstack/web/rest/vm/LoginVM.java @@ -1,47 +1,36 @@ package com.park.utmstack.web.rest.vm; +import com.park.utmstack.service.dto.auditable.AuditableDTO; +import lombok.Getter; +import lombok.Setter; + import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; +import java.util.HashMap; +import java.util.Map; /** * View Model object for storing a user's credentials. */ -public class LoginVM { +@Setter +public class LoginVM implements AuditableDTO { + @Getter @NotNull @Size(min = 1, max = 50) private String username; + @Getter @NotNull @Size(min = ManagedUserVM.PASSWORD_MIN_LENGTH, max = ManagedUserVM.PASSWORD_MAX_LENGTH) private String password; private Boolean rememberMe; - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - public Boolean isRememberMe() { return rememberMe; } - public void setRememberMe(Boolean rememberMe) { - this.rememberMe = rememberMe; - } - @Override public String toString() { return "LoginVM{" + @@ -49,4 +38,13 @@ public String toString() { ", rememberMe=" + rememberMe + '}'; } + + @Override + public Map toAuditMap() { + Map context = new HashMap<>(); + + context.put("loginAttempt", this.username); + + return context; + } } diff --git a/backend/src/main/resources/config/application-prod.yml b/backend/src/main/resources/config/application-prod.yml index 20d1ca27b..90c98cbba 100644 --- a/backend/src/main/resources/config/application-prod.yml +++ b/backend/src/main/resources/config/application-prod.yml @@ -2,7 +2,7 @@ logging: level: ROOT: error tech.jhipster: error - com.park.utmstack: error + com.park.utmstack: info spring: devtools: diff --git a/backend/src/main/resources/config/liquibase/changelog/20251010001_adding_utmstack_integration.xml b/backend/src/main/resources/config/liquibase/changelog/20251010001_adding_utmstack_integration.xml new file mode 100644 index 000000000..afbdf24ae --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20251010001_adding_utmstack_integration.xml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + do + $$ + begin + perform public.execute_register_integration_function(); + end; + $$ + language plpgsql; + + + + + + + + + + + diff --git a/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml new file mode 100644 index 000000000..e73010f6a --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20251017001_create_api_keys_table.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/config/liquibase/changelog/20251029001-add-identity-provider-config.xml b/backend/src/main/resources/config/liquibase/changelog/20251029001-add-identity-provider-config.xml new file mode 100644 index 000000000..d693a64ce --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20251029001-add-identity-provider-config.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/config/liquibase/changelog/20251106001_update_incident_menu_position.xml b/backend/src/main/resources/config/liquibase/changelog/20251106001_update_incident_menu_position.xml new file mode 100644 index 000000000..88f527528 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20251106001_update_incident_menu_position.xml @@ -0,0 +1,34 @@ + + + + + Update Incidents menu position to appear before Tagging rules + + + + + id = 314 AND parent_id = 300 + + + + + + id = 402 AND parent_id = 300 + + + + + + id = 314 AND parent_id = 300 + + + + id = 402 AND parent_id = 300 + + + + + diff --git a/backend/src/main/resources/config/liquibase/changelog/20251107001_hide_compliance_menu_items.xml b/backend/src/main/resources/config/liquibase/changelog/20251107001_hide_compliance_menu_items.xml new file mode 100644 index 000000000..78990e6de --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20251107001_hide_compliance_menu_items.xml @@ -0,0 +1,21 @@ + + + + + + + id IN (502, 503, 504, 505) + + + + + id IN (502, 503, 504, 505) + + + + + + diff --git a/backend/src/main/resources/config/liquibase/changelog/20251107002_update_filters_fields_visualization.xml b/backend/src/main/resources/config/liquibase/changelog/20251107002_update_filters_fields_visualization.xml new file mode 100644 index 000000000..e26f39754 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20251107002_update_filters_fields_visualization.xml @@ -0,0 +1,75 @@ + + + + + + + + + filters::text LIKE '%log.winlog.event_id%' OR + filters::text LIKE '%log.winlog.event_data.SubjectUserName%' OR + filters::text LIKE '%log.winlog.event_data.TargetUserName%' OR + filters::text LIKE '%log.winlog.event_data.NewProcessName.keyword%' OR + filters::text LIKE '%log.winlog.event.code.keyword%' OR + filters::text LIKE '%log.winlog.event_name.keyword%' OR + filters::text LIKE '%log.winlog.event_data.SubjectUserSid.keyword%' OR + filters::text LIKE '%log.winlog.beat.hostname.keyword%' OR + filters::text LIKE '%log.winlog.event_data.LogonType.keyword%' + + + + + + + + aggregation::text LIKE '%log.winlog.event_id%' OR + aggregation::text LIKE '%log.winlog.event_data.SubjectUserName%' OR + aggregation::text LIKE '%log.winlog.event_data.TargetUserName%' OR + aggregation::text LIKE '%log.winlog.event_data.NewProcessName.keyword%' OR + aggregation::text LIKE '%log.winlog.event.code.keyword%' OR + aggregation::text LIKE '%log.winlog.event_name.keyword%' OR + aggregation::text LIKE '%log.winlog.event_data.SubjectUserSid.keyword%' OR + aggregation::text LIKE '%log.winlog.beat.hostname.keyword%' OR + aggregation::text LIKE '%log.winlog.event_data.LogonType.keyword%' + + + + + + + + + filters::text LIKE '%log.winlogEventId%' OR + filters::text LIKE '%log.winlogEventDataSubjectUserName%' OR + filters::text LIKE '%log.winlogEventDataTargetUserName%' OR + filters::text LIKE '%winlogEventDataProcessName.keyword%' OR + filters::text LIKE '%log.eventCode%' OR + filters::text LIKE '%log.eventName%' OR + filters::text LIKE '%log.winlogEventDataSubjectUserSid%' OR + filters::text LIKE '%dataSource.keyword%' OR + filters::text LIKE '%log.winlogEventDataLogonType.keyword%' + + + + + + + + aggregation::text LIKE '%log.winlogEventId%' OR + aggregation::text LIKE '%log.winlogEventDataSubjectUserName%' OR + aggregation::text LIKE '%log.winlogEventDataTargetUserName%' OR + aggregation::text LIKE '%winlogEventDataProcessName.keyword%' OR + aggregation::text LIKE '%log.eventCode%' OR + aggregation::text LIKE '%log.eventName%' OR + aggregation::text LIKE '%log.winlogEventDataSubjectUserSid%' OR + aggregation::text LIKE '%dataSource.keyword%' OR + aggregation::text LIKE '%log.winlogEventDataLogonType.keyword%' + + + + + + diff --git a/backend/src/main/resources/config/liquibase/changelog/20251110001_disable_correlation_rules_with_regex.xml b/backend/src/main/resources/config/liquibase/changelog/20251110001_disable_correlation_rules_with_regex.xml new file mode 100644 index 000000000..af63e50d8 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20251110001_disable_correlation_rules_with_regex.xml @@ -0,0 +1,23 @@ + + + + + + + + rule_definition_def LIKE '%.matches(%' + + + + + + rule_definition_def LIKE '%.matches(%' + + + + + + diff --git a/backend/src/main/resources/config/liquibase/changelog/20251110002_add_system_owner_to_alert_response_rules.xml b/backend/src/main/resources/config/liquibase/changelog/20251110002_add_system_owner_to_alert_response_rules.xml new file mode 100644 index 000000000..160560da9 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20251110002_add_system_owner_to_alert_response_rules.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/config/liquibase/changelog/20251110003_add_alert-response_rule_data.xml b/backend/src/main/resources/config/liquibase/changelog/20251110003_add_alert-response_rule_data.xml new file mode 100644 index 000000000..928457d78 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20251110003_add_alert-response_rule_data.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/backend/src/main/resources/config/liquibase/changelog/20251111001_set_alert-response_rule_seq.xml b/backend/src/main/resources/config/liquibase/changelog/20251111001_set_alert-response_rule_seq.xml new file mode 100644 index 000000000..f34def25f --- /dev/null +++ b/backend/src/main/resources/config/liquibase/changelog/20251111001_set_alert-response_rule_seq.xml @@ -0,0 +1,13 @@ + + + + + + SELECT setval('utm_alert_response_rule_id_seq', 5000, false); + + + + diff --git a/backend/src/main/resources/config/liquibase/data/utm_alert_response_rule.sql b/backend/src/main/resources/config/liquibase/data/utm_alert_response_rule.sql new file mode 100644 index 000000000..dab2efc73 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/data/utm_alert_response_rule.sql @@ -0,0 +1,29 @@ + +INSERT INTO public.utm_alert_response_rule VALUES (43, 'Windows Defender Exclusions Added via PowerShell', 'Automatically removes any Windows Defender exclusions added via PowerShell to prevent defense evasion.', '[{"operator":"IS","field":"name","value":"Windows Defender Exclusions Added via PowerShell"}]', '''powershell.exe -Command "Remove-MpPreference -ExclusionPath \"$(log.message | regex_extract -pattern ''''-ExclusionPath\s+''''(.+?)'''')\""'' ; ''powershell.exe -Command "Remove-MpPreference -ExclusionProcess \"$(log.message | regex_extract -pattern ''''-ExclusionProcess\s+''''(.+?)'''')\""''', false, 'windows', NULL, 'admin', '2025-08-21 01:06:59.177318', 'admin', '2025-11-10 17:26:44.510513', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (37, 'Windows: Possible Brute Force Attack', 'Detects patterns of repeated and rapid login attempts from the same IP address or source, indicating a potential brute force attack. This playbook responds by blacklisting the adversary''s IP in the firewall to block further connections.', '[{"operator":"IS","field":"name","value":"Windows: Possible Brute Force Attack"}]', 'netsh advfirewall firewall add rule name="Block-Brute-Force-$(adversary.ip)" dir=in action=block remoteip="$(adversary.ip)" enable=yes', false, 'windows', NULL, 'admin', '2025-07-30 13:16:08.342486', 'admin', '2025-11-10 17:26:58.839489', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (44, 'Windows: PowerShell Keylogging Script', 'Detects the use of Win32 API Functions that can be used to capture user keystrokes in PowerShell scripts. Terminate the process using the detected process name.', '[{"operator":"IS","field":"name","value":"Windows: PowerShell Keylogging Script"}]', 'taskkill /F /PID $(log.winlogEventDataProcessId) ; taskkill /F /IM $(log.winlogEventDataProcessName)', false, 'windows', NULL, 'admin', '2025-08-25 23:49:30.14447', 'maykel', '2025-09-17 16:17:39.748827', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (38, 'Windows: Multiple Logon Failure Followed by Logon Success', 'Detects a sequence of multiple failed login attempts immediately followed by a successful login from the same IP address or source. This playbook responds by blocking the adversary''s IP and disabling the user account.', '[{"operator":"IS","field":"name","value":"Windows: Multiple Logon Failure Followed by Logon Success"}]', 'netsh advfirewall firewall add rule name="Block-Brute-Force-Success-$(adversary.ip)" dir=in action=block remoteip="$(adversary.ip)" enable=yes ; net user "$(target.user)" /active:no', false, 'windows', NULL, 'admin', '2025-07-30 13:28:18.178386', 'admin', '2025-09-11 14:17:41.67997', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (27, 'Windows Server - Failed RDP Brute Force Response', 'Automates a simple, direct response to RDP brute-force attempts by blocking the attacker''s IP.', '[{"operator":"IS","field":"name","value":"RDP Brute Force Attack"}]', 'netsh advfirewall firewall add rule name="Block-RDP-Brute-Force-$(adversary.ip)" dir=in action=block remoteip="$(adversary.ip)" enable=yes', false, 'windows', NULL, 'admin', '2025-07-21 15:36:37.752404', 'admin', '2025-09-11 14:17:46.972405', '', true); +INSERT INTO public.utm_alert_response_rule VALUES (30, 'Windows Server - PowerShell Empire Detection', 'Detects potential PowerShell Empire framework usage based on characteristic command patterns, obfuscation techniques, and encoded payloads. This playbook automates actions to kill suspicious processes, delete malicious files, and disable compromised user accounts.', '[{"operator":"IS","field":"name","value":"PowerShell Empire Detection"}]', 'taskkill /F /IM "$(target.process)" ; Remove-Item -LiteralPath "$(target.file)" -Force -Recurse ; net user "$(target.user)" /active:no', false, 'windows', NULL, 'admin', '2025-07-21 16:24:30.144016', 'admin', '2025-09-11 14:17:49.276392', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (40, 'Windows: A user account was added to administration groups', 'Disable the user account that was added to an administrative group to prevent privilege escalation. +', '[{"operator":"IS","field":"name","value":"Windows: A user account was added to administration groups"}]', 'net user "$(target.user)" /active:no +', false, 'windows', NULL, 'admin', '2025-08-20 16:53:47.031384', 'admin', '2025-09-11 14:17:54.071588', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (36, 'System Linux: Attempt to Disable Syslog Service', 'Detects attempts to disable the syslog service to disrupt event logging and evade security controls. This playbook responds by logging out and disabling the user account involved.', '[{"operator":"IS","field":"name","value":"System Linux: Attempt to Disable Syslog Service"}]', 'sudo pkill -KILL -u $(target.user) ; sudo usermod --expiredate 1 $(target.user)', false, 'ubuntu', NULL, 'admin', '2025-07-30 13:10:01.375524', 'admin', '2025-09-11 14:18:07.87563', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (42, 'Windows: Disable Windows Firewall Rules via Netsh', 'Re-enables Windows Firewall rules that were disabled by the detected netsh command. +', '[{"operator":"IS","field":"name","value":"Windows: Disable Windows Firewall Rules via Netsh"}]', 'netsh advfirewall set allprofiles state on +', false, 'windows', NULL, 'admin', '2025-08-21 00:04:56.548359', 'admin', '2025-09-11 14:17:11.064259', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (29, 'Windows Server - Volume Shadow Copy Deletion', 'Detects and responds to unauthorized deletion of Volume Shadow Copies by terminating suspicious processes, logging out the user, and disabling the user account.', '[{"operator":"IS","field":"name","value":"Volume Shadow Copy Deletion"}]', 'taskkill /F /IM "$(target.process)" ; logoff $(target.user) ; net user "$(target.user)" /active:no', false, 'windows', NULL, 'admin', '2025-07-21 16:06:25.490435', 'admin', '2025-09-11 14:17:14.95432', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (31, 'Linux - Ubuntu - Brute Force User via SSH', 'Detects multiple failed SSH authentication attempts from the same source IP, indicating a potential brute force attack. This playbook blocks the attacking IP, disables the targeted user account, and initiates a host shutdown.', '[{"operator":"IS","field":"name","value":"SSH Brute Force Attempts"}]', 'sudo ufw insert 1 deny from $(adversary.ip) to any port 22 proto tcp ; sudo usermod --expiredate 1 $(target.user) ; sudo shutdown -h now', false, 'ubuntu', NULL, 'admin', '2025-07-21 18:55:28.968204', 'admin', '2025-09-11 14:17:02.54344', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (32, 'Linux - CentOS/RHEL - CVE Vulnerability Detection in RHEL System', 'Detects critical CVE vulnerabilities on RHEL-based systems. Upon detection, this playbook initiates an immediate server shutdown to prevent exploitation.', '[{"operator":"IS","field":"name","value":"CVE Vulnerability Detection in RHEL System"}]', 'sudo shutdown -h now', false, 'centos', NULL, 'admin', '2025-07-21 19:25:03.883518', 'maykel', '2025-09-17 16:17:42.806521', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (34, 'Linux - Process Injection Techniques Detection', 'Detects and responds to suspicious process injection techniques on Ubuntu 20.04/22.04 LTS. This playbook automates disabling the user account associated with the anomalous process execution.', '[{"operator":"IS","field":"name","value":"Process Execution Anomalies"}]', 'sudo usermod --expiredate 1 $(target.user)', false, 'ubuntu', NULL, 'admin', '2025-07-24 18:15:26.605242', 'maykel', '2025-10-15 21:07:02.163459', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (35, 'System Linux: Possible Brute Force Attack', 'Identifies multiple SSH login failures followed by a successful one from the same source address. Adversaries can attempt to login into multiple users with a common or known password to gain access to accounts.', '[{"operator":"IS","field":"name","value":"System Linux: Possible Brute Force Attack"}]', 'sudo ufw deny from $(adversary.ip) to any', false, 'ubuntu', NULL, 'admin', '2025-07-30 01:23:44.667895', 'maykel', '2025-09-17 19:33:08.862106', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (41, 'Windows Defender: Protection Disabled', 'Re-enable Windows Defender protection to restore security defenses.', '[{"operator":"IS","field":"name","value":"Windows Defender: Protection Disabled"}]', '"%ProgramFiles%\Windows Defender\MpCmdRun.exe" -Enable', false, 'windows', NULL, 'admin', '2025-08-20 17:28:58.710767', 'maykel', '2025-09-17 16:17:19.899829', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (28, 'Windows Server - AdminSDHolder Abuse Detection', 'Detects modifications to the AdminSDHolder object, which can be used for persistence by granting elevated privileges. This playbook automates disabling the user account that performed the modification.', '[{"operator":"IS","field":"name","value":"AdminSDHolder Abuse Detection"}]', 'net user "$(adversary.user)" /active:no', false, 'windows', NULL, 'admin', '2025-07-21 15:52:59.512221', 'admin', '2025-09-11 14:17:59.212754', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (1, 'Windows: A user account was added to administration groups', 'Disable the user account that was added to an administrative group to prevent privilege escalation.', '[{"operator":"IS","field":"name","value":"Windows: A user account was added to administration groups"}]', 'net user "$(target.user)" /active:no', false, 'windows', NULL, 'maykel', '2025-09-13 20:24:00.667045', 'maykel', '2025-09-18 12:56:47.534763', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (48, 'Windows: Volume Shadow Copy Deletion', 'Kill Suspicious Process, Disable Compromised User Account and Logout Compromised User.', '[{"operator":"IS","field":"name","value":"Volume Shadow Copy Deletion"}]', 'powershell taskkill /F /IM "$(target.process)" & net user "$(target.user)" /active:no & logoff $(target.user)', false, 'windows', NULL, 'maykel', '2025-09-03 03:41:35.824516', 'maykel', '2025-09-03 03:45:56.409799', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (33, 'Linux - CentOS/RHEL - RHEL-Specific Kernel Exploitation Attempt', 'Detects attempts to exploit kernel vulnerabilities specific to RHEL-based systems. This playbook automates logging out and disabling the user account associated with the exploitation attempt.', '[{"operator":"IS","field":"name","value":"RHEL-Specific Kernel Exploitation Attempt"}]', 'sudo pkill -KILL -u $(target.user) ; sudo usermod --expiredate 1 $(target.user)', false, 'centos', NULL, 'admin', '2025-07-21 19:35:09.53047', 'admin', '2025-09-11 14:17:11.860741', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (45, 'Windows: Disabling Windows Defender Security Settings via PowerShell', 'enable Identifies use of the Set-MpPreference PowerShell command to disable or weaken certain Windows Defender settings.', '[{"operator":"IS","field":"name","value":"Windows: Disabling Windows Defender Security Settings via PowerShell"}]', 'powershell Set-MpPreference -DisableRealtimeMonitoring $(if ("$(log.message)" -match ''-DisableRealtimeMonitoring'') { $false } else { $true }) ; powershell Set-MpPreference -DisableBehaviorMonitoring $(if ("$(log.message)" -match ''-DisableBehaviorMonitoring'') { $false } else { $true }) ; powershell Set-MpPreference -PUAProtection $(if ("$(log.message)" -match ''-PUAProtection'') { ''"Enabled"'' } else { ''"Disabled"'' }) ; powershell Set-MpPreference -MAPSReporting $(if ("$(log.message)" -match ''-MAPSReporting'') { 1 } else { 0 }) ; powershell Set-MpPreference -CloudProtection $(if ("$(log.message)" -match ''CloudProtection'') { 1 } else { 0 }) ; powershell Remove-MpPreference -ExclusionProcess $(if ("$(log.message)" -match ''-ExclusionProcess'') { ''"evil.exe"'' } else { ''"Not A Process"'' })', false, 'windows', NULL, 'maykel', '2025-08-30 15:50:58.22476', 'admin', '2025-09-11 14:17:17.742859', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (46, 'Windows: Multiple remote access login failures', 'Block the IP that is establishing the connection.', '[{"operator":"IS","field":"name","value":"Windows: Multiple remote access login failures"}]', 'powershell New-NetFirewallRule -DisplayName "Blocked_BruteForce_IP" -Direction Inbound -RemoteAddress "$(origin.ip)" -Action Block -Protocol Any -Profile Any -Enabled True', false, 'windows', NULL, 'maykel', '2025-08-30 19:42:23.870136', 'admin', '2025-09-11 14:17:22.106034', NULL, true); +INSERT INTO public.utm_alert_response_rule VALUES (47, 'Windows: Remote Desktop Enabled in Windows Firewall by Netsh', 'Disable inbound Remote Desktop Protocol and remove Firewall rule.', '[{"operator":"IS","field":"name","value":"Windows: Remote Desktop Enabled in Windows Firewall by Netsh"}]', 'powershell Set-ItemProperty -Path ''HKLM:\System\CurrentControlSet\Control\Terminal Server'' -Name ''fDenyTSConnections'' -Value 1 -Force;if ("$(log.message)" -match ''name=([^''''"\s]+)'') { Remove-NetFirewallRule -DisplayName $matches[1] -ErrorAction SilentlyContinue }', false, 'windows', NULL, 'maykel', '2025-09-01 21:03:20.282646', 'admin', '2025-09-11 14:18:13.915659', NULL, true); + + diff --git a/backend/src/main/resources/config/liquibase/data/utm_alert_response_rule_template.sql b/backend/src/main/resources/config/liquibase/data/utm_alert_response_rule_template.sql new file mode 100644 index 000000000..091c531f8 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/data/utm_alert_response_rule_template.sql @@ -0,0 +1,35 @@ + +INSERT INTO public.utm_alert_response_rule_template VALUES (46, 67); +INSERT INTO public.utm_alert_response_rule_template VALUES (27, 67); +INSERT INTO public.utm_alert_response_rule_template VALUES (38, 67); +INSERT INTO public.utm_alert_response_rule_template VALUES (37, 67); +INSERT INTO public.utm_alert_response_rule_template VALUES (29, 91); +INSERT INTO public.utm_alert_response_rule_template VALUES (1, 91); +INSERT INTO public.utm_alert_response_rule_template VALUES (48, 91); +INSERT INTO public.utm_alert_response_rule_template VALUES (40, 91); +INSERT INTO public.utm_alert_response_rule_template VALUES (30, 91); +INSERT INTO public.utm_alert_response_rule_template VALUES (38, 91); +INSERT INTO public.utm_alert_response_rule_template VALUES (28, 91); +INSERT INTO public.utm_alert_response_rule_template VALUES (37, 91); +INSERT INTO public.utm_alert_response_rule_template VALUES (33, 81); +INSERT INTO public.utm_alert_response_rule_template VALUES (31, 81); +INSERT INTO public.utm_alert_response_rule_template VALUES (34, 81); +INSERT INTO public.utm_alert_response_rule_template VALUES (36, 81); +INSERT INTO public.utm_alert_response_rule_template VALUES (32, 78); +INSERT INTO public.utm_alert_response_rule_template VALUES (31, 78); +INSERT INTO public.utm_alert_response_rule_template VALUES (33, 75); +INSERT INTO public.utm_alert_response_rule_template VALUES (31, 75); +INSERT INTO public.utm_alert_response_rule_template VALUES (36, 75); +INSERT INTO public.utm_alert_response_rule_template VALUES (29, 73); +INSERT INTO public.utm_alert_response_rule_template VALUES (48, 73); +INSERT INTO public.utm_alert_response_rule_template VALUES (41, 43); +INSERT INTO public.utm_alert_response_rule_template VALUES (29, 70); +INSERT INTO public.utm_alert_response_rule_template VALUES (48, 70); +INSERT INTO public.utm_alert_response_rule_template VALUES (44, 70); +INSERT INTO public.utm_alert_response_rule_template VALUES (30, 72); +INSERT INTO public.utm_alert_response_rule_template VALUES (47, 64); +INSERT INTO public.utm_alert_response_rule_template VALUES (35, 16); +INSERT INTO public.utm_alert_response_rule_template VALUES (42, 47); +INSERT INTO public.utm_alert_response_rule_template VALUES (45, 54); +INSERT INTO public.utm_alert_response_rule_template VALUES (43, 59); + diff --git a/backend/src/main/resources/config/liquibase/data/utm_response_action_template.sql b/backend/src/main/resources/config/liquibase/data/utm_response_action_template.sql new file mode 100644 index 000000000..a0d30cba2 --- /dev/null +++ b/backend/src/main/resources/config/liquibase/data/utm_response_action_template.sql @@ -0,0 +1,79 @@ + + +INSERT INTO public.utm_response_action_template VALUES (32, 'Enable Windows Defender', 'Enables real-time protection in Windows Defender.', '"%ProgramFiles%\Windows Defender\MpCmdRun.exe" -Enable', true); +INSERT INTO public.utm_response_action_template VALUES (15, 'Disable Compromised User Account', 'Disable the user account associated with the anomalous process execution', 'usermod --expiredate 1 $(target.user)', true); +INSERT INTO public.utm_response_action_template VALUES (16, 'Linux Debian: Block Attacker IP via Firewall', 'Adds a firewall rule on the Linux host to block all incoming connections from the attacker''s IP address (extracted from the alert''s adversary.ip field) to prevent further access.', 'ufw deny from $(adversary.ip) to any', true); +INSERT INTO public.utm_response_action_template VALUES (112, 'Macos: Delete file', 'Delete file', 'rm -f $(adversary.path)', true); +INSERT INTO public.utm_response_action_template VALUES (113, 'Windows: Block server outbound network access', 'Block server outbound network access', 'powershell netsh advfirewall firewall add rule name="Block-Outbound-$(adversary.ip)" dir=out action=block remoteip="$(adversary.ip)"', true); +INSERT INTO public.utm_response_action_template VALUES (33, 'Enable Windows Defender', 'Enables real-time protection in Windows Defender.', '"powershell.exe -Command \"Set-MpPreference -DisableRealtimeMonitoring $false\""', true); +INSERT INTO public.utm_response_action_template VALUES (35, 'Enable Firewall Rules', 'Reactivates the firewall rules that were previously disabled to restore system protection.', 'netsh advfirewall set allprofiles state on +', true); +INSERT INTO public.utm_response_action_template VALUES (36, 'Remove Folder Exclusion', 'Removes folder or file exclusions added via PowerShell.', '''powershell.exe -Command "Remove-MpPreference -ExclusionPath \"$(log.message | regex_extract -pattern ''''-ExclusionPath\s+''''(.+?)'''')\""''', true); +INSERT INTO public.utm_response_action_template VALUES (37, 'Remove Process Exclusion', 'Removes process exclusions added via PowerShell.', '''powershell.exe -Command "Remove-MpPreference -ExclusionProcess \"$(log.message | regex_extract -pattern ''''-ExclusionProcess\s+''''(.+?)'''')\""''', true); +INSERT INTO public.utm_response_action_template VALUES (38, 'Remove Folder Exclusion', 'Removes folder or file exclusions added via PowerShell.', 'powershell.exe -Command "Remove-MpPreference -ExclusionPath \"$(log.message | regex_extract -pattern ''''-ExclusionPath\s+''''(.+?)'''')\""', true); +INSERT INTO public.utm_response_action_template VALUES (39, 'Remove Process Exclusion', 'Removes process exclusions added via PowerShell.', 'powershell.exe -Command "Remove-MpPreference -ExclusionProcess \"$(log.message | regex_extract -pattern ''''-ExclusionProcess\s+''''(.+?)'''')\""', true); +INSERT INTO public.utm_response_action_template VALUES (40, 'Remove Folder Exclusion', 'Removes folder or file exclusions added via PowerShell.', 'powershell.exe -Command Remove-MpPreference -ExclusionPath $(log.message | regex_extract -pattern ''-ExclusionPath\s+(.+?)'') +', true); +INSERT INTO public.utm_response_action_template VALUES (41, 'Remove Process Exclusion', 'Removes process exclusions added via PowerShell.', 'powershell.exe -Command Remove-MpPreference -ExclusionProcess $(log.message | regex_extract -pattern ''-ExclusionProcess\s+(.+?)'') +', true); +INSERT INTO public.utm_response_action_template VALUES (42, 'Delete Empire Artifact File', 'Deletes the malicious file (extracted from the alert''s target.file field) associated with PowerShell Empire activity from the compromised system.', 'Remove-Item -LiteralPath $(target.file) -Force -Recurse', true); +INSERT INTO public.utm_response_action_template VALUES (44, 'Kill suspicious keylogging process by PID', 'Terminate the process using the detected PID in log.winlogEventDataProcessId', 'taskkill /F /PID $(log.winlogEventDataProcessId)', true); +INSERT INTO public.utm_response_action_template VALUES (45, 'Kill suspicious keylogging process by name', 'Terminate the process using the detected process name in log.winlogEventDataProcessName', 'taskkill /F /IM $(log.winlogEventDataProcessName)', true); +INSERT INTO public.utm_response_action_template VALUES (48, 'Restore Real-time Monitoring', 'Re-enables real-time monitoring for Windows Defender.', 'powershell Set-MpPreference -DisableRealtimeMonitoring $(if ("$(log.message)" -match ''-DisableRealtimeMonitoring'') { $false } else { $true })', true); +INSERT INTO public.utm_response_action_template VALUES (49, 'Restore Behavior Monitoring', 'Re-enables behavior monitoring for Windows Defender.', 'powershell Set-MpPreference -DisableBehaviorMonitoring $(if ("$(log.message)" -match ''-DisableBehaviorMonitoring'') { $false } else { $true })', true); +INSERT INTO public.utm_response_action_template VALUES (50, 'Restore PUA Protection', 'Re-enables protection against Potentially Unwanted Applications ', 'powershell Set-MpPreference -PUAProtection $(if ("$(log.message)" -match ''-PUAProtection'') { ''"Enabled"'' } else { ''"Disabled"'' })', true); +INSERT INTO public.utm_response_action_template VALUES (51, 'Restore MAPS Reporting', 'Re-enables the MAPS (Microsoft Active Protection Service) reporting, which sends malware and suspicious file information to Microsoft for analysis.', 'powershell Set-MpPreference -MAPSReporting $(if ("$(log.message)" -match ''-MAPSReporting'') { 1 } else { 0 })', true); +INSERT INTO public.utm_response_action_template VALUES (52, 'Restore Cloud-based Protection', 'Re-enables cloud-based protection for Windows Defender.', 'powershell Set-MpPreference -CloudProtection $(if ("$(log.message)" -match ''CloudProtection'') { 1 } else { 0 })', true); +INSERT INTO public.utm_response_action_template VALUES (53, 'Remove Process Exclusion', 'Removes a process exclusion that was added to Windows Defender.', 'powershell Remove-MpPreference -ExclusionProcess $(if ("$(log.message)" -match ''-ExclusionProcess'') { ''"evil.exe"'' } else { ''"Not A Process"'' })', true); +INSERT INTO public.utm_response_action_template VALUES (58, 'Remove folder and process exclusion added via powershell', 'Remove folder and process exclusion added via powershell', 'if ("$(log.message)" -match ''-ExclusionProcess\s+([^\s]+)'') { Remove-MpPreference -ExclusionProcess $matches[1] }; if ("$(log.message)" -match ''-ExclusionPath\s+([^\s]+)'') { Remove-MpPreference -ExclusionPath $matches[1] }', true); +INSERT INTO public.utm_response_action_template VALUES (59, 'Remove exclusions via added powershell', 'Remove exclusions via added powershell', 'powershell if ("$(log.message)" -match ''-ExclusionProcess\s+(\S+)'') { Remove-MpPreference -ExclusionProcess $matches[1] }; if ("$(log.message)" -match ''-ExclusionPath\s+(\S+)'') { Remove-MpPreference -ExclusionPath $matches[1] }', true); +INSERT INTO public.utm_response_action_template VALUES (60, 'Disable RDP and remove rule in FW', 'Disable RDP and remove rule in FW', 'powershell Set-ItemProperty -Path ''HKLM:\System\CurrentControlSet\Control\Terminal Server'' -Name ''fDenyTSConnections'' -Value 1 -Force;if ("$(log.message)" -match ''name=([^''''"\s]+)'') { Remove-NetFirewallRule -DisplayName $matches[1] -ErrorAction SilentlyContinue }', true); +INSERT INTO public.utm_response_action_template VALUES (43, 'Windows Defender: Enable Windows Defender protection', 'Enable Windows Defender protection', 'powershell Set-MpPreference -DisableRealtimeMonitoring $false +', true); +INSERT INTO public.utm_response_action_template VALUES (115, 'Macos: Block server outbound network access', 'Block server outbound network access', 'echo "block out from any to $(adversary.ip)" | pfctl -f -', true); +INSERT INTO public.utm_response_action_template VALUES (114, 'Linux: Block server outbound network access', 'Block server outbound network access', 'iptables -A OUTPUT -d $(adversary.ip) -j DROP', true); +INSERT INTO public.utm_response_action_template VALUES (127, 'Macos: Disable adversary.user', 'Disable adversary.user', 'chsh -s /usr/bin/false $(adversary.user)', true); +INSERT INTO public.utm_response_action_template VALUES (96, 'Linux: Disable target.user', 'Disable target.user', 'usermod -s /sbin/nologin $(target.user)', true); +INSERT INTO public.utm_response_action_template VALUES (97, 'Macos: Disable target.user', 'Disable target.user', 'chsh -s /usr/bin/false $(target.user)', true); +INSERT INTO public.utm_response_action_template VALUES (67, 'Windows: Block Adversary IP', 'Block Adversary IP', 'powershell netsh advfirewall firewall add rule name="Block-Brute-Force-$(adversary.ip)" dir=in action=block remoteip="$(adversary.ip)" enable=yes', true); +INSERT INTO public.utm_response_action_template VALUES (70, 'Windows: Kill process', 'Kill process', 'powershell taskkill /F /IM (Split-Path -Path "$(adversary.process)" -Leaf)', true); +INSERT INTO public.utm_response_action_template VALUES (72, 'Windows: Delete file', 'Delete file', 'powershell Remove-Item -LiteralPath "$(adversary.path)" -Force -Recurse', true); +INSERT INTO public.utm_response_action_template VALUES (95, 'Windows: Disable target.user', 'Disable target.user', 'powershell net user $(target.user) /active:no', true); +INSERT INTO public.utm_response_action_template VALUES (110, 'Macos: Uninstall application', 'Uninstall application', 'brew uninstall --force "$(target.applicationname)" 2>/dev/null || find /Applications -iname "$(target.applicationname).app" -maxdepth 2 -type d -exec rm -rf {} + 2>/dev/null', true); +INSERT INTO public.utm_response_action_template VALUES (108, 'Linux Debian: Uninstall application', 'Uninstall application', 'apt-get remove -y "$(dpkg-query -W -f=''${Package}\n'' | grep -xi "^$(target.applicationname)$" | head -1)"', true); +INSERT INTO public.utm_response_action_template VALUES (64, 'Windows: Disable RDP and disable FW rule', 'Disable RDP and disable FW rule', 'powershell Set-ItemProperty -Path ''HKLM:\System\CurrentControlSet\Control\Terminal Server'' -Name ''fDenyTSConnections'' -Value 1 -Force; if ("$(log.message)" -match ''name=\"(.+?)\"'') { Remove-NetFirewallRule -DisplayName $matches[1] -ErrorAction SilentlyContinue }', true); +INSERT INTO public.utm_response_action_template VALUES (103, 'Windows: Stop Service', 'Stop Service', 'powershell Stop-Service -Name "$(adversary.windowsServiceDisplayName )" -Force ', true); +INSERT INTO public.utm_response_action_template VALUES (92, 'Windows: Isolate host ', 'Isolate host ', 'powershell Get-NetAdapter ^| Disable-NetAdapter -Confirm:$false', true); +INSERT INTO public.utm_response_action_template VALUES (106, 'Windows: Uninstall application', 'Uninstall application', 'powershell ForEach-Object {Start-Process -FilePath ($_.UninstallString -replace ''"([^"]*)".*'', ''$1'') -ArgumentList "/S" -Wait} -InputObject (Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*, HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {$_.DisplayName -like "*$($(target.applicactionname))*"})', true); +INSERT INTO public.utm_response_action_template VALUES (100, 'Linux OpenSUSE: Block Adversary IP', 'Block Adversary IP', 'firewall-cmd --zone=public --add-rich-rule=''rule family="ipv4" source address="$(adversary.ip)" drop'' --permanent && firewall-cmd --reload', true); +INSERT INTO public.utm_response_action_template VALUES (107, 'Linux RHEL: Uninstall application', 'Uninstall application', ' yum remove -y "$(rpm -qa --queryformat ''%{NAME}\n'' | grep -xi "^$(target.applicationname)$" | head -1)"', true); +INSERT INTO public.utm_response_action_template VALUES (102, 'Macos: Kill process', 'Kill process', 'pkill -9 $(basename "$(adversary.process)")', true); +INSERT INTO public.utm_response_action_template VALUES (94, 'Macos: Isolate host', 'Isolate host', 'for interface in $(networksetup -listallnetworkservices | grep -v "aster"); do networksetup -setnetworkserviceenabled "$interface" off; done', true); +INSERT INTO public.utm_response_action_template VALUES (104, 'Linux: Stop Service', 'Stop Service', 'systemctl stop "$(adversary.service)"', true); +INSERT INTO public.utm_response_action_template VALUES (93, 'Linux: Isolate host', 'Isolate host', 'for interface in $(ip link show | grep -E ''^[0-9]+:'' | grep -v ''lo:'' | awk -F: ''{print $2}'' | tr -d '' ''); do ip link set $interface down; done', true); +INSERT INTO public.utm_response_action_template VALUES (99, 'Linux Debian: Block Adversary IP', 'Block Adversary IP', 'iptables -A INPUT -s $(adversary.ip) -j DROP', true); +INSERT INTO public.utm_response_action_template VALUES (105, 'Macos: Stop Service', 'Stop Service', 'launchctl stop "$(adversary.service)"', true); +INSERT INTO public.utm_response_action_template VALUES (109, 'Linux OpenSUSE: Uninstall application', 'Uninstall application', 'zypper remove -y "$(rpm -qa --queryformat ''%{NAME}\n'' | grep -xi "^$(target.applicationname)$" | head -1)"', true); +INSERT INTO public.utm_response_action_template VALUES (101, 'Linux: Kill process', 'Kill process', 'pkill -9 $(basename "$(adversary.process)")', true); +INSERT INTO public.utm_response_action_template VALUES (54, 'Windows Defender: Auto Reenable Protections', 'Restore disabled settings', 'powershell if ("$(log.message)" -match ''DisableRealtimeMonitoring'') { Set-MpPreference -DisableRealtimeMonitoring $false }; if ("$(log.message)" -match ''DisableBehaviorMonitoring'') { Set-MpPreference -DisableBehaviorMonitoring $false }; if ("$(log.message)" -match ''DisableIOAVProtection'') { Set-MpPreference -DisableIOAVProtection $false }; if ("$(log.message)" -match ''DisableScriptScanning'') { Set-MpPreference -DisableScriptScanning $false }; if ("$(log.message)" -match ''DisableCloudProtection'') { Set-MpPreference -EnableNetworkProtection Enabled; Set-MpPreference -MAPSReporting Advanced }; if ("$(log.message)" -match ''Set-MpPreference\s+-ExclusionProcess\s+"?([a-zA-Z0-9\._-]+)"?'') { Remove-MpPreference -ExclusionProcess $matches[1] }', true); +INSERT INTO public.utm_response_action_template VALUES (118, 'Linux Debian: Block server inbound network access', 'Block server inbound network access', 'iptables -A INPUT -s $(adversary.ip) -j DROP', true); +INSERT INTO public.utm_response_action_template VALUES (47, 'Windows: Enable firewall rules previously disabled', 'Reactivates the firewall rules that were previously disabled to restore system protection.', 'powershell netsh advfirewall firewall set rule name=all new enable=yes', true); +INSERT INTO public.utm_response_action_template VALUES (91, 'Windows: Disable adversary.user', 'Disable adversary.user', 'powershell net user $(adversary.user) /active:no', true); +INSERT INTO public.utm_response_action_template VALUES (81, 'Linux: Disable adversary.user', 'Disable adversary.user', 'usermod -s /sbin/nologin $(adversary.user)', true); +INSERT INTO public.utm_response_action_template VALUES (78, 'Linux: Shutdown OS forced', 'Shutdown OS forced', 'shutdown -h now', true); +INSERT INTO public.utm_response_action_template VALUES (75, 'Linux: Kill session and logout user', 'Kill session and logout user', 'pkill -KILL -u $(target.user)', true); +INSERT INTO public.utm_response_action_template VALUES (73, 'Windows: Kill session and logout user', 'Kill session and logout user', 'logoff $(target.user)', true); +INSERT INTO public.utm_response_action_template VALUES (116, 'Windows: Block server inbound network access', 'Block server inbound network access', 'powershell netsh advfirewall firewall add rule name="Block-Inbound-$(adversary.ip)" dir=in action=block remoteip="$(adversary.ip)"', true); +INSERT INTO public.utm_response_action_template VALUES (121, 'Windows: Remove all permissions for a user', 'Remove all permissions for a user', 'powershell Get-LocalGroup ^| Where-Object { $_.Name -ne "Users" } ^| ForEach-Object { Remove-LocalGroupMember -Group $_.Name -Member "$(adversary.user)" -ErrorAction SilentlyContinue }', true); +INSERT INTO public.utm_response_action_template VALUES (120, 'Macos: Block server inbound network access', 'Block server inbound network access', 'echo "block in from $(adversary.ip) to any" | pfctl -f -', true); +INSERT INTO public.utm_response_action_template VALUES (126, 'Macos: Shutdown OS forced', 'Shutdown OS forced', 'shutdown -h now', true); +INSERT INTO public.utm_response_action_template VALUES (117, 'Linux RHEL: Block server inbound network access', 'Block server inbound network access', 'firewall-cmd --zone=public --add-rich-rule=''rule family="ipv4" source address="$(adversary.ip)" drop'' --permanent && firewall-cmd --reload', true); +INSERT INTO public.utm_response_action_template VALUES (128, 'Windows: Disable RPD', 'Disable RDP', 'powershell Set-ItemProperty -Path ''HKLM:\System\CurrentControlSet\Control\Terminal Server'' -Name ''fDenyTSConnections'' -Value 1 -Force', true); +INSERT INTO public.utm_response_action_template VALUES (111, 'Linux: Delete file', 'Delete file', 'rm -f $(adversary.path)', true); +INSERT INTO public.utm_response_action_template VALUES (98, 'Linux RHEL: Block Adversary IP', 'Block Adversary IP', 'firewall-cmd --zone=public --add-rich-rule=''rule family="ipv4" source address="$(adversary.ip)" drop'' --permanent && firewall-cmd --reload', true); +INSERT INTO public.utm_response_action_template VALUES (122, 'Linux: Remove all permissions for a user', 'Remove all permissions for a user', 'for grp in $(id -nG $(adversary.user) | tr '' '' ''\n'' | grep -v "^$(adversary.user)$"); do gpasswd -d $(adversary.user) "$grp"; done', true); +INSERT INTO public.utm_response_action_template VALUES (119, 'Linux OpenSUSE: Block server inbound network access', 'Block server inbound network access', 'firewall-cmd --zone=public --add-rich-rule=''rule family="ipv4" source address="$(adversary.ip)" drop'' --permanent && firewall-cmd --reload', true); +INSERT INTO public.utm_response_action_template VALUES (124, 'Macos: Kill session and logout user', 'Kill session and logout user', 'pkill -KILL -u $(target.user)', true); +INSERT INTO public.utm_response_action_template VALUES (123, 'Macos: Remove all permissions for a user', 'Remove all permissions for a user', 'for grp in $(id -nG $(adversary.user) | tr '' '' ''\n'' | grep -v -E "^($(adversary.user)|staff|everyone)$"); do dseditgroup -o edit -d $(adversary.user) -t user "$grp" 2>/dev/null; done', true); +INSERT INTO public.utm_response_action_template VALUES (125, 'Windows: Shutdown OS forced', 'Shutdown OS forced', 'shutdown /s /f /t 0', true); + diff --git a/backend/src/main/resources/config/liquibase/master.xml b/backend/src/main/resources/config/liquibase/master.xml index 9d0c56aac..08d72af53 100644 --- a/backend/src/main/resources/config/liquibase/master.xml +++ b/backend/src/main/resources/config/liquibase/master.xml @@ -251,5 +251,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml index 4aa548af3..cfb304983 100644 --- a/backend/src/main/resources/logback-spring.xml +++ b/backend/src/main/resources/logback-spring.xml @@ -2,30 +2,38 @@ - - - - + + + + timestamp + + + severity + + + msg + + + + + - - 512 - - - - - + + ---> + + @@ -41,8 +49,8 @@ - - + + @@ -55,6 +63,13 @@ + + + + + + + diff --git a/backend/src/main/resources/templates/reports/customs/threatActivityForAlerts.html b/backend/src/main/resources/templates/reports/customs/threatActivityForAlerts.html index d280bc8bf..30f3b573a 100644 --- a/backend/src/main/resources/templates/reports/customs/threatActivityForAlerts.html +++ b/backend/src/main/resources/templates/reports/customs/threatActivityForAlerts.html @@ -14,7 +14,7 @@

STACK

- +
diff --git a/filters/utmstack/utmstack.yml b/filters/utmstack/utmstack.yml new file mode 100644 index 000000000..b1819e459 --- /dev/null +++ b/filters/utmstack/utmstack.yml @@ -0,0 +1,8 @@ +# UTMStack filter, version 1.0.0 + +pipeline: + - dataTypes: + - utmstack + steps: + - json: + source: raw \ No newline at end of file diff --git a/frontend/src/app/active-directory/active-directory.component.html b/frontend/src/app/active-directory/active-directory.component.html index 4249c341b..7e1da3779 100644 --- a/frontend/src/app/active-directory/active-directory.component.html +++ b/frontend/src/app/active-directory/active-directory.component.html @@ -1,5 +1,5 @@ -
-
+
+
Users with Activity
diff --git a/frontend/src/app/active-directory/active-directory.component.scss b/frontend/src/app/active-directory/active-directory.component.scss index 4ec090ce7..5eab083ef 100644 --- a/frontend/src/app/active-directory/active-directory.component.scss +++ b/frontend/src/app/active-directory/active-directory.component.scss @@ -2,6 +2,14 @@ @import "../../assets/styles/var"; @import "../../assets/styles/custom-elements"; +:host { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + height: 100%; +} + .vul-container { .tab-header { .nav { diff --git a/frontend/src/app/active-directory/shared/components/active-directory-detail/active-directory-detail.component.html b/frontend/src/app/active-directory/shared/components/active-directory-detail/active-directory-detail.component.html index c0d7be74b..33524eb3b 100644 --- a/frontend/src/app/active-directory/shared/components/active-directory-detail/active-directory-detail.component.html +++ b/frontend/src/app/active-directory/shared/components/active-directory-detail/active-directory-detail.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/frontend/src/app/active-directory/shared/components/active-directory-detail/active-directory-detail.component.scss b/frontend/src/app/active-directory/shared/components/active-directory-detail/active-directory-detail.component.scss index f965edb4c..ffaa19f4f 100644 --- a/frontend/src/app/active-directory/shared/components/active-directory-detail/active-directory-detail.component.scss +++ b/frontend/src/app/active-directory/shared/components/active-directory-detail/active-directory-detail.component.scss @@ -1,3 +1,11 @@ +:host { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + height: 100%; +} + .message-container { } diff --git a/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.html b/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.html index 17a7d7816..d3c348721 100644 --- a/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.html +++ b/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.html @@ -1,17 +1,19 @@ -
+
-
+
-
-
+
+
Event detail
-
-

+
+
+

+
diff --git a/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.scss b/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.scss index fc643059c..5a0a07cad 100644 --- a/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.scss +++ b/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.scss @@ -1,10 +1,7 @@ -.message-container { - overflow-y: auto; - height: 70vh; -} - -.card-detail { +:host { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; height: 100%; - box-sizing: border-box; - display: block } diff --git a/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.ts b/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.ts index 294f0db6f..fffe3a936 100644 --- a/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.ts +++ b/frontend/src/app/active-directory/shared/components/active-directory-event/active-directory-event.component.ts @@ -1,7 +1,7 @@ import {Component, Input, OnInit} from '@angular/core'; +import {Event} from '../../../../shared/types/event/event'; import {TimeFilterType} from '../../../../shared/types/time-filter.type'; import {TreeObjectBehavior} from '../../behavior/tree-object.behvior'; -import {WinlogbeatEventType} from '../../types/winlogbeat-event.type'; @Component({ selector: 'app-active-directory-event', @@ -13,7 +13,7 @@ export class AdEventComponent implements OnInit { @Input() eventsFilter: string[]; @Input() time: TimeFilterType; message: string; - event: WinlogbeatEventType; + event: Event; constructor(private treeObjectBehavior: TreeObjectBehavior) { } @@ -29,8 +29,8 @@ export class AdEventComponent implements OnInit { return msg; } - onEventChange($event: WinlogbeatEventType) { + onEventChange($event: Event) { this.event = $event; - this.message = this.event ? this.replaceDetail($event.logx.wineventlog.message) : ''; + this.message = this.event ? this.replaceDetail(this.event.log.message) : ''; } } diff --git a/frontend/src/app/active-directory/shared/components/event-timeline/event-timeline.component.html b/frontend/src/app/active-directory/shared/components/event-timeline/event-timeline.component.html index 099fd49cd..cb6aa2e08 100644 --- a/frontend/src/app/active-directory/shared/components/event-timeline/event-timeline.component.html +++ b/frontend/src/app/active-directory/shared/components/event-timeline/event-timeline.component.html @@ -18,10 +18,10 @@
Events timeline
-
-

+

@@ -36,7 +36,7 @@
Events timeline
No more events
-
+
(); objectId: ActiveDirectoryTreeType; sevenDaysRange: ElasticFilterCommonType = {time: ElasticTimeEnum.DAY, last: 7, label: 'last 7 days'}; - items: WinlogbeatEventType[] = []; + items: Event[] = []; loadingMore = false; totalItems: any; page = 1; @@ -133,13 +134,14 @@ export class EventTimelineComponent implements OnInit, AfterViewInit { } - selectEvent(item: WinlogbeatEventType) { + selectEvent(item: Event) { this.itemSelected = this.getUniqueEventId(item); this.eventChange.emit(item); } - getUniqueEventId(item: WinlogbeatEventType) { - return item.id + '-' + item.logx.wineventlog.eventId + '-' + new Date(item.timestamp).getTime(); + getUniqueEventId(item: Event) { + const eventCode = item.log && item.log.eventCode; + return item.id + '-' + eventCode + '-' + new Date(item.timestamp).getTime(); } onFilterTimeChange($event: TimeFilterType) { diff --git a/frontend/src/app/active-directory/shared/services/winlogbeat.service.ts b/frontend/src/app/active-directory/shared/services/winlogbeat.service.ts index 98b263e7d..1c6290b90 100644 --- a/frontend/src/app/active-directory/shared/services/winlogbeat.service.ts +++ b/frontend/src/app/active-directory/shared/services/winlogbeat.service.ts @@ -2,8 +2,8 @@ import {HttpClient, HttpResponse} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {Observable} from 'rxjs'; import {SERVER_API_URL} from '../../../app.constants'; +import {Event} from '../../../shared/types/event/event'; import {createRequestOption} from '../../../shared/util/request-util'; -import {WinlogbeatEventType} from '../types/winlogbeat-event.type'; @Injectable({ @@ -15,9 +15,9 @@ export class WinlogbeatService { constructor(private http: HttpClient) { } - query(req?: any): Observable> { + query(req?: any): Observable> { const options = createRequestOption(req); - return this.http.get(this.resourceUrl, {params: options, observe: 'response'}); + return this.http.get(this.resourceUrl, {params: options, observe: 'response'}); } } diff --git a/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.html b/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.html index 990a83831..d705eacb5 100644 --- a/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.html +++ b/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.html @@ -1,4 +1,4 @@ -
+
{{object.name}}
-
+
diff --git a/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.scss b/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.scss index 96500ce77..ef5ecfaac 100644 --- a/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.scss +++ b/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.scss @@ -2,15 +2,15 @@ @import "../../../../assets/styles/var"; @import "../../../../assets/styles/custom-elements"; -.active-directory-tree { - +:host { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + height: 100%; } -.active-directory-tree { - //max-height: 90vh; - min-height: 82vh; - //height: 90vh; -} + .card-right-detail { .card-detail { diff --git a/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.ts b/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.ts index 580496bf0..f2101c1c6 100644 --- a/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.ts +++ b/frontend/src/app/active-directory/view/active-directory-view/active-directory-view.component.ts @@ -3,14 +3,12 @@ import {Router} from '@angular/router'; import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import {ResizeEvent} from 'angular-resizable-element'; import {Observable} from 'rxjs'; -import {tap} from 'rxjs/operators'; import {AdReportCreateComponent} from '../../reports/ad-report-create/ad-report-create.component'; import {TreeObjectBehavior} from '../../shared/behavior/tree-object.behvior'; import {resolveType} from '../../shared/functions/ad-util.function'; import {ActiveDirectoryService} from '../../shared/services/active-directory.service'; -import {ActiveDirectoryUsers} from '../../shared/types/active-directory-users'; +import {ActiveDirectoryTreeType} from '../../shared/types/active-directory-tree.type'; import {AdTrackerCreateComponent} from '../../tracker/ad-tracker-create/ad-tracker-create.component'; -import {ActiveDirectoryTreeType} from "../../shared/types/active-directory-tree.type"; @Component({ selector: 'app-active-directory-view', diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.html b/frontend/src/app/app-management/api-keys/api-keys.component.html new file mode 100644 index 000000000..e2a5158ff --- /dev/null +++ b/frontend/src/app/app-management/api-keys/api-keys.component.html @@ -0,0 +1,151 @@ +
+
+
+
+ API Keys +
+ + The API key is a simple encrypted string that identifies you in the application. With this key, you can access the REST API. + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Name + + Allowed IPs + + Expires At + + Created At + ACTIONS
{{ key.name }}{{ key.allowedIp?.join(', ') || 'โ€”' }} + + + + + + + + + + + + + {{ key.expiresAt ? (key.expiresAt | date:'dd/MM/yy HH:mm':'UTC') : 'โ€”' }} + {{ key.createdAt | date:'dd/MM/yy HH:mm' :'UTC' }} + + + +
+ +
+
+ + +
+
+
+ +
+
+ + + + +
+ +
+
+
+ + + + + +
+
+ Copy it now because it will be shown only once! +
+
+ {{ maskSecrets(generatedApiKey) }} + +
+
+ + {{ 'Keep this key safeKeep this key safe' }} +
+
+
+ +
+
diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.scss b/frontend/src/app/app-management/api-keys/api-keys.component.scss new file mode 100644 index 000000000..b3751c83a --- /dev/null +++ b/frontend/src/app/app-management/api-keys/api-keys.component.scss @@ -0,0 +1,7 @@ +:host{ + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + height: 100%; +} diff --git a/frontend/src/app/app-management/api-keys/api-keys.component.ts b/frontend/src/app/app-management/api-keys/api-keys.component.ts new file mode 100644 index 000000000..82d9e3450 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/api-keys.component.ts @@ -0,0 +1,232 @@ +import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core'; +import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; +import * as moment from 'moment'; +import {UtmToastService} from '../../shared/alert/utm-toast.service'; +import { + ModalConfirmationComponent +} from '../../shared/components/utm/util/modal-confirmation/modal-confirmation.component'; +import {ITEMS_PER_PAGE} from '../../shared/constants/pagination.constants'; +import {SortEvent} from '../../shared/directives/sortable/type/sort-event'; +import {ApiKeyModalComponent} from './shared/components/api-key-modal/api-key-modal.component'; +import {ApiKeyResponse} from './shared/models/ApiKeyResponse'; +import {ApiKeysService} from './shared/service/api-keys.service'; + +@Component({ + selector: 'app-api-keys', + templateUrl: './api-keys.component.html', + styleUrls: ['./api-keys.component.scss'] +}) +export class ApiKeysComponent implements OnInit { + + generating: string[] = []; + noData = false; + apiKeys: ApiKeyResponse[] = []; + loading = false; + generatedApiKey = ''; + @ViewChild('generatedModal') generatedModal!: TemplateRef; + generatedModalRef!: NgbModalRef; + copied = false; + readonly itemsPerPage = ITEMS_PER_PAGE; + totalItems = 0; + page = 0; + size = this.itemsPerPage; + + request = { + sort: 'createdAt,desc', + page: this.page, + size: this.size + }; + + constructor( private toastService: UtmToastService, + private apiKeyService: ApiKeysService, + private modalService: NgbModal + ) {} + + ngOnInit(): void { + this.loadKeys(); + } + + loadKeys(): void { + this.loading = true; + this.apiKeyService.list(this.request).subscribe({ + next: (res) => { + this.totalItems = Number(res.headers.get('X-Total-Count')); + this.apiKeys = res.body || []; + this.noData = this.apiKeys.length === 0; + this.loading = false; + }, + error: () => { + this.loading = false; + this.apiKeys = []; + } + }); + } + + copyToClipboard(): void { + if (!this.generatedApiKey) { return; } + + if (navigator && (navigator as any).clipboard && (navigator as any).clipboard.writeText) { + (navigator as any).clipboard.writeText(this.generatedApiKey) + .then(() => this.copied = true) + .catch(err => { + console.error('Error al copiar con clipboard API', err); + this.fallbackCopy(this.generatedApiKey); + }); + } else { + this.fallbackCopy(this.generatedApiKey); + } + } + + private fallbackCopy(text: string): void { + try { + const textarea = document.createElement('textarea'); + textarea.value = text; + + textarea.style.position = 'fixed'; + textarea.style.top = '0'; + textarea.style.left = '0'; + textarea.style.opacity = '0'; + + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + + const successful = document.execCommand('copy'); + document.body.removeChild(textarea); + + if (successful) { + this.showCopiedFeedback(); + } else { + console.warn('Fallback copy failed'); + } + } catch (err) { + console.error('Error en fallback copy', err); + } + } + + private showCopiedFeedback(): void { + this.copied = true; + setTimeout(() => this.copied = false, 2000); + } + + openCreateModal(): void { + const modalRef = this.modalService.open(ApiKeyModalComponent, { centered: true }); + + modalRef.result.then((key: ApiKeyResponse) => { + if (key) { + this.generateKey(key); + } + }); + } + + editKey(key: ApiKeyResponse): void { + const modalRef = this.modalService.open(ApiKeyModalComponent, {centered: true}); + modalRef.componentInstance.apiKey = key; + + modalRef.result.then((key: ApiKeyResponse) => { + if (key) { + this.generateKey(key); + } + }); + } + + deleteKey(apiKey: ApiKeyResponse): void { + const modalRef = this.modalService.open(ModalConfirmationComponent, {centered: true}); + modalRef.componentInstance.header = `Delete API Key: ${apiKey.name}`; + modalRef.componentInstance.message = 'Are you sure you want to delete this API key?'; + modalRef.componentInstance.confirmBtnType = 'delete'; + modalRef.componentInstance.type = 'danger'; + modalRef.componentInstance.confirmBtnText = 'Delete'; + modalRef.componentInstance.confirmBtnIcon = 'icon-cross-circle'; + + modalRef.result.then(reason => { + if (reason === 'ok') { + this.delete(apiKey); + } + }); + } + + delete(apiKey: ApiKeyResponse): void { + this.apiKeyService.delete(apiKey.id).subscribe({ + next: () => { + this.toastService.showSuccess('API key deleted successfully.'); + this.loadKeys(); + }, + error: (err) => { + this.toastService.showError('Error', 'An error occurred while deleting the API key.'); + throw err; + } + }); + } + + getDaysUntilExpire(expiresAt: string): number { + if (!expiresAt) { + return -1; + } + + const today = moment().startOf('day'); + const expireDate = moment(expiresAt).startOf('day'); + return expireDate.diff(today, 'days'); + } + + onSortBy($event: SortEvent) { + this.request.sort = $event.column + ',' + $event.direction; + this.loadKeys(); + } + + maskSecrets(str: string): string { + if (!str || str.length <= 10) { + return str; + } + const prefix = str.substring(0, 10); + const maskLength = str.length - 30; + const maskedPart = '*'.repeat(maskLength); + return prefix + maskedPart; + } + + generateKey(apiKey: ApiKeyResponse): void { + this.generating.push(apiKey.id); + this.apiKeyService.generateApiKey(apiKey.id).subscribe(response => { + this.generatedApiKey = response.body ? response.body : ""; + this.generatedModalRef = this.modalService.open(this.generatedModal, {centered: true}); + const index = this.generating.indexOf(apiKey.id); + if (index > -1) { + this.generating.splice(index, 1); + } + this.loadKeys(); + }); + } + + isApiKeyExpired(expiresAt?: string | null ): boolean { + if (!expiresAt) { + return false; + } + const expirationTime = new Date(expiresAt).getTime(); + return expirationTime < Date.now(); + } + + close() { + this.generatedModalRef.close(); + this.copied = false; + this.generatedApiKey = ''; + } + + loadPage($event: number) { + this.page = $event - 1; + this.request = { + ...this.request, + page: this.page + }; + this.loadKeys(); + } + + onItemsPerPageChange($event: number) { + this.request = { + ...this.request, + size: $event, + page: 0 + }; + this.page = 0; + this.loadKeys(); + } +} diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html new file mode 100644 index 000000000..7e1399bbb --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.html @@ -0,0 +1,127 @@ + + +
+ +
+ {{ errorMsg }} +
+ +
+
+ + +
+ +
+ +
+ +
+ +
+
+
+ +
+ + +
+ + +
+ +
+ {{ ipInputError }} +
+ +
    +
  • + +
    +
    + + {{ ip.value }} + {{ getIpType(ip.value) }} +
    +
    + +
    +
    +
  • +
+
+ + +
+ +
+ + + +
+
+ + + diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss new file mode 100644 index 000000000..e31427b61 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.scss @@ -0,0 +1,12 @@ +.disabled-rounded-start { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} +.disabled-rounded-end { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.mt-4 { + margin-top: 9rem !important; +} diff --git a/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts new file mode 100644 index 000000000..f2378c13d --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/components/api-key-modal/api-key-modal.component.ts @@ -0,0 +1,139 @@ +import {HttpErrorResponse} from '@angular/common/http'; +import {Component, Input, OnInit} from '@angular/core'; +import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import {IpFormsValidators} from '../../../../../rule-management/app-rule/validators/ip.forms.validators'; +import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; +import {ApiKeyResponse} from '../../models/ApiKeyResponse'; +import { ApiKeysService } from '../../service/api-keys.service'; + +@Component({ + selector: 'app-api-key-modal', + templateUrl: './api-key-modal.component.html', + styleUrls: ['./api-key-modal.component.scss'] +}) +export class ApiKeyModalComponent implements OnInit { + + @Input() apiKey: ApiKeyResponse = null; + + apiKeyForm: FormGroup; + ipInput = ''; + loading = false; + errorMsg = ''; + isSaving: string | string[] | Set | { [p: string]: any }; + minDate = { year: new Date().getFullYear(), month: new Date().getMonth() + 1, day: new Date().getDate() }; + ipInputError = ''; + + constructor( public activeModal: NgbActiveModal, + private apiKeyService: ApiKeysService, + private fb: FormBuilder, + private toastService: UtmToastService) { + } + + ngOnInit(): void { + + const expiresAtDate = this.apiKey && this.apiKey.expiresAt ? new Date(this.apiKey.expiresAt) : null; + const expiresAtNgbDate = expiresAtDate ? { + year: expiresAtDate.getUTCFullYear(), + month: expiresAtDate.getUTCMonth() + 1, + day: expiresAtDate.getUTCDate() + } : null; + + this.apiKeyForm = this.fb.group({ + name: [ this.apiKey ? this.apiKey.name : '', Validators.required], + allowedIp: this.fb.array(this.apiKey ? this.apiKey.allowedIp : []), + expiresAt: [expiresAtNgbDate, Validators.required], + }); + } + + get allowedIp(): FormArray { + return this.apiKeyForm.get('allowedIp') as FormArray; + } + + addIp(): void { + const trimmedIp = this.ipInput.trim(); + + if (!trimmedIp) { + this.ipInputError = 'Please enter an IP address or CIDR'; // Error is assigned + return; + } + + const tempControl = this.fb.control(trimmedIp, [IpFormsValidators.ipOrCidr()]); + + if (tempControl.invalid) { + if (tempControl.hasError('invalidIp')) { + this.ipInputError = 'Invalid IP address format'; + } else if (tempControl.hasError('invalidCidr')) { + this.ipInputError = 'Invalid CIDR format'; + } + return; + } + + const isDuplicate = this.allowedIp.controls.some( + control => control.value === trimmedIp + ); + + if (isDuplicate) { + this.ipInputError = 'This IP is already added'; + return; + } + + this.allowedIp.push(this.fb.control(trimmedIp, [IpFormsValidators.ipOrCidr()])); + this.ipInput = ''; + this.ipInputError = ''; + } + + removeIp(index: number): void { + this.allowedIp.removeAt(index); + } + + create(): void { + this.errorMsg = ''; + + if (this.apiKeyForm.invalid) { + this.errorMsg = 'Name is required.'; + return; + } + + this.loading = true; + + const rawDate = this.apiKeyForm.get('expiresAt').value; + let formattedDate = rawDate; + + if (rawDate && typeof rawDate === 'object') { + formattedDate = `${rawDate.year}-${String(rawDate.month).padStart(2, '0')}-${String(rawDate.day).padStart(2, '0')}T00:00:00.000Z`; + } + + const payload = { + ...this.apiKeyForm.value, + expiresAt: formattedDate, + }; + + const save = this.apiKey ? this.apiKeyService.update(this.apiKey.id, payload) : + this.apiKeyService.create(payload); + + save.subscribe({ + next: (response) => { + this.loading = false; + this.activeModal.close(response.body as ApiKeyResponse); + }, + error: (err: HttpErrorResponse) => { + this.loading = false; + if (err.status === 409) { + this.toastService.showError('Error', 'An API key with this name already exists.'); + } else if (err.status === 500) { + this.toastService.showError('Error', 'Server error occurred while creating the API key.'); + } + } + }); + } + + getIpType(value: string): string { + if (!value) { return ''; } + if (value.includes('/')) { + return value.includes(':') ? 'IPv6 CIDR' : 'IPv4 CIDR'; + } + return value.includes(':') ? 'IPv6' : 'IPv4'; + } +} + diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts new file mode 100644 index 000000000..3f6b890d7 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyResponse.ts @@ -0,0 +1,8 @@ +export interface ApiKeyResponse { + id: string; + name: string; + allowedIp: string[]; + createdAt: string; + expiresAt?: string; + generatedAt?: string; +} diff --git a/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts new file mode 100644 index 000000000..7ae630a43 --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/models/ApiKeyUpsert.ts @@ -0,0 +1,6 @@ +export interface ApiKeyUpsert { + id: string; + name: string; + allowedIp?: string[]; + expiresAt?: Date; +} diff --git a/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts new file mode 100644 index 000000000..e239d5e4d --- /dev/null +++ b/frontend/src/app/app-management/api-keys/shared/service/api-keys.service.ts @@ -0,0 +1,117 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { SERVER_API_URL } from '../../../../app.constants'; +import { ApiKeyResponse } from '../models/ApiKeyResponse'; +import { ApiKeyUpsert } from '../models/ApiKeyUpsert'; +import {createRequestOption} from "../../../../shared/util/request-util"; + +/** + * Service for managing API keys + */ +@Injectable({ + providedIn: 'root' +}) +export class ApiKeysService { + public resourceUrl = SERVER_API_URL + 'api/api-keys'; + + constructor(private http: HttpClient) {} + + /** + * Create a new API key + */ + create(dto: ApiKeyUpsert): Observable> { + return this.http.post( + this.resourceUrl, + dto, + { observe: 'response' } + ); + } + + /** + * Generate (or renew) a plain API key for the given id + * Returns the plain text key (only once) + */ + generate(id: string): Observable> { + return this.http.post( + `${this.resourceUrl}/${id}/generate`, + {}, + { observe: 'response', responseType: 'text' } + ); + } + + /** + * Get API key by id + */ + get(id: string): Observable> { + return this.http.get( + `${this.resourceUrl}/${id}`, + { observe: 'response' } + ); + } + + /** + * List all API keys (with optional pagination) + */ + list(params?: any): Observable> { + const httpParams = createRequestOption(params); + return this.http.get( + this.resourceUrl, + { observe: 'response', params: httpParams }, + ); + } + + /** + * Update an existing API key + */ + update(id: string, dto: ApiKeyUpsert): Observable> { + return this.http.put( + `${this.resourceUrl}/${id}`, + dto, + { observe: 'response' } + ); + } + + /** + * Delete API key + */ + delete(id: string): Observable> { + return this.http.delete( + `${this.resourceUrl}/${id}`, + { observe: 'response' } + ); + } + + generateApiKey(apiKeyId: string): Observable> { + return this.http.post(`${this.resourceUrl}/${apiKeyId}/generate`, null, { + observe: 'response', + responseType: 'text' + }); + } + + /** + * Search API key usage in Elasticsearch + */ + usage(params: { + filters?: any[]; + top: number; + indexPattern: string; + includeChildren?: boolean; + page?: number; + size?: number; + }): Observable { + return this.http.get( + `${this.resourceUrl}/usage`, + { + params: { + top: params.top.toString(), + indexPattern: params.indexPattern, + includeChildren: params.includeChildren.toString() || 'false', + page: params.page.toString() || '0', + size: params.size.toString() || '10' + } + } + ); + } +} + diff --git a/frontend/src/app/app-management/app-logs/app-logs.component.html b/frontend/src/app/app-management/app-logs/app-logs.component.html index 42cdece17..866834253 100644 --- a/frontend/src/app/app-management/app-logs/app-logs.component.html +++ b/frontend/src/app/app-management/app-logs/app-logs.component.html @@ -4,15 +4,15 @@
Application logs
- - --> + - + {{log["@timestamp"]| date:'medium':'UTC'}} - {{log.source}} + {{'PANEL ( '+ log.dataSource + ' )'}} - - {{log.type}} + {{log.log.severity}} - - {{getPreview(log.message)}} + + {{getPreview(log.log.msg)}} diff --git a/frontend/src/app/app-management/app-logs/app-logs.component.ts b/frontend/src/app/app-management/app-logs/app-logs.component.ts index 4d78e2ea3..2ac9ecdf0 100644 --- a/frontend/src/app/app-management/app-logs/app-logs.component.ts +++ b/frontend/src/app/app-management/app-logs/app-logs.component.ts @@ -15,7 +15,7 @@ import {AppLogType} from './shared/type/app-log.type'; styleUrls: ['./app-logs.component.css'] }) export class AppLogsComponent implements OnInit { - logs: AppLogType[] = []; + logs: any[] = []; links: any; totalItems: number; page = 1; @@ -26,8 +26,9 @@ export class AppLogsComponent implements OnInit { message: string; req: { filters: ElasticFilterType[], index: string, top: number } = { filters: [ - {field: '@timestamp', operator: ElasticOperatorsEnum.IS_BETWEEN, value: ['now-7d', 'now']} - ], index: '.utmstack-logs*', top: 10000000 + {field: '@timestamp', operator: ElasticOperatorsEnum.IS_BETWEEN, value: ['now-7d', 'now']}, + {field: 'log.containerName.keyword', operator: ElasticOperatorsEnum.IS, value: 'utmstack_backend'} + ], index: 'v11-log-utmstack-*', top: 10000000 }; sources = ['PANEL', 'AGENT']; types = ['ERROR', 'WARNING', 'INFO']; @@ -56,7 +57,7 @@ export class AppLogsComponent implements OnInit { private onSuccess(data, headers) { this.totalItems = headers.get('X-Total-Count'); - this.logs = data; + this.logs = data || []; this.loading = false; } @@ -81,7 +82,7 @@ export class AppLogsComponent implements OnInit { this.loadAll(); } - filterBySelect($event: any, field: 'source' | 'type') { + filterBySelect($event: any, field: string) { const index = this.req.filters.findIndex(value => value.field === field); if (index === -1) { this.req.filters.push({field, operator: ElasticOperatorsEnum.IS, value: $event}); @@ -100,7 +101,18 @@ export class AppLogsComponent implements OnInit { } getPreview(message: string): string { - const preview = message.substring(0, message.indexOf(':')).substring(0, 200); - return preview.length > 200 ? (preview + '..') : preview; + if (!message) { return ''; } + + const colonIndex = message.indexOf(':'); + const textToTruncate = colonIndex !== -1 + ? message.substring(0, colonIndex) + : message; + + if (textToTruncate.length > 200) { + return textToTruncate.substring(0, 200) + '..'; + } + + return textToTruncate; } + } diff --git a/frontend/src/app/app-management/app-logs/shared/enum/app-log-type.enum.ts b/frontend/src/app/app-management/app-logs/shared/enum/app-log-type.enum.ts index 7f06dcfa1..d5280fc55 100644 --- a/frontend/src/app/app-management/app-logs/shared/enum/app-log-type.enum.ts +++ b/frontend/src/app/app-management/app-logs/shared/enum/app-log-type.enum.ts @@ -1,5 +1,5 @@ export enum AppLogTypeEnum { ERROR = 'ERROR', INFO = 'INFO', - WARNING = 'WARNING' + WARNING = 'WARN' } diff --git a/frontend/src/app/app-management/app-management-routing.module.ts b/frontend/src/app/app-management/app-management-routing.module.ts index 616d6fa01..96c7c0800 100644 --- a/frontend/src/app/app-management/app-management-routing.module.ts +++ b/frontend/src/app/app-management/app-management-routing.module.ts @@ -16,6 +16,8 @@ import {MenuComponent} from './menu/menu.component'; import {RolloverConfigComponent} from './rollover-config/rollover-config.component'; import {UtmApiDocComponent} from './utm-api-doc/utm-api-doc.component'; import {UtmNotificationViewComponent} from './utm-notification/components/notifications-view/utm-notification-view.component'; +import {ApiKeysComponent} from "./api-keys/api-keys.component"; +import {IdentityProviderComponent} from "./identity-provider/identity-provider.component"; const routes: Routes = [ {path: '', redirectTo: 'settings', pathMatch: 'full'}, @@ -123,7 +125,24 @@ const routes: Routes = [ data: { authorities: [ADMIN_ROLE] }, - }], + }, + { + path: 'api-keys', + component: ApiKeysComponent, + canActivate: [UserRouteAccessService], + data: { + authorities: [ADMIN_ROLE] + }, + }, + { + path: 'providers', + component: IdentityProviderComponent, + canActivate: [UserRouteAccessService], + data: { + authorities: [ADMIN_ROLE] + }, + } + ], }, ]; diff --git a/frontend/src/app/app-management/app-management.module.ts b/frontend/src/app/app-management/app-management.module.ts index 32484f1c9..d56cca75b 100644 --- a/frontend/src/app/app-management/app-management.module.ts +++ b/frontend/src/app/app-management/app-management.module.ts @@ -11,6 +11,8 @@ import {ComplianceManagementModule} from '../compliance/compliance-management/co import {NavBehavior} from '../shared/behaviors/nav.behavior'; import {VersionUpdateBehavior} from '../shared/behaviors/version-update.behavior'; import {UtmSharedModule} from '../shared/utm-shared.module'; +import { ApiKeysComponent } from './api-keys/api-keys.component'; +import { ApiKeyModalComponent } from './api-keys/shared/components/api-key-modal/api-key-modal.component'; import {AppConfigComponent} from './app-config/app-config.component'; import {AppLogsComponent} from './app-logs/app-logs.component'; import {AppManagementRoutingModule} from './app-management-routing.module'; @@ -30,6 +32,9 @@ import {TokenActivateComponent} from './connection-key/token-activate/token-acti import {HealthChecksComponent} from './health-checks/health-checks.component'; import {HealthClusterComponent} from './health-checks/health-cluster/health-cluster.component'; import {HealthDetailComponent} from './health-checks/health-detail/health-detail.component'; +import { IdentityProviderComponent } from './identity-provider/identity-provider.component'; +import { ProviderFormComponent } from './identity-provider/shared/components/provider-form/provider-form.component'; +import { ProviderComponent } from './identity-provider/shared/components/provider/provider.component'; import {IndexDeleteComponent} from './index-management/index-delete/index-delete.component'; import {IndexManagementComponent} from './index-management/index-management.component'; import {IndexPatternDeleteComponent} from './index-pattern/index-pattern-delete/index-pattern-delete.component'; @@ -44,10 +49,13 @@ import {AppManagementSharedModule} from './shared/app-management-shared.module'; import {UtmApiDocComponent} from './utm-api-doc/utm-api-doc.component'; import { UtmNotificationViewComponent -} from "./utm-notification/components/notifications-view/utm-notification-view.component"; +} from './utm-notification/components/notifications-view/utm-notification-view.component'; +import { IdentityProviderModalComponent } from './identity-provider/shared/components/identity-provider-modal/identity-provider-modal.component'; @NgModule({ declarations: [ + ApiKeysComponent, + ApiKeyModalComponent, AppManagementComponent, AppManagementSidebarComponent, IndexPatternHelpComponent, @@ -77,14 +85,22 @@ import { TokenActivateComponent, UtmHttpRequestsPreviewComponent, UtmServicesOverviewComponent, - UtmNotificationViewComponent], + UtmNotificationViewComponent, + IdentityProviderComponent, + ProviderComponent, + ProviderFormComponent, + IdentityProviderModalComponent + ], entryComponents: [ IndexPatternHelpComponent, IndexPatternDeleteComponent, HealthDetailComponent, MenuDeleteDialogComponent, TokenActivateComponent, - IndexDeleteComponent], + ApiKeyModalComponent, + IndexDeleteComponent, + IdentityProviderModalComponent + ], imports: [ CommonModule, AppManagementRoutingModule, diff --git a/frontend/src/app/app-management/audits/audits.component.html b/frontend/src/app/app-management/audits/audits.component.html index 1dd2024b4..b5f210997 100644 --- a/frontend/src/app/app-management/audits/audits.component.html +++ b/frontend/src/app/app-management/audits/audits.component.html @@ -48,7 +48,7 @@
- + diff --git a/frontend/src/app/app-management/identity-provider/identity-provider.component.html b/frontend/src/app/app-management/identity-provider/identity-provider.component.html new file mode 100644 index 000000000..061893785 --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/identity-provider.component.html @@ -0,0 +1,44 @@ +
+
+
+
+ Identity Providers +
+ + Configure OAuth 2.0 / OpenID Connect providers. + +
+ +
+ +
+ +
+ +
No providers configured
+

Add your first OAuth provider to enable SSO login

+ +
+ + +
+
+ + +
+
+ + + +
+
diff --git a/frontend/src/app/app-management/identity-provider/identity-provider.component.scss b/frontend/src/app/app-management/identity-provider/identity-provider.component.scss new file mode 100644 index 000000000..db7f73b3d --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/identity-provider.component.scss @@ -0,0 +1,212 @@ +// Host container +:host { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + flex: 1 1 auto; +} + +// Modal styles +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1050; + animation: fadeIn 0.15s ease; +} + +.modal-container { + background: #ffffff; + border-radius: 0.25rem; + width: 90%; + max-width: 800px; + max-height: 90vh; + display: flex; + flex-direction: column; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + animation: slideUp 0.2s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modal-body { + overflow-y: auto; + padding: 1.25rem; + flex: 1; +} + +.form-section { + margin-bottom: 1.5rem; + + &:last-child { + margin-bottom: 0; + } + + .section-title { + font-size: 0.8125rem; + font-weight: 600; + color: #333; + text-transform: uppercase; + letter-spacing: 0.02em; + margin: 0 0 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #e9ecef; + } +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + + @media (max-width: 768px) { + grid-template-columns: 1fr; + } +} + +.form-group { + margin-bottom: 1rem; + + label { + display: block; + font-size: 0.8125rem; + font-weight: 600; + color: #333; + margin-bottom: 0.375rem; + } + + input, + select { + width: 100%; + height: 2.375rem; + padding: 0.375rem 0.75rem; + border: 1px solid #ddd; + border-radius: 0.1875rem; + font-size: 0.8125rem; + color: #333; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + + &:focus { + outline: none; + border-color: #2196f3; + box-shadow: 0 0 0 0.1rem rgba(33, 150, 243, 0.25); + } + } + + small { + display: block; + margin-top: 0.25rem; + font-size: 0.75rem; + color: #999; + } +} + +.checkbox-group { + label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + + input[type="checkbox"] { + width: 1.125rem; + height: 1.125rem; + cursor: pointer; + } + + span { + font-weight: 500; + font-size: 0.8125rem; + } + } +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + padding: 1rem 1.25rem; + border-top: 1px solid #e9ecef; + + button { + height: 2.375rem; + padding: 0 1rem; + border-radius: 0.1875rem; + font-weight: 600; + font-size: 0.8125rem; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + gap: 0.375rem; + border: 1px solid transparent; + + &:disabled { + opacity: 0.65; + cursor: not-allowed; + } + } + + .btn-secondary { + background: #fff; + border-color: #ddd; + color: #333; + + &:hover:not(:disabled) { + background: #f8f9fa; + border-color: #bbb; + } + } + + .btn-test { + background: #fff; + border-color: #ddd; + color: #333; + + &:hover:not(:disabled) { + background: #f8f9fa; + } + } + + .btn-primary { + background: #2196f3; + border-color: #2196f3; + color: #fff; + + &:hover:not(:disabled) { + background: #0c7cd5; + border-color: #0c7cd5; + } + } +} + +.spinner { + animation: spin 0.75s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} diff --git a/frontend/src/app/app-management/identity-provider/identity-provider.component.ts b/frontend/src/app/app-management/identity-provider/identity-provider.component.ts new file mode 100644 index 000000000..abc2d4f4b --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/identity-provider.component.ts @@ -0,0 +1,106 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {UtmToastService} from '../../shared/alert/utm-toast.service'; +import { + IdentityProviderModalComponent +} from './shared/components/identity-provider-modal/identity-provider-modal.component'; +import {UtmIdentityProvider} from './shared/models/utm-identity-provider.model'; +import {UtmIdentityProviderService} from './shared/services/utm-identity-provider.service'; +import { + ModalConfirmationComponent +} from "../../shared/components/utm/util/modal-confirmation/modal-confirmation.component"; + +@Component({ + selector: 'app-identity-provider-config', + templateUrl: './identity-provider.component.html', + styleUrls: ['./identity-provider.component.scss'] +}) +export class IdentityProviderComponent implements OnInit { + providers: UtmIdentityProvider[] = []; + providerForm: FormGroup; + showModal = false; + editMode = false; + loading = false; + selectedProvider: UtmIdentityProvider | null = null; + + constructor( + private modalService: NgbModal, + private providerService: UtmIdentityProviderService, + private toast: UtmToastService) { + } + + ngOnInit(): void { + this.loadProviders(); + } + + loadProviders(): void { + this.loading = true; + this.providerService.query({page: 0, size: 10}).subscribe({ + next: (res) => { + this.providers = res.body || []; + this.loading = false; + }, + error: () => { + this.toast.showError('Error', 'An error occurred while loading providers'); + this.loading = false; + } + }); + } + + openModal(provider?: UtmIdentityProvider): void { + const modalRef = this.modalService.open(IdentityProviderModalComponent, { + size: 'lg', + centered: true, + }); + + modalRef.componentInstance.provider = provider; + modalRef.componentInstance.editMode = !!provider; + + modalRef.result.then( + (result) => { + if (result) { + this.loadProviders(); + } + }, + () => { + // Modal dismissed + } + ); + } + + deleteProvider(provider: UtmIdentityProvider): void { + const deleteModalRef = this.modalService.open(ModalConfirmationComponent, {centered: true}); + + deleteModalRef.componentInstance.header = 'Confirm delete operation'; + deleteModalRef.componentInstance.message = 'Are you sure that you want to delete the provider: ' + provider.providerType; + deleteModalRef.componentInstance.confirmBtnText = 'Delete'; + deleteModalRef.componentInstance.confirmBtnIcon = 'icon-database-remove'; + deleteModalRef.componentInstance.confirmBtnType = 'delete'; + deleteModalRef.result.then(() => { + + this.providerService.delete(provider.id).subscribe({ + next: () => { + this.toast.showSuccess('Provider deleted successfully'); + this.loadProviders(); + }, + error: () => { + this.toast.showError( 'Error', 'An error occurred while deleting the provider'); + } + }); + + }); + } + + toggleActive(provider: UtmIdentityProvider): void { + this.providerService.update(provider).subscribe({ + next: () => { + this.toast.showSuccess(`Provider ${provider.active ? 'activated' : 'deactivated'}`); + this.loadProviders(); + }, + error: () => { + this.toast.showError( 'Error', 'An error occurred while updating provider status'); + } + }); + } +} diff --git a/frontend/src/app/app-management/identity-provider/shared/components/identity-provider-modal/identity-provider-modal.component.html b/frontend/src/app/app-management/identity-provider/shared/components/identity-provider-modal/identity-provider-modal.component.html new file mode 100644 index 000000000..97cbe0ad7 --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/shared/components/identity-provider-modal/identity-provider-modal.component.html @@ -0,0 +1,47 @@ + + + + + + + + + diff --git a/frontend/src/app/app-management/identity-provider/shared/components/identity-provider-modal/identity-provider-modal.component.scss b/frontend/src/app/app-management/identity-provider/shared/components/identity-provider-modal/identity-provider-modal.component.scss new file mode 100644 index 000000000..904453688 --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/shared/components/identity-provider-modal/identity-provider-modal.component.scss @@ -0,0 +1,73 @@ +:host { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + flex: 1 1 auto; +} + +::ng-deep .modal-content { + display: flex; + flex-direction: column; + max-height: 90vh; + overflow: hidden; +} + +::ng-deep app-utm-modal-header { + flex-shrink: 0; + position: sticky; + top: 0; + z-index: 10; + background: white; +} + + +.modal-body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 1.5rem; + + &::-webkit-scrollbar { + width: 8px; + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; + } + + &::-webkit-scrollbar-thumb { + background: #cbd5e0; + border-radius: 4px; + + &:hover { + background: #a0aec0; + } + } +} + +// Footer fijo +.modal-footer { + flex-shrink: 0; + position: sticky; + bottom: 0; + z-index: 10; + background: white; + border-top: 1px solid #e2e8f0; + padding: 1rem 1.5rem; + display: flex; + justify-content: flex-end; + gap: 0.5rem; + + // Sombra sutil para separaciรณn visual + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05); +} + +.spinner { + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/frontend/src/app/app-management/identity-provider/shared/components/identity-provider-modal/identity-provider-modal.component.ts b/frontend/src/app/app-management/identity-provider/shared/components/identity-provider-modal/identity-provider-modal.component.ts new file mode 100644 index 000000000..f71ebef94 --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/shared/components/identity-provider-modal/identity-provider-modal.component.ts @@ -0,0 +1,103 @@ +import {Component, Input, OnInit, ViewChild} from '@angular/core'; +import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {UtmIdentityProvider} from '../../models/utm-identity-provider.model'; +import {UtmIdentityProviderService} from '../../services/utm-identity-provider.service'; +import {ProviderFormComponent} from '../provider-form/provider-form.component'; + +@Component({ + selector: 'app-identity-provider-modal', + templateUrl: './identity-provider-modal.component.html', + styleUrls: ['./identity-provider-modal.component.scss'] +}) +export class IdentityProviderModalComponent implements OnInit { + @Input() provider?: UtmIdentityProvider; + @Input() editMode = false; + + @ViewChild('providerForm') providerFormComponent!: ProviderFormComponent; + + selectedProvider?: UtmIdentityProvider; + loading = false; + testingConnection = false; + + constructor( + public activeModal: NgbActiveModal, + private providerService: UtmIdentityProviderService + ) {} + + ngOnInit(): void { + this.selectedProvider = this.provider; + } + + saveProvider(): void { + + if (!this.providerFormComponent.providerForm.valid) { + this.markFormGroupTouched(this.providerFormComponent.providerForm); + return; + } + + this.loading = true; + + const formValue = this.providerFormComponent.providerForm.value; + const providerData: UtmIdentityProvider = { + ...formValue, + id: this.provider ? this.provider.id : null, + scopes: Array.isArray(formValue.scopes) ? formValue.scopes.join(',') : formValue.scopes, + allowedDomains: Array.isArray(formValue.allowedDomains) + ? formValue.allowedDomains.join(',') + : formValue.allowedDomains, + providerType: (formValue && formValue.providerType && formValue.providerType.value) + ? formValue.providerType.value : formValue.providerType, + }; + + const request = this.editMode && this.provider.id + ? this.providerService.update(providerData) + : this.providerService.create(providerData); + + request.subscribe({ + next: () => { + this.loading = false; + this.activeModal.close(true); + }, + error: (error) => { + this.loading = false; + console.error('Error saving provider:', error); + } + }); + } + + testConnection(): void { + if (this.providerFormComponent.providerForm && !this.providerFormComponent.providerForm.valid) { + // โœ… Usar funciรณn helper para marcar todos los controles como touched + this.markFormGroupTouched(this.providerFormComponent.providerForm); + return; + } + + this.testingConnection = true; + + this.providerService.testConnection(this.providerFormComponent.providerForm.value).subscribe({ + next: () => { + this.testingConnection = false; + alert('โœ… Connection test successful!'); + }, + error: (error) => { + this.testingConnection = false; + alert('โŒ Connection test failed: ' + error.message); + } + }); + } + + closeModal(): void { + this.activeModal.dismiss(); + } + + private markFormGroupTouched(formGroup: any): void { + Object.keys(formGroup.controls).forEach(key => { + const control = formGroup.get(key); + control.markAsTouched(); + + if (control.controls) { + this.markFormGroupTouched(control); + } + }); + } +} diff --git a/frontend/src/app/app-management/identity-provider/shared/components/provider-form/provider-form.component.html b/frontend/src/app/app-management/identity-provider/shared/components/provider-form/provider-form.component.html new file mode 100644 index 000000000..0668ff0c5 --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/shared/components/provider-form/provider-form.component.html @@ -0,0 +1,245 @@ +
+ +
+
+ + Basic Information +
+ +
+
+
+ + +
+ Provider name is required +
+
+
+ +
+
+ + + +
+
+
+
+ + +
+
+ + OAuth 2.0 Configuration +
+ +
+
+
+ +
+ +
+
+ Client ID is required +
+
+
+ +
+
+ + + + โ„น๏ธ Leave blank to keep current secret, or enter a new one to update it + +
+ Client secret is required +
+
+
+ + +
+
+ + + + This URI must match exactly with the one configured in your OAuth provider + +
+ Redirect URI is required +
+
+
+
+
+ + +
+
+ + OAuth Endpoints +
+ +
+ Auto-configuration: + Select a provider type to automatically fill these endpoints +
+ +
+
+
+ + +
+ Authorization URI is required +
+
+
+ +
+
+ + +
+ Token URI is required +
+
+
+ +
+
+ + +
+ User Info URI is required +
+
+
+ +
+
+ + + Optional: For token validation +
+
+
+
+ + +
+
+ + Advanced Settings +
+ +
+
+
+ + + + + OAuth scopes to request. You can add custom scopes by typing and pressing Enter + +
+ At least one scope is required +
+
+
+ +
+
+ + + + + Restrict authentication to specific email domains. Leave empty to allow all domains + +
+
+ +
+
+
+ + +
+ + Users will only be able to authenticate with this provider if it's enabled + +
+
+
+
+ +
diff --git a/frontend/src/app/app-management/identity-provider/shared/components/provider-form/provider-form.component.scss b/frontend/src/app/app-management/identity-provider/shared/components/provider-form/provider-form.component.scss new file mode 100644 index 000000000..c02e9ce56 --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/shared/components/provider-form/provider-form.component.scss @@ -0,0 +1,46 @@ +.provider-form { + .form-section { + margin-bottom: 2rem; + padding-bottom: 2rem; + border-bottom: 1px solid #e0e0e0; + + &:last-of-type { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; + } + + .section-title { + font-size: 0.9375rem; + font-weight: 600; + color: #333; + margin-bottom: 1.25rem; + display: flex; + align-items: center; + + i { + margin-right: 0.5rem; + color: #2196F3; + } + } + } + + .form-label.required::after { + content: " *"; + color: #f44336; + } + + .form-actions { + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid #e0e0e0; + + .spinner { + animation: spin 1s linear infinite; + } + } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} diff --git a/frontend/src/app/app-management/identity-provider/shared/components/provider-form/provider-form.component.ts b/frontend/src/app/app-management/identity-provider/shared/components/provider-form/provider-form.component.ts new file mode 100644 index 000000000..24a246694 --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/shared/components/provider-form/provider-form.component.ts @@ -0,0 +1,97 @@ +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {FormBuilder, FormGroup, Validators} from '@angular/forms'; +import {ClientAuthMethod, ProviderType, UtmIdentityProvider, } from '../../models/utm-identity-provider.model'; + +@Component({ + selector: 'app-provider-form', + templateUrl: './provider-form.component.html', + styleUrls: ['./provider-form.component.scss'] +}) +export class ProviderFormComponent implements OnInit { + @Input() provider?: UtmIdentityProvider; + @Input() loading = false; + @Input() testingConnection = false; + + @Output() save = new EventEmitter(); + @Output() test = new EventEmitter(); + @Output() cancel = new EventEmitter(); + editMode = false; + + providerForm!: FormGroup; + + providerTypes = Object.values(ProviderType).map((value) => ({ + label: value, + value + })); + + availableScopes = [ + 'openid', + 'email', + 'profile', + ]; + + constructor(private fb: FormBuilder) {} + + ngOnInit(): void { + this.editMode = !!this.provider; + this.initForm(); + if (this.editMode) { + + this.providerTypes = this.providerTypes.filter(pt => pt.value === this.provider.providerType); + this.providerForm.patchValue(this.provider); + + const scopes = this.provider.scopes ? this.provider.scopes.split(',') : []; + const allowedDomains = this.provider.allowedDomains ? this.provider.allowedDomains.split(',') : []; + + this.providerForm.get('scopes').setValue(scopes); + this.providerForm.get('allowedDomains').setValue(allowedDomains); + } + } + + initForm(): void { + this.providerForm = this.fb.group({ + name: ['', Validators.required], + providerType: ['', Validators.required], + clientId: ['', Validators.required], + clientSecret: this.editMode ? [''] : ['', Validators.required], + redirectUri: ['', Validators.required], + authUri: ['', Validators.required], + tokenUri: ['', Validators.required], + userInfoUri: ['', Validators.required], + jwksUri: [''], + scopes: ['', Validators.required], + allowedDomains: [''], + active: [true] + }); + } + + onProviderTypeChange(): void { + const type = this.providerForm.get('providerType').value; + const presets = this.getProviderPresets(type.value); + + if (presets) { + this.providerForm.patchValue(presets); + } + } + + getProviderPresets(type: string): Partial | null { + + const presets: Record> = { + [ProviderType.GOOGLE]: { + authUri: 'https://accounts.google.com/o/oauth2/auth', + tokenUri: 'https://oauth2.googleapis.com/token', + userInfoUri: 'https://www.googleapis.com/oauth2/v2/userinfo', + jwksUri: 'https://www.googleapis.com/oauth2/v3/certs', + scopes: 'openid,email,profile' + }, + [ProviderType.MICROSOFT]: { + authUri: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', + tokenUri: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + userInfoUri: 'https://graph.microsoft.com/v1.0/me', + scopes: 'openid,email,profile,User.Read' + } + }; + + return presets[type] || null; + } +} diff --git a/frontend/src/app/app-management/identity-provider/shared/components/provider/provider.component.html b/frontend/src/app/app-management/identity-provider/shared/components/provider/provider.component.html new file mode 100644 index 000000000..a1e8cafea --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/shared/components/provider/provider.component.html @@ -0,0 +1,99 @@ +
+ +
+
+
+ +
+
+
{{provider.name}}
+ {{provider.providerType}} +
+
+ +
+ +
+ + + {{provider.active ? 'Active' : 'Inactive'}} + +
+ + + +
+
+ + +
+ +
+
+ +
+
+ Client ID +
+ {{provider.clientId | slice:0:40}}... + +
+
+
+ + +
+
+ +
+
+ Redirect URI +
+ {{provider.redirectUri}} +
+
+
+ + +
+
+ +
+
+ Allowed Domains +
+ {{provider.allowedDomains}} +
+
+
+
+ + + +
diff --git a/frontend/src/app/app-management/identity-provider/shared/components/provider/provider.component.scss b/frontend/src/app/app-management/identity-provider/shared/components/provider/provider.component.scss new file mode 100644 index 000000000..07a730fa0 --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/shared/components/provider/provider.component.scss @@ -0,0 +1,348 @@ +.provider-card { + background: #ffffff; + border-radius: 0.5rem; + overflow: hidden; + transition: all 0.3s ease; + border: 1px solid #e9ecef; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background: #dee2e6; + transition: background 0.3s ease; + } + + &.provider-active::before { + background: linear-gradient(180deg, #26c281 0%, #1abc9c 100%); + } + + &:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transform: translateY(-2px); + } +} + +// Header +.provider-header { + padding: 1.25rem 1.5rem; + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border-bottom: 1px solid #e9ecef; + display: flex; + justify-content: space-between; + align-items: center; +} + +.provider-brand { + display: flex; + align-items: center; + gap: 1rem; +} + +.provider-icon-wrapper { + width: 3rem; + height: 3rem; + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + color: #fff; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + box-shadow: 0 4px 8px rgba(102, 126, 234, 0.25); + + &.icon-wrapper-google { + background: linear-gradient(135deg, #4285f4 0%, #34a853 100%); + } + + &.icon-wrapper-microsoft { + background: linear-gradient(135deg, #0078d4 0%, #00bcf2 100%); + } + + &.icon-wrapper-github { + background: linear-gradient(135deg, #24292e 0%, #6e5494 100%); + } + + &.icon-wrapper-gitlab { + background: linear-gradient(135deg, #fc6d26 0%, #fca326 100%); + } +} + +.provider-meta { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.provider-name { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: #2c3e50; + line-height: 1.2; +} + +.provider-type-badge { + display: inline-block; + padding: 0.125rem 0.5rem; + font-size: 0.6875rem; + font-weight: 600; + color: #667eea; + background: rgba(102, 126, 234, 0.1); + border-radius: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.provider-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +// Toggle Switch +.status-toggle { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.switch { + position: relative; + display: inline-block; + width: 2.5rem; + height: 1.25rem; + + input { + opacity: 0; + width: 0; + height: 0; + + &:checked + .slider { + background: linear-gradient(135deg, #26c281 0%, #1abc9c 100%); + + &::before { + transform: translateX(1.25rem); + } + } + + &:focus + .slider { + box-shadow: 0 0 0 0.2rem rgba(38, 194, 129, 0.25); + } + } +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #cbd5e0; + transition: 0.3s; + border-radius: 1.25rem; + + &::before { + position: absolute; + content: ""; + height: 1rem; + width: 1rem; + left: 0.125rem; + bottom: 0.125rem; + background-color: white; + transition: 0.3s; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + } +} + +.status-label { + font-size: 0.75rem; + font-weight: 500; + color: #718096; +} + +.btn-icon { + width: 2rem; + height: 2rem; + padding: 0; + border: 1px solid #e2e8f0; + background: #fff; + border-radius: 0.375rem; + color: #718096; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: #f7fafc; + color: #2d3748; + border-color: #cbd5e0; + } +} + +// Body +.provider-body { + padding: 1.25rem 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.provider-detail { + display: flex; + align-items: flex-start; + gap: 0.75rem; +} + +.detail-icon { + width: 2rem; + height: 2rem; + border-radius: 0.375rem; + background: #f7fafc; + display: flex; + align-items: center; + justify-content: center; + color: #718096; + font-size: 1rem; + flex-shrink: 0; +} + +.detail-content { + flex: 1; + min-width: 0; +} + +.detail-label { + display: block; + font-size: 0.6875rem; + font-weight: 600; + color: #a0aec0; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.25rem; +} + +.detail-value { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; + color: #2d3748; + + code { + background: #f7fafc; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + color: #667eea; + border: 1px solid #e2e8f0; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .text-truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.btn-copy { + padding: 0.25rem 0.5rem; + border: 1px solid #e2e8f0; + background: #fff; + border-radius: 0.25rem; + color: #718096; + cursor: pointer; + transition: all 0.15s ease; + flex-shrink: 0; + + &:hover { + background: #f7fafc; + color: #667eea; + border-color: #cbd5e0; + } +} + +.badge-outline { + padding: 0.25rem 0.625rem; + font-size: 0.75rem; + font-weight: 500; + border: 1px solid #e2e8f0; + background: #fff; + color: #718096; + border-radius: 0.375rem; +} + +// Footer +.provider-footer { + padding: 0.75rem 1.5rem; + background: #f7fafc; + border-top: 1px solid #e9ecef; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.75rem; +} + +.status-indicator { + display: flex; + align-items: center; + gap: 0.375rem; + color: #a0aec0; + font-weight: 500; + + i { + font-size: 0.5rem; + } + + &.status-active { + color: #26c281; + + i { + color: #26c281; + } + } +} + +.last-updated { + color: #a0aec0; +} + +.btn-delete { + width: 2rem; + height: 2rem; + padding: 0; + border: 1px solid #fee; + background: #fff; + border-radius: 0.375rem; + color: #e53e3e; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.875rem; + + &:hover { + background: #fff5f5; + border-color: #fc8181; + color: #c53030; + } + + &:active { + transform: scale(0.95); + } +} diff --git a/frontend/src/app/app-management/identity-provider/shared/components/provider/provider.component.ts b/frontend/src/app/app-management/identity-provider/shared/components/provider/provider.component.ts new file mode 100644 index 000000000..6ec333733 --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/shared/components/provider/provider.component.ts @@ -0,0 +1,37 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {ProviderType, UtmIdentityProvider} from '../../models/utm-identity-provider.model'; + +@Component({ + selector: 'app-provider-card', + templateUrl: './provider.component.html', + styleUrls: ['./provider.component.scss'] +}) +export class ProviderComponent { + @Input() provider: UtmIdentityProvider; + @Output() edit = new EventEmitter(); + @Output() delete = new EventEmitter(); + @Output() toggleStatus = new EventEmitter(); + + getProviderIcon(type: ProviderType): string { + const icons: Record = { + [ProviderType.GOOGLE]: 'icon-google', + [ProviderType.MICROSOFT]: 'icon-windows8', + }; + return icons[type] || 'icon-key'; + } + + openModal(provider: UtmIdentityProvider): void { + this.edit.emit(provider); + } + + deleteProvider(provider: UtmIdentityProvider): void { + this.delete.emit(provider); + } + + toggleActive(provider: UtmIdentityProvider): void { + this.toggleStatus.emit({ + ...provider, + active: !provider.active + }); + } +} diff --git a/frontend/src/app/app-management/identity-provider/shared/models/utm-identity-provider.model.ts b/frontend/src/app/app-management/identity-provider/shared/models/utm-identity-provider.model.ts new file mode 100644 index 000000000..7ba387d3e --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/shared/models/utm-identity-provider.model.ts @@ -0,0 +1,35 @@ +export enum ProviderType { + GOOGLE = 'GOOGLE', + MICROSOFT = 'MICROSOFT' +} + +export const PROVIDER_ICONS: Record = { + [ProviderType.GOOGLE]: 'fa-brands fa-google', + [ProviderType.MICROSOFT]: 'fa-brands fa-microsoft' +}; + +export enum ClientAuthMethod { + CLIENT_SECRET_BASIC = 'CLIENT_SECRET_BASIC', + CLIENT_SECRET_POST = 'CLIENT_SECRET_POST', + CLIENT_SECRET_JWT = 'CLIENT_SECRET_JWT', + PRIVATE_KEY_JWT = 'PRIVATE_KEY_JWT' +} + +export interface UtmIdentityProvider { + id?: number; + name: string; + providerType: ProviderType; + clientId: string; + clientSecret: string; + redirectUri: string; + authUri: string; + tokenUri: string; + userInfoUri: string; + jwksUri?: string; + clientAuthMethod?: ClientAuthMethod; + scopes: string; + allowedDomains?: string; + active: boolean; + createdAt?: Date; + updatedAt?: Date; +} diff --git a/frontend/src/app/app-management/identity-provider/shared/services/utm-identity-provider.service.ts b/frontend/src/app/app-management/identity-provider/shared/services/utm-identity-provider.service.ts new file mode 100644 index 000000000..cbc845d79 --- /dev/null +++ b/frontend/src/app/app-management/identity-provider/shared/services/utm-identity-provider.service.ts @@ -0,0 +1,40 @@ +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import {SERVER_API_URL} from '../../../../app.constants'; +import {createRequestOption} from "../../../../shared/util/request-util"; +import { UtmIdentityProvider } from '../models/utm-identity-provider.model'; + +@Injectable({ + providedIn: 'root' +}) +export class UtmIdentityProviderService { + private resourceUrl = SERVER_API_URL + 'api/identity-providers'; + + constructor(private http: HttpClient) {} + + create(provider: UtmIdentityProvider): Observable> { + return this.http.post(this.resourceUrl, provider, { observe: 'response' }); + } + + update(provider: UtmIdentityProvider): Observable> { + return this.http.put(`${this.resourceUrl}/${provider.id}`, provider, { observe: 'response' }); + } + + find(id: number): Observable> { + return this.http.get(`${this.resourceUrl}/${id}`, { observe: 'response' }); + } + + query(request: any): Observable> { + const params = createRequestOption(request); + return this.http.get(this.resourceUrl, { params, observe: 'response' }); + } + + delete(id: number): Observable> { + return this.http.delete(`${this.resourceUrl}/${id}`, { observe: 'response' }); + } + + testConnection(provider: UtmIdentityProvider): Observable> { + return this.http.post<{ success: boolean; message: string }>(`${this.resourceUrl}/test`, provider, { observe: 'response' }); + } +} diff --git a/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html b/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html index 66efbcbfb..dafdb331e 100644 --- a/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html +++ b/frontend/src/app/app-management/layout/app-management-sidebar/app-management-sidebar.component.html @@ -125,6 +125,43 @@ + + +   + API Keys + + + + + + + +   + Identity Providers + + + + + + +   + Identity Providers + + + + + + + + = new Subject(); + ModulesEnterprise = EnterpriseFeatures; + versionType = VersionType; + version: VersionType; constructor(public router: Router, - private licenceChangeBehavior: LicenceChangeBehavior, + public versionTypeService: VersionTypeService, private checkLicenseService: CheckLicenseService, private spinner: NgxSpinnerService) { } @@ -34,6 +41,9 @@ export class AppManagementSidebarComponent implements OnInit { } }); }*/ + this.versionTypeService.versionType$ + .pipe(takeUntil(this.destroy$)) + .subscribe(versionType => this.version = versionType); } private updateView(): void { @@ -59,4 +69,9 @@ export class AppManagementSidebarComponent implements OnInit { this.spinner.hide('licenseSpinner'); }); } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/frontend/src/app/app-module/app-module.module.ts b/frontend/src/app/app-module/app-module.module.ts index 5965ba297..ed0793c39 100644 --- a/frontend/src/app/app-module/app-module.module.ts +++ b/frontend/src/app/app-module/app-module.module.ts @@ -62,6 +62,7 @@ import {UtmListComponent} from './guides/shared/components/utm-list.component'; import {ModuleIntegrationComponent} from './module-integration/module-integration.component'; import {ModuleService} from './services/module.service'; import {AppModuleSharedModule} from './shared/app-module-shared.module'; +import {GuideUtmstackComponent} from "./guides/guide-utmstack/guide-utmstack.component"; @NgModule({ @@ -119,7 +120,8 @@ import {AppModuleSharedModule} from './shared/app-module-shared.module'; AgentActionCommandComponent, InstallLogCollectorComponent, AgentInstallSelectorComponent, - OracleComponent + OracleComponent, + GuideUtmstackComponent ], imports: [ CommonModule, diff --git a/frontend/src/app/app-module/guides/guide-syslog/guide-syslog.component.html b/frontend/src/app/app-module/guides/guide-syslog/guide-syslog.component.html index 378165bc3..052af7fae 100644 --- a/frontend/src/app/app-module/guides/guide-syslog/guide-syslog.component.html +++ b/frontend/src/app/app-module/guides/guide-syslog/guide-syslog.component.html @@ -32,8 +32,7 @@

+ [platforms]="platforms"> diff --git a/frontend/src/app/app-module/guides/guide-syslog/guide-syslog.component.ts b/frontend/src/app/app-module/guides/guide-syslog/guide-syslog.component.ts index bc0d6dc56..42d382e3c 100644 --- a/frontend/src/app/app-module/guides/guide-syslog/guide-syslog.component.ts +++ b/frontend/src/app/app-module/guides/guide-syslog/guide-syslog.component.ts @@ -85,9 +85,7 @@ export class GuideSyslogComponent implements OnInit { } ngOnInit() {} - getImage(): SyslogModuleImages { - return this.moduleImages.filter(value => value.module === this.moduleEnum)[0]; - } + getPorts(): SyslogModulePorts[] { return this.syslogPorts.filter(value => value.module === this.moduleEnum); } diff --git a/frontend/src/app/shared/components/app-filter/app-filter.component.css b/frontend/src/app/app-module/guides/guide-utmstack/guide-utmstack.component.css similarity index 100% rename from frontend/src/app/shared/components/app-filter/app-filter.component.css rename to frontend/src/app/app-module/guides/guide-utmstack/guide-utmstack.component.css diff --git a/frontend/src/app/app-module/guides/guide-utmstack/guide-utmstack.component.html b/frontend/src/app/app-module/guides/guide-utmstack/guide-utmstack.component.html new file mode 100644 index 000000000..e27af5155 --- /dev/null +++ b/frontend/src/app/app-module/guides/guide-utmstack/guide-utmstack.component.html @@ -0,0 +1,53 @@ +
+
+

+ UTMStack +

+
+ +
+
+ This integration can only be installed on an instance of UTMStack. +
+
    + + +
  1. + + {{step.id}} +
    + + + +
    +
      +
    • This integration can only be installed on Ubuntu or Red Hat.
    • +
    +
    +
    + + + + + + +
    + + +
    +
    + +
    +
    +
  2. +
    +
    +
+
+
+ + diff --git a/frontend/src/app/app-module/guides/guide-utmstack/guide-utmstack.component.ts b/frontend/src/app/app-module/guides/guide-utmstack/guide-utmstack.component.ts new file mode 100644 index 000000000..0686415f9 --- /dev/null +++ b/frontend/src/app/app-module/guides/guide-utmstack/guide-utmstack.component.ts @@ -0,0 +1,123 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {FormBuilder, FormGroup} from '@angular/forms'; +import { + FederationConnectionService +} from '../../../app-management/connection-key/shared/services/federation-connection.service'; +import {GroupTypeEnum} from '../../shared/enum/group-type.enum'; +import {UtmModulesEnum} from '../../shared/enum/utm-module.enum'; +import {Step} from '../shared/step'; +import {UtmstackSteps} from './utmstack.steps'; + +@Component({ + selector: 'app-guide-utmstack', + templateUrl: './guide-utmstack.component.html', + styleUrls: ['./guide-utmstack.component.css'] +}) +export class GuideUtmstackComponent implements OnInit { + @Input() integrationId: number; + @Input() serverId: number; + module = UtmModulesEnum; + serverAS400FormArray: FormGroup; + configValidity: boolean; + groupType = GroupTypeEnum.COLLECTOR; + steps: Step[] = UtmstackSteps; + token: string; + ip: string; + vars: any; + disablePreAction = false; + performPreAction = true; + architectures = []; + + + constructor(private formBuilder: FormBuilder, + private federationConnectionService: FederationConnectionService) { + } + + ngOnInit() { + this.getToken(); + } + + + getToken() { + this.federationConnectionService.getToken().subscribe(response => { + if (response.body !== null && response.body !== '') { + this.token = response.body; + } else { + this.token = ''; + } + this.loadArchitectures(); + }); + } + + configValidChange($event: boolean) { + this.configValidity = !$event; + } + + onDisable() { + this.disablePreAction = true; + } + + private loadArchitectures() { + this.architectures = [ + { + id: 1, name: 'Ubuntu 16/18/20+', + install: this.getCommandUbuntu('utmstack_collector'), + uninstall: this.getUninstallCommandUbuntu('utmstack_collector'), + shell: '' + }, + { + id: 2, name: 'CentOS 8+/Red Hat Enterprise Linux', + install: this.getCommandCentos7RedHat('utmstack_collector'), + uninstall: this.getUninstallCommandRedHat('utmstack_collector'), + shell: '' + }, + ]; + } + + getCommandUbuntu(installerName: string): string { + const ip = window.location.host.includes(':') ? window.location.host.split(':')[0] : window.location.host; + + return `sudo bash -c 'apt update -y && \ + apt install wget -y && \ + mkdir -p /opt/utmstack-collector && \ + wget --no-check-certificate -P /opt/utmstack-collector \ + https://${ip}:9001/private/dependencies/collector/${installerName} && \ + chmod -R 777 /opt/utmstack-collector/${installerName} && \ + /opt/utmstack-collector/${installerName} install ${ip} ${this.token}' yes'`; + } + + + getCommandCentos7RedHat(installerName: string): string { + const ip = window.location.host.includes(':') ? window.location.host.split(':')[0] : window.location.host; + + return `sudo bash -c "dnf update -y && \ + dnf install wget -y && \ + mkdir -p /opt/utmstack-collector && \ + wget --no-check-certificate -P /opt/utmstack-collector \ + https://${ip}:9001/private/dependencies/collector/${installerName} && \ + chmod -R 777 /opt/utmstack-collector/${installerName} && \ + /opt/utmstack-collector/${installerName} install ${ip} ${this.token} yes"`; + } + + getUninstallCommandUbuntu(installerName: string): string { + return `sudo bash -c "/opt/utmstack-collector/${installerName} uninstall && \ + (systemctl stop UTMStackCollector 2>/dev/null || service UTMStackCollector stop 2>/dev/null || true) && \ + (systemctl disable UTMStackCollector 2>/dev/null || chkconfig UTMStackCollector off 2>/dev/null || true) && \ + rm -rf /opt/utmstack-collector && \ + rm -f /etc/systemd/system/UTMStackCollector.service && \ + rm -f /etc/init.d/UTMStackCollector && \ + (systemctl daemon-reload 2>/dev/null || true) && \ + echo 'UTMStack Collector uninstalled successfully'"`; + } + + + getUninstallCommandRedHat(installerName: string): string { + return `sudo bash -c "/opt/utmstack-collector/${installerName} uninstall && \ + (systemctl stop UTMStackCollector 2>/dev/null || true) && \ + (systemctl disable UTMStackCollector 2>/dev/null || true) && \ + rm -rf /opt/utmstack-collector && \ + rm -f /etc/systemd/system/UTMStackCollector.service && \ + systemctl daemon-reload && \ + echo 'UTMStack Collector uninstalled successfully'"`; + } +} diff --git a/frontend/src/app/app-module/guides/guide-utmstack/utmstack.steps.ts b/frontend/src/app/app-module/guides/guide-utmstack/utmstack.steps.ts new file mode 100644 index 000000000..3d7ab8fa6 --- /dev/null +++ b/frontend/src/app/app-module/guides/guide-utmstack/utmstack.steps.ts @@ -0,0 +1,20 @@ +import {Step} from '../shared/step'; + +export const UtmstackSteps: Step[] = [ + { id: '1', + name: ' Check pre-installation requirements.', + content: { + id: 'stepContent1' + } + }, + {id: '2', name: 'Install or uninstall according to your operating system:', + content: { + id: 'stepContent2' + } + }, + {id: '3', name: 'Click on the button shown below, to activate the UTMStack features related to this integration', + content: { + id: 'stepContent3' + } + } +]; diff --git a/frontend/src/app/app-module/guides/shared/components/agent-action-command.component.ts b/frontend/src/app/app-module/guides/shared/components/agent-action-command.component.ts index 7c6b35aa9..b10401c4c 100644 --- a/frontend/src/app/app-module/guides/shared/components/agent-action-command.component.ts +++ b/frontend/src/app/app-module/guides/shared/components/agent-action-command.component.ts @@ -33,9 +33,16 @@ import {UtmModulesEnum} from '../../../shared/enum/utm-module.enum'; class="flex-item">

- +
+ After the TLS certificates have been successfully loaded into the system, + it is not necessary to repeat the certificate loading process when enabling + additional integrations that use TLS. The system will automatically apply the + previously configured certificates to ensure secure communication. +
+ {{selectedPlatform.shell}} - + `, styles: [` @@ -59,7 +66,8 @@ export class AgentActionCommandComponent implements OnInit{ @Input() hideProtocols = false; @Input() protocols = [ {id: 1, name: 'TCP'}, - {id: 2, name: 'UDP'} + {id: 2, name: 'TCP/TLS'}, + {id: 3, name: 'UDP'} ]; actions = [ @@ -75,16 +83,27 @@ export class AgentActionCommandComponent implements OnInit{ constructor(private modalService: ModalService) { } - ngOnInit(): void { - console.log(this.selectedPlatform); - } + ngOnInit(): void {} + + get commands() { + + const protocol = this.selectedProtocol && this.selectedProtocol.name === 'TCP/TLS' ? 'tcp' : this.selectedProtocol.name.toLowerCase(); - get command() { - return replaceCommandTokens(this.selectedPlatform.command, { - PORT: this.selectedProtocol && this.selectedProtocol.name.toLowerCase() || '', - AGENT_NAME: this.agent, - ACTION: this.selectedAction && this.selectedAction.action || '' + const command = replaceCommandTokens(this.selectedPlatform.command, { + ACTION: this.selectedAction && this.selectedAction.action || '', + AGENT_NAME: this.agent || '', + PROTOCOL: protocol, + TLS: this.selectedProtocol && this.selectedProtocol.name === 'TCP/TLS' && + this.selectedAction.name === 'ENABLE' ? `--tls` : '' }); + + if (this.selectedProtocol && this.selectedProtocol.name === 'TCP/TLS' && + this.selectedAction.name === 'ENABLE') { + const extras = this.selectedPlatform.extraCommands ? this.selectedPlatform.extraCommands : []; + return [...extras, command]; + } + + return [command]; } get selectedPlatform() { diff --git a/frontend/src/app/app-module/guides/shared/constant.ts b/frontend/src/app/app-module/guides/shared/constant.ts index 5be905914..468dd47fc 100644 --- a/frontend/src/app/app-module/guides/shared/constant.ts +++ b/frontend/src/app/app-module/guides/shared/constant.ts @@ -1,81 +1,109 @@ +const WINDOWS_SHELL = + 'Run the following PowerShell script as โ€œADMINISTRATORโ€ on a server with the UTMStack agent installed.'; + +const LINUX_SHELL = + 'Run the following Bash script as โ€œADMINISTRATORโ€ on a server with the UTMStack agent installed.'; + export interface Platform { id: number; name: string; command: string; shell: string; - path: string; - restart: string; + path?: string; + restart?: string; + extraCommands?: string[]; } -export const createPlatforms = (windowsCommandAMD64: string, - windowsCommandARM64: string, - linuxCommand: string, - windowsPath?: string, - windowsRestart?: string, - linuxPath?: string, - linuxRestart?: string) => [ - { - id: 1, - name: 'WINDOWS (AMD64)', - command: windowsCommandAMD64, - shell: 'Run the following powershell script as โ€œADMINISTRATORโ€ in a Server with the UTMStack agent Installed.', - path: windowsPath, - restart: windowsRestart - }, - { - id: 2, - name: 'WINDOWS (ARM64)', - command: windowsCommandARM64, - shell: 'Run the following powershell script as โ€œADMINISTRATORโ€ in a Server with the UTMStack agent Installed.', - path: windowsPath, - restart: windowsRestart - }, - { - id: 3, - name: 'LINUX', - command: linuxCommand, - shell: 'Run the following bash script as โ€œADMINISTRATORโ€ in a Server with the UTMStack agent Installed.', - path: linuxPath, - restart: linuxRestart - } -]; +function createPlatform( + id: number, + name: string, + command: string, + shell: string, + path?: string, + restart?: string, + extraCommands?: string[]): Platform { + return { id, name, command, shell, path, restart, extraCommands }; +} -export const createFileBeatsPlatforms = (windowsCommand: string, - linuxCommand: string, - windowsPath?: string, - windowsRestart?: string, - linuxPath?: string, - linuxRestart?: string) => [ - { - id: 1, - name: 'WINDOWS', - command: windowsCommand, - shell: 'Run the following powershell script as โ€œADMINISTRATORโ€ in a Server with the UTMStack agent Installed.', - path: windowsPath, - restart: windowsRestart - }, - { - id: 3, - name: 'LINUX', - command: linuxCommand, - shell: 'Run the following bash script as โ€œADMINISTRATORโ€ in a Server with the UTMStack agent Installed.', - path: linuxPath, - restart: linuxRestart - } +export const createPlatforms = ( + windowsCommandAMD64: string, + windowsCommandARM64: string, + linuxCommand: string, + windowsPath?: string, + windowsRestart?: string, + linuxPath?: string, + linuxRestart?: string): Platform[] => [ + createPlatform( + 1, + 'WINDOWS (AMD64)', + windowsCommandAMD64, + WINDOWS_SHELL, + windowsPath, + windowsRestart,[ + 'Start-Process "C:\\Program Files\\UTMStack\\UTMStack Agent\\utmstack_agent_service.exe" ' + + '-ArgumentList \'load-tls-certs\', \'[YOUR_CERT_PATH]\', \'[YOUR_KEY_PATH]\' ' + + '-NoNewWindow -Wait' + ] + ), + createPlatform( + 2, + 'WINDOWS (ARM64)', + windowsCommandARM64, + WINDOWS_SHELL, + windowsPath, + windowsRestart, + [ + 'Start-Process "C:\\Program Files\\UTMStack\\UTMStack Agent\\utmstack_agent_service_arm64.exe" ' + + '-ArgumentList \'load-tls-certs\', \'[YOUR_CERT_PATH]\', \'[YOUR_KEY_PATH]\' ' + + '-NoNewWindow -Wait' + ] + ), + createPlatform( + 3, + 'LINUX', + linuxCommand, + LINUX_SHELL, + linuxPath, + linuxRestart, + [ + `sudo bash -c "/opt/utmstack-linux-agent/utmstack_agent_service load-tls-certs [YOUR_CERT_PATH] [YOUR_KEY_PATH]"` + ] + ) ]; +export const createFileBeatsPlatforms = ( + windowsCommand: string, + linuxCommand: string, + windowsPath?: string, + windowsRestart?: string, + linuxPath?: string, + linuxRestart?: string): Platform[] => [ + createPlatform( + 1, + 'WINDOWS', + windowsCommand, + WINDOWS_SHELL, + windowsPath, + windowsRestart + ), + createPlatform( + 3, + 'LINUX', + linuxCommand, + LINUX_SHELL, + linuxPath, + linuxRestart + ) +]; export const PLATFORMS = createPlatforms( - 'Start-Process "C:\\Program Files\\UTMStack\\UTMStack Agent\\utmstack_agent_service.exe" -ArgumentList \'ACTION\',' + - ' \'AGENT_NAME\', \'PORT\' -NoNewWindow -Wait\n', - 'Start-Process "C:\\Program Files\\UTMStack\\UTMStack Agent\\utmstack_agent_service_arm64.exe" -ArgumentList \'ACTION\',' + - ' \'AGENT_NAME\', \'PORT\' -NoNewWindow -Wait\n', - 'sudo bash -c "/opt/utmstack-linux-agent/utmstack_agent_service ACTION AGENT_NAME PORT"' + 'Start-Process "C:\\Program Files\\UTMStack\\UTMStack Agent\\utmstack_agent_service.exe" -ArgumentList \'ACTION\', \'AGENT_NAME\', \'PROTOCOL\', \'TLS\' -NoNewWindow -Wait\n', + 'Start-Process "C:\\Program Files\\UTMStack\\UTMStack Agent\\utmstack_agent_service_arm64.exe" -ArgumentList \'ACTION\', \'AGENT_NAME\', \'PROTOCOL\, \'TLS\' -NoNewWindow -Wait\n', + 'sudo bash -c "/opt/utmstack-linux-agent/utmstack_agent_service ACTION AGENT_NAME PROTOCOL TLS"' ); - export const FILEBEAT_PLATFORMS = createFileBeatsPlatforms( - 'cd "C:\\Program Files\\UTMStack\\UTMStack Agent\\beats\\filebeat\\"; Start-Process "filebeat.exe" -ArgumentList "modules", "enable", \"AGENT_NAME\"', + 'cd "C:\\Program Files\\UTMStack\\UTMStack Agent\\beats\\filebeat\\"; Start-Process "filebeat.exe" -ArgumentList "modules", "enable", "AGENT_NAME"', 'cd /opt/utmstack-linux-agent/beats/filebeat/ && ./filebeat modules enable AGENT_NAME', 'C:\\Program Files\\UTMStack\\UTMStack Agent\\beats\\filebeat\\modules.d\\', 'Stop-Service -Name UTMStackModulesLogsCollector; Start-Sleep -Seconds 5; Start-Service -Name UTMStackModulesLogsCollector', diff --git a/frontend/src/app/app-module/module-integration/module-integration.component.html b/frontend/src/app/app-module/module-integration/module-integration.component.html index c43c51ed0..8e45b63fb 100644 --- a/frontend/src/app/app-module/module-integration/module-integration.component.html +++ b/frontend/src/app/app-module/module-integration/module-integration.component.html @@ -173,6 +173,9 @@ + + diff --git a/frontend/src/app/app-module/services/module.service.ts b/frontend/src/app/app-module/services/module.service.ts index 786f011a8..1cea3a333 100644 --- a/frontend/src/app/app-module/services/module.service.ts +++ b/frontend/src/app/app-module/services/module.service.ts @@ -7,12 +7,6 @@ import {UtmModulesService} from '../shared/services/utm-modules.service'; import {UtmServerService} from '../shared/services/utm-server.service'; import {UtmServerType} from '../shared/type/utm-server.type'; -export const ModulesEnterprise = [ - UtmModulesEnum.MACOS, - UtmModulesEnum.AS_400 -]; - - export interface RequestModule { 'moduleCategory.equals'?: string | null; 'prettyName.contains'?: string | null; diff --git a/frontend/src/app/app-module/shared/components/app-module-card/app-module-card.component.html b/frontend/src/app/app-module/shared/components/app-module-card/app-module-card.component.html index 8cf7e65b1..9a086b794 100644 --- a/frontend/src/app/app-module/shared/components/app-module-card/app-module-card.component.html +++ b/frontend/src/app/app-module/shared/components/app-module-card/app-module-card.component.html @@ -1,8 +1,8 @@
-
+
{{module.prettyName}}

- - + -->
diff --git a/frontend/src/app/app-module/shared/components/app-module-card/app-module-card.component.ts b/frontend/src/app/app-module/shared/components/app-module-card/app-module-card.component.ts index c620f5098..7693919de 100644 --- a/frontend/src/app/app-module/shared/components/app-module-card/app-module-card.component.ts +++ b/frontend/src/app/app-module/shared/components/app-module-card/app-module-card.component.ts @@ -5,8 +5,11 @@ import {takeUntil} from 'rxjs/operators'; import { ModalConfirmationComponent } from '../../../../shared/components/utm/util/modal-confirmation/modal-confirmation.component'; -import {VersionType, VersionTypeService} from '../../../../shared/services/util/version-type.service'; -import {ModulesEnterprise} from '../../../services/module.service'; +import { + EnterpriseFeatures, + VersionType, + VersionTypeService +} from '../../../../shared/services/util/version-type.service'; import {UtmModulesEnum} from '../../enum/utm-module.enum'; import {UtmModuleType} from '../../type/utm-module.type'; @@ -19,7 +22,7 @@ import {UtmModuleType} from '../../type/utm-module.type'; export class AppModuleCardComponent implements OnInit, OnDestroy { constructor(private versionTypeService: VersionTypeService, - private modalService: NgbModal,) { + private modalService: NgbModal) { } @Input() module: UtmModuleType; @Output() showModuleIntegration = new EventEmitter(); @@ -27,7 +30,7 @@ export class AppModuleCardComponent implements OnInit, OnDestroy { version: VersionType; modules = UtmModulesEnum; destroy$: Subject = new Subject(); - ModulesEnterprise = ModulesEnterprise; + ModulesEnterprise = EnterpriseFeatures; ngOnInit() { this.versionTypeService.versionType$ diff --git a/frontend/src/app/app-module/shared/enum/utm-module.enum.ts b/frontend/src/app/app-module/shared/enum/utm-module.enum.ts index 72dbf7a76..7da0fe630 100644 --- a/frontend/src/app/app-module/shared/enum/utm-module.enum.ts +++ b/frontend/src/app/app-module/shared/enum/utm-module.enum.ts @@ -68,5 +68,6 @@ export enum UtmModulesEnum { AS_400 = 'AS_400', SOC_AI = 'SOC_AI', ORACLE = 'ORACLE', - SURICATA= 'SURICATA' + SURICATA= 'SURICATA', + UTMSTACK= 'UTMSTACK' } diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 1362ea28d..13b1d7528 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -154,7 +154,7 @@ const routes: Routes = [ }, {path: '', component: LoginComponent}, {path: 'totp', component: TotpComponent}, - {path: 'tfa-setup', component: TfaSetupComponent}, + {path: 'enroll-tfa', component: TfaSetupComponent}, {path: 'reset/finish', component: PasswordResetFinishComponent}, {path: 'page-not-found', component: NotFoundComponent}, {path: 'confirm-identity/:id', component: ConfirmIdentityComponent}, diff --git a/frontend/src/app/assets-discover/assets-view/assets-view.component.html b/frontend/src/app/assets-discover/assets-view/assets-view.component.html index 0b30d633d..8776a0ae6 100644 --- a/frontend/src/app/assets-discover/assets-view/assets-view.component.html +++ b/frontend/src/app/assets-discover/assets-view/assets-view.component.html @@ -120,7 +120,7 @@
- +
@@ -149,7 +149,7 @@
- {{ getAssetSource(asset) }} + {{ asset.displayName }}
- + {{ getLastInput(asset) }} diff --git a/frontend/src/app/assets-discover/assets-view/assets-view.component.ts b/frontend/src/app/assets-discover/assets-view/assets-view.component.ts index f124e569a..27285420e 100644 --- a/frontend/src/app/assets-discover/assets-view/assets-view.component.ts +++ b/frontend/src/app/assets-discover/assets-view/assets-view.component.ts @@ -35,6 +35,7 @@ import {AssetFilterType} from '../shared/types/asset-filter.type'; import {UtmDataInputStatus} from '../shared/types/data-source-input.type'; import {NetScanType} from '../shared/types/net-scan.type'; import {SourceDataTypeConfigComponent} from '../source-data-type-config/source-data-type-config.component'; +import {SortDirection} from "../../shared/directives/sortable/type/sort-direction.type"; @Component({ selector: 'app-assets-view', @@ -126,13 +127,18 @@ export class AssetsViewComponent implements OnInit, OnDestroy { asset.dataInputList = []; } - return asset; + const displayName = asset.assetName && asset.assetIp ? `${asset.assetName} (${asset.assetIp})` + : asset.assetName ? asset.assetName : asset.assetIp ? asset.assetIp : 'Unknown source'; + + const sortKey = (asset.assetName || '') + (asset.assetIp || ''); + + return { ...asset, displayName, sortKey }; }); }) ); this.utmNetScanService.notifyRefresh(true); - this.starInterval(); + //this.starInterval(); } setInitialWidth() { @@ -147,14 +153,6 @@ export class AssetsViewComponent implements OnInit, OnDestroy { this.utmNetScanService.notifyRefresh(true); } - trackByFn(index: number, item: NetScanType) { - return item.id; - } - - trackByDataInputFn(index: number, item: UtmDataInputStatus) { - return item.dataType; - } - onItemsPerPageChange($event: number) { this.itemsPerPage = $event; this.requestParam.size = $event; @@ -180,8 +178,42 @@ export class AssetsViewComponent implements OnInit, OnDestroy { } onSortBy($event: SortEvent) { - this.requestParam.sort = $event.column + ',' + $event.direction; - this.utmNetScanService.notifyRefresh(true); + if ($event.column === 'displayName') { + this.sortAssets($event.direction); + } else { + this.requestParam.sort = $event.column + ',' + $event.direction; + this.getAssets(); + } + } + + sortAssets(direction: SortDirection) { + this.assets.sort((a, b) => { + + if (a.displayName === 'Unknown source') { return 1; } + if (b.displayName === 'Unknown source') { return -1; } + + const aVal = a.sortKey; + const bVal = b.sortKey; + + const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/; + const aIsIP = ipRegex.test(aVal); + const bIsIP = ipRegex.test(bVal); + + if (aIsIP && bIsIP) { + const aOctets = aVal.split('.').map(Number); + const bOctets = bVal.split('.').map(Number); + for (let i = 0; i < 4; i++) { + if (aOctets[i] !== bOctets[i]) { return direction === 'asc' ? aOctets[i] - bOctets[i] : bOctets[i] - aOctets[i]; } + } + return 0; + } + + if (aIsIP) { return direction === 'asc' ? -1 : 1; } + if (bIsIP) { return direction === 'asc' ? 1 : -1; } + + const cmp = aVal.localeCompare(bVal, undefined, { numeric: true, sensitivity: 'base' }); + return direction === 'asc' ? cmp : -cmp; + }); } toggleCheck() { @@ -308,19 +340,6 @@ export class AssetsViewComponent implements OnInit, OnDestroy { }); } - - getAssetSource(asset: NetScanType) { - if (asset.assetName && asset.assetIp) { - return asset.assetName + ' (' + asset.assetIp + ')'; - } else if (asset.assetName) { - return asset.assetName; - } else if (asset.assetIp) { - return asset.assetIp; - } else { - return 'Unknown source'; - } - } - navigateToDataManagement(ip: string) { const queryParams = {alertType: 'ALERT'}; queryParams[ALERT_SENSOR_FIELD] = ElasticOperatorsEnum.IS + ChartValueSeparator.BUCKET_SEPARATOR + ip; @@ -403,14 +422,22 @@ export class AssetsViewComponent implements OnInit, OnDestroy { if (!this.interval) { this.interval = setInterval(() => { this.utmNetScanService.notifyRefresh(true); - }, 30000); + }, 60000); } } - getAssets(){ + getAssets() { this.utmNetScanService.notifyRefresh(true); } + trackByFn(index: number, item: any) { + return item.id; + } + + trackByDataInputFn(index: number, item: UtmDataInputStatus) { + return item.id; + } + ngOnDestroy(): void { this.stopInterval(true); this.assetFiltersBehavior.$assetFilter.next(null); diff --git a/frontend/src/app/assets-discover/shared/types/net-scan.type.ts b/frontend/src/app/assets-discover/shared/types/net-scan.type.ts index 0d10ede9b..3d2b67cb0 100644 --- a/frontend/src/app/assets-discover/shared/types/net-scan.type.ts +++ b/frontend/src/app/assets-discover/shared/types/net-scan.type.ts @@ -35,4 +35,6 @@ export class NetScanType { assetOsPlatform?: string; assetOsMinorVersion?: string; assetOsMajorVersion?: string; + displayName?: string; + sortKey?: string; } diff --git a/frontend/src/app/compliance/shared/services/cp-standard.service.ts b/frontend/src/app/compliance/shared/services/cp-standard.service.ts index 68321a6eb..b80abc0c7 100644 --- a/frontend/src/app/compliance/shared/services/cp-standard.service.ts +++ b/frontend/src/app/compliance/shared/services/cp-standard.service.ts @@ -5,6 +5,7 @@ import {SERVER_API_URL} from '../../../app.constants'; import {createRequestOption} from '../../../shared/util/request-util'; import {ComplianceStandardType} from '../type/compliance-standard.type'; import {RefreshDataService} from '../../../shared/services/util/refresh-data.service'; +import {map} from "rxjs/operators"; @Injectable({ providedIn: 'root' @@ -38,7 +39,12 @@ export class CpStandardService extends RefreshDataService(this.resourceUrl, { params: options, observe: 'response' - }); + }).pipe( + map((response) => { + const data = response.body as ComplianceStandardType[]; + return response.clone({body: data.filter(s => s.id >= 500 ) || []}); + }) + ); } delete(standard: number): Observable> { diff --git a/frontend/src/app/core/auth/account.service.ts b/frontend/src/app/core/auth/account.service.ts index e394efcd2..02ec14cb5 100644 --- a/frontend/src/app/core/auth/account.service.ts +++ b/frontend/src/app/core/auth/account.service.ts @@ -6,6 +6,12 @@ import {SERVER_API_URL} from '../../app.constants'; import {HttpCancelService} from '../../blocks/service/httpcancel.service'; import {Account} from '../user/account.model'; import {AuthServerProvider} from './auth-jwt.service'; +import {extractQueryParamsForNavigation} from "../../shared/util/query-params-to-filter.util"; +import {ADMIN_DEFAULT_EMAIL, ADMIN_ROLE} from "../../shared/constants/global.constant"; +import {StateStorageService} from "./state-storage.service"; +import {Router} from "@angular/router"; +import {NgxSpinnerService} from "ngx-spinner"; +import {UtmToastService} from "../../shared/alert/utm-toast.service"; @Injectable({providedIn: 'root'}) export class AccountService { @@ -13,7 +19,13 @@ export class AccountService { private authenticated = false; private authenticationState = new Subject(); - constructor(private http: HttpClient, private authServerProvider: AuthServerProvider, private httpCancelService: HttpCancelService) { + constructor(private http: HttpClient, + private authServerProvider: AuthServerProvider, + private httpCancelService: HttpCancelService, + private stateStorageService: StateStorageService, + private router: Router, + private spinner: NgxSpinnerService, + private utmToast: UtmToastService) { } fetch(): Observable> { @@ -25,7 +37,8 @@ export class AccountService { } checkPassword(password: string, uuid: string): Observable> { - return this.http.get(SERVER_API_URL + `api/check-credentials?password=${password}&checkUUID=${uuid}`, { + const sanitized_password = encodeURIComponent(password) + return this.http.get(SERVER_API_URL + `api/check-credentials?password=${sanitized_password}&checkUUID=${uuid}`, { observe: 'response', responseType: 'text' }); @@ -126,4 +139,23 @@ export class AccountService { return this.userIdentity.openvasUserID; } } + + startNavigation() { + this.identity(true).then(account => { + if (account) { + const { path, queryParams } = + extractQueryParamsForNavigation(this.stateStorageService.getUrl() ? this.stateStorageService.getUrl() : '' ); + if (path) { + this.stateStorageService.resetPreviousUrl(); + } + const redirectTo = (account.authorities.includes(ADMIN_ROLE) && account.email === ADMIN_DEFAULT_EMAIL) + ? '/getting-started' : !!path ? path : '/dashboard/overview'; + console.log(redirectTo); + this.router.navigate([redirectTo], {queryParams}) + .then(() => this.spinner.hide()); + } else { + this.utmToast.showError('Login error', 'User without privileges.'); + } + }); + } } diff --git a/frontend/src/app/core/auth/auth-jwt.service.ts b/frontend/src/app/core/auth/auth-jwt.service.ts index 3f2737a4c..500f35118 100644 --- a/frontend/src/app/core/auth/auth-jwt.service.ts +++ b/frontend/src/app/core/auth/auth-jwt.service.ts @@ -14,6 +14,7 @@ import {CSRFService} from './csrf.service'; export class AuthServerProvider { private _tfaMethod: TfaMethod; + private _tfaExpireTimeInSec: number; constructor(private http: HttpClient, private $localStorage: LocalStorageService, @@ -35,8 +36,9 @@ export class AuthServerProvider { }; const authenticateSuccess = (resp: HttpResponse) => { this.storeAuthenticationToken(resp.body.token); - if (resp.body.method) { + if (resp.body.method && resp.body.forceTfa) { this.tfaMethod = resp.body.method as TfaMethod; + this.tfaExpireTimeInSec = resp.body.tfaExpiresInSeconds; } return resp.body; }; @@ -46,7 +48,7 @@ export class AuthServerProvider { } renewCode(): Observable { - return this.http.get(SERVER_API_URL + 'api/tfa/generate-challenge'); + return this.http.get(SERVER_API_URL + 'api/tfa/refresh'); } verifyCode(code): Observable { @@ -54,7 +56,7 @@ export class AuthServerProvider { this.storeAuthenticationToken(resp.body.id_token); return resp.body.authenticated; }; - return this.http.post(SERVER_API_URL + 'api/tfa/verifyCode', + return this.http.post(SERVER_API_URL + 'api/tfa/verify-code', code, {observe: 'response'}).pipe(map(authenticateSuccess.bind(this))); } @@ -112,4 +114,12 @@ export class AuthServerProvider { this._tfaMethod = ftaMethod; } + get tfaExpireTimeInSec(): number { + return this._tfaExpireTimeInSec; + } + + set tfaExpireTimeInSec(tfaExpireTimeInSec: number) { + this._tfaExpireTimeInSec = tfaExpireTimeInSec; + } + } diff --git a/frontend/src/app/data-management/alert-management/alert-management.module.ts b/frontend/src/app/data-management/alert-management/alert-management.module.ts index db2d1faaf..61177baef 100644 --- a/frontend/src/app/data-management/alert-management/alert-management.module.ts +++ b/frontend/src/app/data-management/alert-management/alert-management.module.ts @@ -18,6 +18,7 @@ import {AlertReportFilterComponent} from './alert-reports/shared/components/aler import {SaveAlertReportComponent} from './alert-reports/shared/components/save-report/save-report.component'; import {AlertViewComponent} from './alert-view/alert-view.component'; import {AlertManagementSharedModule} from './shared/alert-management-shared.module'; +import {InfiniteScrollModule} from "ngx-infinite-scroll"; @NgModule({ declarations: [ @@ -29,25 +30,25 @@ import {AlertManagementSharedModule} from './shared/alert-management-shared.modu AlertReportViewComponent, AlertFullDetailComponent, ], - imports: [ - CommonModule, - AlertManagementRouting, - UtmSharedModule, - FormsModule, - NgbModule, - AlertManagementSharedModule, - NgxJsonViewerModule, - ResizableModule, - TranslateModule, - NgSelectModule, - ReactiveFormsModule, - IncidentSharedModule - ], + imports: [ + CommonModule, + AlertManagementRouting, + UtmSharedModule, + FormsModule, + NgbModule, + AlertManagementSharedModule, + NgxJsonViewerModule, + ResizableModule, + TranslateModule, + NgSelectModule, + ReactiveFormsModule, + IncidentSharedModule, + InfiniteScrollModule + ], entryComponents: [ SaveAlertReportComponent, AlertReportFilterComponent], providers: [NewAlertBehavior, AlertIncidentStatusChangeBehavior], - exports: [], schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA] }) export class AlertManagementModule { diff --git a/frontend/src/app/data-management/alert-management/alert-rules/alert-rules.component.ts b/frontend/src/app/data-management/alert-management/alert-rules/alert-rules.component.ts index 6e8dfd4bb..a03468b4f 100644 --- a/frontend/src/app/data-management/alert-management/alert-rules/alert-rules.component.ts +++ b/frontend/src/app/data-management/alert-management/alert-rules/alert-rules.component.ts @@ -132,7 +132,11 @@ export class AlertRulesComponent implements OnInit { } createRule(rule?: AlertRuleType) { - const modal = this.modalService.open(AlertRuleCreateComponent, {centered: true, size: 'lg'}); + const modal = this.modalService.open(AlertRuleCreateComponent, { + centered: true, + size: 'lg', + windowClass: 'alert-rule-modal' + }); const falsePositive: AlertTags[] = [FALSE_POSITIVE_OBJECT]; if (rule) { modal.componentInstance.rule = rule; diff --git a/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html b/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html index e0c6fb69f..f63071799 100644 --- a/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html +++ b/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html @@ -86,189 +86,61 @@
-
+
- - - - - - - + - - - - - - - - - - - - - - - - - - + + - - - - - + + + - - - + + +
- - - - {{item.label}} -
-
-
- - - - - - - - - - -
-
-
- -
- - - -
- -
+ +
-
- - -
-
+
+ + +
+
@@ -297,15 +169,18 @@
-
-
+
+
{{getRuleName()}}
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.scss b/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.scss index 20f911a94..27f615cc4 100644 --- a/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.scss +++ b/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.scss @@ -20,3 +20,15 @@ min-height: 0; overflow: hidden; } + +.children-alert-container{ + height: 200px; +} + +tr.no-bottom-border { + border-bottom-width: 0 !important; +} + +.bg-children-light { + background-color: #cccccc52 !important; +} diff --git a/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.ts b/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.ts index 116ffe7c5..c8baca144 100644 --- a/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.ts +++ b/frontend/src/app/data-management/alert-management/alert-view/alert-view.component.ts @@ -8,9 +8,7 @@ import {NgxSpinnerService} from 'ngx-spinner'; import {LocalStorageService} from 'ngx-webstorage'; import {Observable, Subject} from 'rxjs'; import {filter, takeUntil, tap} from 'rxjs/operators'; -import { - IrCreateRuleComponent -} from '../../../incident-response/shared/component/ir-create-rule/ir-create-rule.component'; +import {TimelineItem} from 'src/app/shared/types/utm-timeline-item'; import {UtmToastService} from '../../../shared/alert/utm-toast.service'; import { ElasticFilterDefaultTime @@ -24,11 +22,12 @@ import { ALERT_STATUS_FIELD_AUTO, ALERT_STATUS_LABEL_FIELD, ALERT_TAGS_FIELD, ALERT_TARGET_FIELD, - ALERT_TIMESTAMP_FIELD, + ALERT_TIMESTAMP_FIELD, ALERTS_CHILDREN_FIELDS, EVENT_FIELDS, EVENT_IS_ALERT, FALSE_POSITIVE_OBJECT, - INCIDENT_FIELDS + INCIDENT_FIELDS, + ALERT_ECHOES_FIELD } from '../../../shared/constants/alert/alert-field.constant'; import {AUTOMATIC_REVIEW, IGNORED} from '../../../shared/constants/alert/alert-status.constant'; import {ADMIN_ROLE} from '../../../shared/constants/global.constant'; @@ -53,6 +52,7 @@ import {AlertFiltersBehavior} from '../shared/behavior/alert-filters.behavior'; import {AlertStatusBehavior} from '../shared/behavior/alert-status.behavior'; import {RowToFiltersComponent} from '../shared/components/filters/row-to-filter/row-to-filters.component'; import {EventDataTypeEnum} from '../shared/enums/event-data-type.enum'; +import {AlertActionRefreshService} from '../shared/services/alert-action-refresh.service'; import {AlertTagService} from '../shared/services/alert-tag.service'; import {OPEN_ALERTS_KEY, OpenAlertsService} from '../shared/services/open-alerts.service'; import {getCurrentAlertStatus, getStatusName} from '../shared/util/alert-util-function'; @@ -63,13 +63,35 @@ import {getCurrentAlertStatus, getStatusName} from '../shared/util/alert-util-fu styleUrls: ['./alert-view.component.scss'] }) export class AlertViewComponent implements OnInit, OnDestroy { + + + constructor(private elasticDataService: ElasticDataService, + private modalService: NgbModal, + private utmToastService: UtmToastService, + private translate: TranslateService, + private alertFiltersBehavior: AlertFiltersBehavior, + private updateStatusServiceBehavior: AlertStatusBehavior, + private activatedRoute: ActivatedRoute, + public router: Router, + private openAlertsService: OpenAlertsService, + private alertDataTypeBehavior: AlertDataTypeBehavior, + private alertTagService: AlertTagService, + private spinner: NgxSpinnerService, + private checkEmailConfigService: CheckEmailConfigService, + private localStorage: LocalStorageService, + private alertActionRefreshService: AlertActionRefreshService) { + // this.tableWidth = this.pageWidth - 300; + } fields = ALERT_FIELDS; + childrenFields = ALERTS_CHILDREN_FIELDS; manageTags: number; ADMIN = ADMIN_ROLE; alerts: UtmAlertType[] = []; + childrenAlerts: UtmAlertType[] = []; tableWidth: number; checkbox = false; loading = true; + loadingChildren = false; /** * Contains ID of selected alerts */ @@ -83,12 +105,16 @@ export class AlertViewComponent implements OnInit, OnDestroy { totalItems: any; page = 1; itemsPerPage = ITEMS_PER_PAGE; - // By default all alert will contain all except alerts in review filters: ElasticFilterType[] = [ {field: ALERT_STATUS_FIELD_AUTO, operator: ElasticOperatorsEnum.IS_NOT, value: AUTOMATIC_REVIEW}, {field: ALERT_TAGS_FIELD, operator: ElasticOperatorsEnum.IS_NOT, value: FALSE_POSITIVE_OBJECT.tagName}, + {field: ALERT_PARENT_ID, operator: ElasticOperatorsEnum.DOES_NOT_EXIST}, {field: ALERT_TIMESTAMP_FIELD, operator: ElasticOperatorsEnum.IS_BETWEEN, value: ['now-7d', 'now']} ]; + filtersChildren: ElasticFilterType[] = [ + {field: ALERT_STATUS_FIELD_AUTO, operator: ElasticOperatorsEnum.IS_NOT, value: AUTOMATIC_REVIEW}, + {field: ALERT_TAGS_FIELD, operator: ElasticOperatorsEnum.IS_NOT, value: FALSE_POSITIVE_OBJECT.tagName}, + ]; defaultStatus: number; dataNature = DataNatureTypeEnum.ALERT; sortEvent: SortEvent; @@ -109,24 +135,11 @@ export class AlertViewComponent implements OnInit, OnDestroy { openAlerts = 0; ALERT_ADVERSARY_FIELD = ALERT_ADVERSARY_FIELD; ALERT_TARGET_FIELD = ALERT_TARGET_FIELD; - - - constructor(private elasticDataService: ElasticDataService, - private modalService: NgbModal, - private utmToastService: UtmToastService, - private translate: TranslateService, - private alertFiltersBehavior: AlertFiltersBehavior, - private updateStatusServiceBehavior: AlertStatusBehavior, - private activatedRoute: ActivatedRoute, - public router: Router, - private openAlertsService: OpenAlertsService, - private alertDataTypeBehavior: AlertDataTypeBehavior, - private alertTagService: AlertTagService, - private spinner: NgxSpinnerService, - private checkEmailConfigService: CheckEmailConfigService, - private localStorage: LocalStorageService ) { - // this.tableWidth = this.pageWidth - 300; - } + currentChildrenPage = 1; + totalChildren: number; + pageSizeChildren = ITEMS_PER_PAGE * 4; + readonly ALERT_STATUS_FIELD = ALERT_STATUS_FIELD; + readonly Math = Math; ngOnInit() { this.openAlerts = this.localStorage.retrieve(OPEN_ALERTS_KEY); @@ -175,6 +188,20 @@ export class AlertViewComponent implements OnInit, OnDestroy { takeUntil(this.destroy$), tap(() => this.showRefresh = true), filter( incomingAlerts => this.openAlerts === null || this.openAlerts < incomingAlerts)); + + this.alertActionRefreshService.incidentCreated$ + .pipe( + takeUntil(this.destroy$), + filter(incident => !!incident), + tap(() => this.refreshAlerts()) + ).subscribe(); + + this.alertActionRefreshService.alertTagRuleCreated$ + .pipe( + takeUntil(this.destroy$), + filter(incident => !!incident), + tap(() => this.refreshAlerts()) + ).subscribe(); } refreshAlerts() { @@ -311,10 +338,10 @@ export class AlertViewComponent implements OnInit, OnDestroy { this.getAlert('on time filter change'); } - getAlert(calledFrom?: string) { + getAlert(calledFrom?: string, filtersParam?: ElasticFilterType[]) { this.elasticDataService.search(this.page, this.itemsPerPage, 100000000, this.dataNature, - sanitizeFilters(this.filters), this.sortBy, ALERT_PARENT_ID).subscribe( + sanitizeFilters(this.filters), this.sortBy, true).subscribe( (res: HttpResponse) => { this.totalItems = Number(res.headers.get('X-Total-Count')); this.alerts = res.body; @@ -350,6 +377,7 @@ export class AlertViewComponent implements OnInit, OnDestroy { } addToSelected(alert: any) { + console.log(alert); const index = this.alertSelected.indexOf(alert); if (index === -1) { this.alertSelected.push(alert); @@ -358,10 +386,6 @@ export class AlertViewComponent implements OnInit, OnDestroy { } } - isSelected(alert: any): boolean { - return this.alertSelected.findIndex(value => value.id === alert.id) !== -1; - } - onSortBy($event: SortEvent) { this.sortBy = $event.column + ',' + $event.direction; this.getAlert('on sort by'); @@ -381,7 +405,6 @@ export class AlertViewComponent implements OnInit, OnDestroy { }); } - loadPage(page: any) { this.page = page; this.getAlert('on load page'); @@ -453,8 +476,12 @@ export class AlertViewComponent implements OnInit, OnDestroy { return this.alertDetail.name; } - viewDetailAlert(alert: any, td: UtmFieldType) { - if (td.field !== ALERT_STATUS_FIELD) { + viewDetailAlert(alert: UtmAlertType, td: UtmFieldType) { + if (td.field !== ALERT_STATUS_FIELD && td.field !== ALERT_ECHOES_FIELD) { + if (alert.echoes > 0) { + alert.expanded = true; + this.loadChildrenAlerts(alert); + } this.alertDetail = alert; this.viewAlertDetail = true; } @@ -555,20 +582,29 @@ export class AlertViewComponent implements OnInit, OnDestroy { openIncidentResponseAutomationModal(alert: UtmAlertType) { this.router.navigate(['soar/create-flow'], { - queryParams:{alertName:alert.name} - }) + queryParams: {alertName: alert.name} + }); } getFilterTime() { return this.filters.find(f => f.field === ALERT_TIMESTAMP_FIELD); } + loadChildrenAlerts(alert: UtmAlertType) { + if (alert.expanded) { + this.alerts = this.alerts.map(a => { + if (a.id !== alert.id) { + a.expanded = false; + } + return a; + }); + } + + } + ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); - } - - getAlertById(parentId: string) { - return this.alerts.filter(alert => alert.parentId === parentId); + this.alertActionRefreshService.clearValues(); } } diff --git a/frontend/src/app/data-management/alert-management/shared/alert-management-shared.module.ts b/frontend/src/app/data-management/alert-management/shared/alert-management-shared.module.ts index d3b6904de..c98748668 100644 --- a/frontend/src/app/data-management/alert-management/shared/alert-management-shared.module.ts +++ b/frontend/src/app/data-management/alert-management/shared/alert-management-shared.module.ts @@ -17,6 +17,7 @@ import {AlertTagsCreateComponent} from '../alert-tags/alert-tags-create/alert-ta import {AlertTagsManagementComponent} from '../alert-tags/alert-tags-management.component'; import {AlertTagsRenderComponent} from '../alert-tags/alert-tags-render/alert-tags-render.component'; import {AlertTagsViewComponent} from '../alert-tags/alert-tags-view/alert-tags-view.component'; +import { AlertActionsContentComponent } from './components/alert-actions-content/alert-actions-content.component'; import {AlertApplyIncidentComponent} from './components/alert-actions/alert-apply-incident/alert-apply-incident.component'; import {AlertApplyNoteComponent} from './components/alert-actions/alert-apply-note/alert-apply-note.component'; import {AlertApplyStatusComponent} from './components/alert-actions/alert-apply-status/alert-apply-status.component'; @@ -29,12 +30,16 @@ import {AlertCategoryComponent} from './components/alert-category/alert-category import {AlertCompleteComponent} from './components/alert-complete/alert-complete.component'; import {AlertDescriptionComponent} from './components/alert-description/alert-description.component'; import {AlertDocUpdateInProgressComponent} from './components/alert-doc-update-in-progress/alert-doc-update-in-progress.component'; +import {AlertEchoesComponent} from "./components/alert-echoes/alert-echoes.component"; import { AlertEntityDisplayComponent } from './components/alert-entity-display/alert-entity-display.component'; +import {AlertEventsRelatedComponent} from './components/alert-events-related/alert-events-related.component'; import {AlertFullLogComponent} from './components/alert-full-log/alert-full-log.component'; import {AlertHistoryComponent} from './components/alert-history/alert-history.component'; import {AlertHostDetailComponent} from './components/alert-host-detail/alert-host-detail.component'; +import {AlertImpactComponent} from './components/alert-impact/alert-impact.component'; import {AlertIncidentDetailComponent} from './components/alert-incident-detail/alert-incident-detail.component'; import {AlertIpComponent} from './components/alert-ip/alert-ip.component'; +import {AlertLogsRelatedButtonComponent} from './components/alert-logs-related-button/alert-logs-related-button.component'; import {AlertMapLocationComponent} from './components/alert-map-location/alert-map-location.component'; import {AlertProposedSolutionComponent} from './components/alert-proposed-solution/alert-proposed-solution.component'; import {AlertRuleCreateComponent} from './components/alert-rule-create/alert-rule-create.component'; @@ -54,7 +59,9 @@ import {AlertGenericFilterComponent} from './components/filters/alert-generic-fi import {FilterAppliedComponent} from './components/filters/filter-applied/filter-applied.component'; import {RowToFiltersComponent} from './components/filters/row-to-filter/row-to-filters.component'; import {StatusFilterComponent} from './components/filters/status-filter/status-filter.component'; -import {AlertImpactComponent} from "./components/alert-impact/alert-impact.component"; +import { AlertActionSelectComponent } from './components/alert-action-select/alert-action-select.component'; +import { AlertChildColumnComponent } from './components/alert-child-column/alert-child-column.component'; +import {AlertEchoesTimelineService} from "./components/alert-echoes-timeline/alert-echoes-timeline.service"; @NgModule({ declarations: [ @@ -80,6 +87,7 @@ import {AlertImpactComponent} from "./components/alert-impact/alert-impact.compo AlertIpComponent, AlertHistoryComponent, AlertMapLocationComponent, + AlertChildColumnComponent, AlertSeverityDescriptionComponent, AlertFullLogComponent, AlertHostDetailComponent, @@ -99,7 +107,12 @@ import {AlertImpactComponent} from "./components/alert-impact/alert-impact.compo AlertIncidentDetailComponent, AlertSocAiComponent, AlertEntityDisplayComponent, - AlertBadgeFieldComponent + AlertBadgeFieldComponent, + AlertLogsRelatedButtonComponent, + AlertEventsRelatedComponent, + AlertActionsContentComponent, + AlertEchoesComponent, + AlertActionSelectComponent ], entryComponents: [ AlertStatusComponent, @@ -122,6 +135,7 @@ import {AlertImpactComponent} from "./components/alert-impact/alert-impact.compo ActiveFiltersComponent, RowToFiltersComponent, AlertGenericFilterComponent, + AlertChildColumnComponent, AlertFilterComponent, DataFieldRenderComponent, StatusFilterComponent, @@ -152,23 +166,29 @@ import {AlertImpactComponent} from "./components/alert-impact/alert-impact.compo AlertIncidentDetailComponent, AlertSocAiComponent, AlertEntityDisplayComponent, - AlertBadgeFieldComponent + AlertBadgeFieldComponent, + AlertEventsRelatedComponent, + AlertActionsContentComponent, + AlertLogsRelatedButtonComponent + ], + imports: [ + CommonModule, + TranslateModule, + UtmSharedModule, + FormsModule, + NgbModule, + ReactiveFormsModule, + NgSelectModule, + InfiniteScrollModule, + NgxJsonViewerModule, + IncidentResponseSharedModule, + DataMgmtSharedModule, + RouterModule, + InlineSVGModule, + ], + providers: [ + AlertEchoesTimelineService ], - imports: [ - CommonModule, - TranslateModule, - UtmSharedModule, - FormsModule, - NgbModule, - ReactiveFormsModule, - NgSelectModule, - InfiniteScrollModule, - NgxJsonViewerModule, - IncidentResponseSharedModule, - DataMgmtSharedModule, - RouterModule, - InlineSVGModule, - ] }) export class AlertManagementSharedModule { } diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-action-select/alert-action-select.component.css b/frontend/src/app/data-management/alert-management/shared/components/alert-action-select/alert-action-select.component.css new file mode 100644 index 000000000..e34dab007 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-action-select/alert-action-select.component.css @@ -0,0 +1,236 @@ +:host { + display: flex; + width: fit-content; +} + +.span-small-icon > span:last-child { + width: 25px; +} + +.utm-alert-action-select .popover-content { + padding: 0; + border-radius: 12px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + border: 1px solid #e1e5e9; + overflow: hidden; +} + +.utm-alert-action-select .popover { + border-radius: 12px; + border: none; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +.utm-alert-action-select .arrow, +.utm-alert-action-select .arrow:after { + border-bottom-color: white; +} + +.utm-action-menu { + background: white; + font-size: 13px; +} + +/* Headers de grupos mejorados */ +.utm-action-menu .list-group-header { + background: #f8f9fa; + border: none; + color: #6c757d; + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 12px 16px 8px; + margin-bottom: 4px; + border-bottom: 1px solid #e9ecef; +} + +.utm-action-menu .list-group-header:not(:first-child) { + margin-top: 8px; + border-top: 1px solid #e9ecef; + padding-top: 12px; +} + +.utm-action-menu .span-small-icon { + display: block; + padding: 10px 16px; + text-decoration: none; + border: none; + background: white; + transition: all 0.2s ease; + cursor: pointer; + position: relative; + margin: 1px 0; +} + +.utm-action-menu .span-small-icon:hover { + background: linear-gradient(90deg, #f8f9fa, #e9ecef); + padding-left: 20px; + border-left: 3px solid #007bff; + text-decoration: none; +} + +.utm-action-menu .span-small-icon:focus { + background: #e7f3ff; + outline: none; + border-left: 3px solid #007bff; + padding-left: 20px; +} + +.utm-action-menu .span-small-icon i { + margin-right: 10px; + width: 16px; + text-align: center; + transition: color 0.2s ease; +} + +.utm-action-menu .position-relative { + position: relative; +} + +.utm-action-menu .position-relative .pull-right { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + color: #adb5bd; + font-size: 12px; + transition: all 0.2s ease; +} + +.utm-action-menu .position-relative:hover .pull-right { + color: #007bff; + right: 8px; +} + +.utm-action-menu .bg-success:hover { + border-left-color: #28a745; + background: linear-gradient(90deg, #f8fff8, #e6f7e6); +} + +.utm-action-menu .bg-success:hover i { + color: #28a745; +} + +.utm-action-menu .bg-info:hover { + border-left-color: #17a2b8; + background: linear-gradient(90deg, #f8fdff, #e6f7ff); +} + +.utm-action-menu .bg-info:hover i { + color: #17a2b8; +} + +.utm-action-menu .bg-warning:hover { + border-left-color: #ffc107; + background: linear-gradient(90deg, #fffef8, #fff8e6); +} + +.utm-action-menu .bg-warning:hover i { + color: #e0a800; +} + +.utm-action-menu .bg-danger:hover { + border-left-color: #dc3545; + background: linear-gradient(90deg, #fff8f8, #ffe6e6); +} + +.utm-action-menu .bg-danger:hover i { + color: #dc3545; +} + +.utm-submenu-popover .popover { + margin-left: 8px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.utm-submenu-popover .popover-content { + padding: 4px 0; + border-radius: 8px; +} + +.utm-submenu .list-group-item { + border: none; + padding: 8px 16px; + background: white; + color: #495057; + font-size: 12px; + transition: all 0.15s ease; + margin: 1px 0; +} + +.utm-submenu .list-group-item:hover { + background: #f8f9fa; + color: #212529; + padding-left: 20px; + text-decoration: none; +} + +.utm-submenu .list-group-item:first-child { + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +.utm-submenu .list-group-item:last-child { + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(5px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.utm-action-menu .span-small-icon { + animation: fadeInUp 0.15s ease forwards; +} + +.utm-action-menu .span-small-icon:nth-child(2) { animation-delay: 0.02s; } +.utm-action-menu .span-small-icon:nth-child(3) { animation-delay: 0.04s; } +.utm-action-menu .span-small-icon:nth-child(4) { animation-delay: 0.06s; } +.utm-action-menu .span-small-icon:nth-child(5) { animation-delay: 0.08s; } +.utm-action-menu .span-small-icon:nth-child(6) { animation-delay: 0.1s; } + +.utm-action-menu .span-small-icon.active, +.utm-action-menu .span-small-icon:active { + background: #e7f3ff; + color: #0056b3; + border-left: 3px solid #007bff; + padding-left: 20px; +} + +/* Responsive para mรณviles */ +@media (max-width: 767px) { + .utm-action-menu { + min-width: 250px; + font-size: 12px; + } + + .utm-action-menu .span-small-icon { + padding: 8px 12px; + } + + .utm-action-menu .list-group-header { + padding: 10px 12px 6px; + font-size: 10px; + } +} + +.utm-alert-action-select-trigger { + transition: all 0.2s ease; +} + +.utm-alert-action-select-trigger:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); +} +i{ + font-size: 12px; +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-action-select/alert-action-select.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-action-select/alert-action-select.component.html new file mode 100644 index 000000000..b05c09fc3 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-action-select/alert-action-select.component.html @@ -0,0 +1,125 @@ +
+ +
+
+   + {{ label | translate}} +
+ + + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-action-select/alert-action-select.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-action-select/alert-action-select.component.ts new file mode 100644 index 000000000..7fcf92eff --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-action-select/alert-action-select.component.ts @@ -0,0 +1,225 @@ +import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core'; +import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {TranslateService} from '@ngx-translate/core'; +import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; +import {AlertIncidentStatusChangeBehavior} from '../../../../../shared/behaviors/alert-incident-status-change.behavior'; +import { + CLOSED, + CLOSED_ICON, + IGNORED, + IGNORED_ICON, + OPEN, + OPEN_ICON, + REVIEW, + REVIEW_ICON +} from '../../../../../shared/constants/alert/alert-status.constant'; +import {UtmIncidentAlertsService} from '../../../../../shared/services/incidents/utm-incident-alerts.service'; +import {AlertStatusEnum, UtmAlertType} from '../../../../../shared/types/alert/utm-alert.type'; +import {AlertIncidentStatusUpdateType} from '../../../../../shared/types/incident/alert-incident-status-update.type'; +import {AlertStatusBehavior} from '../../behavior/alert-status.behavior'; +import {AlertUpdateHistoryBehavior} from '../../behavior/alert-update-history.behavior'; +import {EventDataTypeEnum} from '../../enums/event-data-type.enum'; +import {AlertManagementService} from '../../services/alert-management.service'; +import {getStatusName} from '../../util/alert-util-function'; +import {AlertCompleteComponent} from '../alert-complete/alert-complete.component'; + +export enum AlertActionType { + ADD_OR_CREATE_INCIDENT, + ADD_FALSE_POSITIVE_TAG_RULE, + ADD_INCIDENT, + CREATE_INCIDENT +} + +export interface AlertAction { + label: string; + value?: AlertStatusEnum; + group: string; + subActions?: AlertAction[]; + icon?: string; + background?: string; + action?: AlertActionType; +} + +@Component({ + selector: 'app-alert-action-select', + templateUrl: './alert-action-select.component.html', + styleUrls: ['./alert-action-select.component.css'] +}) +export class AlertActionSelectComponent implements OnInit, OnDestroy { + + @Input() alert: UtmAlertType; + @Input() showDrop: boolean; + @Input() statusField: string; + @Output() statusChange = new EventEmitter(); + @Input() status: number; + @Input() tags: any[]; + @Input() dataType: EventDataTypeEnum; + + + rawActions: AlertAction[] = [ + { label: 'Open', value: AlertStatusEnum.OPEN , group: 'Estado', icon: OPEN_ICON, background: 'border-success-400 text-success-400' }, + { label: 'In Review', value: AlertStatusEnum.IN_REVIEW, group: 'Estado', + icon: REVIEW_ICON, background: 'border-info-400 text-info-400' }, + {label: 'Completed', value: AlertStatusEnum.COMPLETED, group: 'Estado', icon: CLOSED_ICON, + background: 'border-blue-800 text-blue-800'}, + {label: 'Completed as False Positive', value: AlertStatusEnum.COMPLETED_AS_FALSE_POSITIVE, group: 'Estado', icon: 'icon-checkmark', + background: 'border-warning-400 text-warning-800'}, + /*subActions: [ + { label: 'Completed', value: 'completed_plain', group: 'Estado', icon: CLOSED_ICON, + background: 'border-blue-800 text-blue-800', }, + { label: 'Completed โ€“ Has False Positive', value: 'completed_fp', group: 'Estado', icon: CLOSED_ICON, + background: 'border-blue-800 text-blue-800', } + ]*/ + { label: 'Manage Incident', + value: AlertStatusEnum.AUTOMATIC_REVIEW, + group: 'Acciones', + icon: 'icon-target', + background: 'border-blue-800 text-blue-800', + action: AlertActionType.ADD_OR_CREATE_INCIDENT, + subActions: [ + { label: 'Add to Incident', group: 'Estado', icon: 'icon-make-group', + background: 'border-blue-800 text-blue-800', action: AlertActionType.ADD_INCIDENT }, + { label: 'Create an Incident', group: 'Estado', icon: 'icon-plus2', + background: 'border-blue-800 text-blue-800', action: AlertActionType.CREATE_INCIDENT }, + ]}, + { label: 'Create False Positive Rule', value: AlertStatusEnum.AUTOMATIC_REVIEW, group: 'Acciones', + icon: 'icon-price-tag3', background: 'border-blue-800 text-blue-800', action: AlertActionType.ADD_FALSE_POSITIVE_TAG_RULE }, + ]; + + actionGroups: { [group: string]: AlertAction[] } = {}; + icon: string; + background: string; + label: string; + changing = false; + isIncident: boolean; + incidentId: number; + AlertActionType = AlertActionType; + + constructor(private alertServiceManagement: AlertManagementService, + private modalService: NgbModal, + private translate: TranslateService, + private alertUpdateHistoryBehavior: AlertUpdateHistoryBehavior, + private updateStatusServiceBehavior: AlertStatusBehavior, + private alertIncidentStatusChangeBehavior: AlertIncidentStatusChangeBehavior, + private utmIncidentAlertsService: UtmIncidentAlertsService, + private utmToastService: UtmToastService) { } + + ngOnInit() { + this.actionGroups = this.rawActions.reduce((acc, action) => { + if (!acc[action.group]) { acc[action.group] = []; } + acc[action.group].push(action); + return acc; + }, {} as { [group: string]: AlertAction[] }); + + if (typeof this.status === 'string') { + this.status = Number(this.status); + } + if (!this.status) { + this.status = this.alert.status; + } + this.isIncident = this.alert.isIncident; + if (this.isIncident) { + this.incidentId = Number(this.alert.incidentDetail.incidentId); + } + + this.resolveAlert(); + } + + + handleAction(action: any) { + + } + + private resolveAlert() { + switch (this.status) { + case OPEN: + this.icon = OPEN_ICON; + this.background = 'border-success-400 text-success-400'; + this.label = 'alertStatus.open'; + break; + case REVIEW: + this.icon = REVIEW_ICON; + this.background = 'border-info-400 text-info-400'; + this.label = 'alertStatus.inReview'; + break; + case CLOSED: + this.icon = CLOSED_ICON; + this.background = 'border-blue-800 text-blue-800'; + this.label = 'alertStatus.closed'; + break; + case IGNORED: + this.icon = IGNORED_ICON; + this.background = 'border-warning-400 text-warning-400'; + this.label = 'alertStatus.ignored'; + break; + default: + this.icon = 'icon-hammer'; + this.background = 'border-slate-800 text-slate-800'; + this.label = 'alertStatus.pending'; + break; + } + } + + changeStatus(status: number) { + const alert = this.alert; + this.changing = true; + if (status === AlertStatusEnum.COMPLETED || status === AlertStatusEnum.COMPLETED_AS_FALSE_POSITIVE) { + console.log('status', status); + const modalRef = this.modalService.open(AlertCompleteComponent, {centered: true}); + modalRef.componentInstance.alertsIDs = [alert.id]; + modalRef.componentInstance.canCreateRule = true; + modalRef.componentInstance.status = AlertStatusEnum.COMPLETED; + modalRef.componentInstance.asFalsePositive = status === AlertStatusEnum.COMPLETED_AS_FALSE_POSITIVE; + modalRef.componentInstance.alert = this.alert; + modalRef.componentInstance.statusClose.subscribe(() => { + this.changing = false; + }); + + modalRef.componentInstance.statusChange.subscribe((statusChange) => { + this.changing = false; + if (statusChange === 'success') { + this.statusChange.emit(status); + this.alertUpdateHistoryBehavior.$refreshHistory.next(true); + this.statusChangedSuccess(status); + this.syncIncidentAlertStatus([this.alert.id], status); + } else { + this.changing = false; + } + }); + } else { + this.alertServiceManagement.updateAlertStatus([this.alert.id], status).subscribe(al => { + this.statusChangedSuccess(status); + this.changing = false; + this.alertUpdateHistoryBehavior.$refreshHistory.next(true); + this.syncIncidentAlertStatus([this.alert.id], status); + }); + } + } + + statusChangedSuccess(status) { + this.updateStatusServiceBehavior.$updateStatus.next(true); + this.status = status; + this.resolveAlert(); + this.statusChange.emit(status); + const msg = getStatusName(status); + this.translate.get(['toast.changeAlertStatus', msg]).subscribe(value => { + this.utmToastService.showSuccessBottom(value['toast.changeAlertStatus'] + ' ' + value[msg].toString().toUpperCase()); + }); + } + + syncIncidentAlertStatus(alerts: string[], status: number) { + if (this.isIncident) { + const update: AlertIncidentStatusUpdateType = { + status, + incidentId: Number(this.incidentId), + alertIds: alerts + }; + this.utmIncidentAlertsService.updateIncidentAlertStatus(update).subscribe(() => { + this.alertIncidentStatusChangeBehavior.$incidentAlertChange.next(this.incidentId); + }); + } + } + + ngOnDestroy(): void { + } +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-actions-content/alert-actions-content.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-actions-content/alert-actions-content.component.html new file mode 100644 index 000000000..cca7171a3 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-actions-content/alert-actions-content.component.html @@ -0,0 +1,38 @@ +
+
+ + + + + + + + +
+
+ + + + diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-actions-content/alert-actions-content.component.scss b/frontend/src/app/data-management/alert-management/shared/components/alert-actions-content/alert-actions-content.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-actions-content/alert-actions-content.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-actions-content/alert-actions-content.component.ts new file mode 100644 index 000000000..aff31c467 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-actions-content/alert-actions-content.component.ts @@ -0,0 +1,56 @@ +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; + +@Component({ + selector: 'app-alert-actions-content', + templateUrl: './alert-actions-content.component.html', + styleUrls: ['./alert-actions-content.component.scss'] +}) +export class AlertActionsContentComponent { + + @Input() alert: any; + @Input() dataType: any; + @Input() tags: any[]; + @Input() loadingChildren = false; + @Input() alertSelected = []; + + @Output() toggleExpand = new EventEmitter(); + @Output() select = new EventEmitter(); + @Output() filterRow = new EventEmitter(); + @Output() incident = new EventEmitter(); + @Output() openAutomation = new EventEmitter(); + @Output() applyNote = new EventEmitter(); + @Output() applyTags = new EventEmitter(); + + onToggleExpand() { + this.toggleExpand.emit(this.alert); + } + + onSelect() { + this.select.emit(this.alert); + } + + onFilterRow() { + this.filterRow.emit(this.alert); + } + + onIncident(event: any) { + this.incident.emit(event); + } + + onOpenAutomation() { + this.openAutomation.emit(this.alert); + } + + onApplyNote(event: any) { + this.applyNote.emit(event); + } + + onApplyTags() { + this.applyTags.emit(); + } + + isSelected(alert: any): boolean { + return this.alertSelected.findIndex(value => value.id === alert.id) !== -1; + } + +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.html index 24a7ad693..4629d6675 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.html +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.html @@ -1,8 +1,9 @@ -
+ +
-
- - - - - -
Add incident
-
+ + + + + +
Add incident
+
+ + + + +
- - - + + + diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.scss b/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.scss index e69de29bb..d7cd2d0e8 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.scss +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.scss @@ -0,0 +1,228 @@ +.span-small-icon > span:last-child { + width: 25px; +} + +.utm-alert-action-select .popover-content { + padding: 0; + border-radius: 12px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + border: 1px solid #e1e5e9; + overflow: hidden; +} + +.utm-alert-action-select .popover { + border-radius: 12px; + border: none; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +.utm-alert-action-select .arrow, +.utm-alert-action-select .arrow:after { + border-bottom-color: white; +} + +.utm-action-menu { + background: white; + font-size: 13px; +} + +/* Headers de grupos mejorados */ +.utm-action-menu .list-group-header { + background: #f8f9fa; + border: none; + color: #6c757d; + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 12px 16px 8px; + margin-bottom: 4px; + border-bottom: 1px solid #e9ecef; +} + +.utm-action-menu .list-group-header:not(:first-child) { + margin-top: 8px; + border-top: 1px solid #e9ecef; + padding-top: 12px; +} + +.utm-action-menu .span-small-icon { + display: block; + padding: 10px 16px; + text-decoration: none; + border: none; + background: white; + transition: all 0.2s ease; + cursor: pointer; + position: relative; + margin: 1px 0; +} + +.utm-action-menu .span-small-icon:hover { + background: linear-gradient(90deg, #f8f9fa, #e9ecef); + padding-left: 20px; + border-left: 3px solid #007bff; + text-decoration: none; +} + +.utm-action-menu .span-small-icon:focus { + background: #e7f3ff; + outline: none; + border-left: 3px solid #007bff; + padding-left: 20px; +} + +.utm-action-menu .span-small-icon i { + margin-right: 10px; + width: 16px; + text-align: center; + transition: color 0.2s ease; +} + +.utm-action-menu .position-relative { + position: relative; +} + +.utm-action-menu .position-relative .pull-right { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + color: #adb5bd; + font-size: 12px; + transition: all 0.2s ease; +} + +.utm-action-menu .position-relative:hover .pull-right { + color: #007bff; + right: 8px; +} + +.utm-action-menu .bg-success:hover { + border-left-color: #28a745; + background: linear-gradient(90deg, #f8fff8, #e6f7e6); +} + +.utm-action-menu .bg-success:hover i { + color: #28a745; +} + +.utm-action-menu .bg-info:hover { + border-left-color: #17a2b8; + background: linear-gradient(90deg, #f8fdff, #e6f7ff); +} + +.utm-action-menu .bg-info:hover i { + color: #17a2b8; +} + +.utm-action-menu .bg-warning:hover { + border-left-color: #ffc107; + background: linear-gradient(90deg, #fffef8, #fff8e6); +} + +.utm-action-menu .bg-warning:hover i { + color: #e0a800; +} + +.utm-action-menu .bg-danger:hover { + border-left-color: #dc3545; + background: linear-gradient(90deg, #fff8f8, #ffe6e6); +} + +.utm-action-menu .bg-danger:hover i { + color: #dc3545; +} + +.utm-submenu-popover .popover { + margin-left: 8px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.utm-submenu-popover .popover-content { + padding: 4px 0; + border-radius: 8px; +} + +.utm-submenu .list-group-item { + border: none; + padding: 8px 16px; + background: white; + color: #495057; + font-size: 12px; + transition: all 0.15s ease; + margin: 1px 0; +} + +.utm-submenu .list-group-item:hover { + background: #f8f9fa; + color: #212529; + padding-left: 20px; + text-decoration: none; +} + +.utm-submenu .list-group-item:first-child { + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +.utm-submenu .list-group-item:last-child { + border-bottom-left-radius: 8px; + border-bottom-right-radius: 8px; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(5px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.utm-action-menu .span-small-icon { + animation: fadeInUp 0.15s ease forwards; +} + +.utm-action-menu .span-small-icon:nth-child(2) { animation-delay: 0.02s; } +.utm-action-menu .span-small-icon:nth-child(3) { animation-delay: 0.04s; } +.utm-action-menu .span-small-icon:nth-child(4) { animation-delay: 0.06s; } +.utm-action-menu .span-small-icon:nth-child(5) { animation-delay: 0.08s; } +.utm-action-menu .span-small-icon:nth-child(6) { animation-delay: 0.1s; } + +.utm-action-menu .span-small-icon.active, +.utm-action-menu .span-small-icon:active { + background: #e7f3ff; + color: #0056b3; + border-left: 3px solid #007bff; + padding-left: 20px; +} + +/* Responsive para mรณviles */ +@media (max-width: 767px) { + .utm-action-menu { + min-width: 250px; + font-size: 12px; + } + + .utm-action-menu .span-small-icon { + padding: 8px 12px; + } + + .utm-action-menu .list-group-header { + padding: 10px 12px 6px; + font-size: 10px; + } +} + +.utm-alert-action-select-trigger { + transition: all 0.2s ease; +} + +.utm-alert-action-select-trigger:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transform: translateY(-1px); +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.ts index 044d03163..8e719fe0f 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.ts +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-incident/alert-apply-incident.component.ts @@ -3,12 +3,16 @@ import {NgbModal, NgbPopover} from '@ng-bootstrap/ng-bootstrap'; import { AddAlertToIncidentComponent } from '../../../../../../incident/incident-shared/component/add-alert-to-incident/add-alert-to-incident.component'; -import {CreateIncidentComponent} from '../../../../../../incident/incident-shared/component/create-incident/create-incident.component'; +import { + CreateIncidentComponent +} from '../../../../../../incident/incident-shared/component/create-incident/create-incident.component'; import {UtmToastService} from '../../../../../../shared/alert/utm-toast.service'; import {UtmAlertType} from '../../../../../../shared/types/alert/utm-alert.type'; import {AlertUpdateHistoryBehavior} from '../../../behavior/alert-update-history.behavior'; import {EventDataTypeEnum} from '../../../enums/event-data-type.enum'; import {AlertManagementService} from '../../../services/alert-management.service'; +import {AlertActionType} from '../../alert-action-select/alert-action-select.component'; + @Component({ selector: 'app-alert-apply-incident', templateUrl: './alert-apply-incident.component.html', @@ -22,6 +26,9 @@ export class AlertApplyIncidentComponent implements OnInit { @Input() alerts: any[]; @Input() multiple = false; @Input() eventType: EventDataTypeEnum; + @Input() template: 'default' | 'menu-item' = 'default'; + @Input() actions: any[]; + @Output() markAsIncident = new EventEmitter(); @ViewChild('incidentPopoverSpan') incidentPopoverSpan: NgbPopover; @ViewChild('incidentPopoverButton') incidentPopoverButton: NgbPopover; @@ -93,4 +100,11 @@ export class AlertApplyIncidentComponent implements OnInit { } } + handleAction(action: AlertActionType) { + if (action === AlertActionType.CREATE_INCIDENT) { + this.createIncident(); + } else { + this.addToIncident(); + } + } } diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-tags/alert-tags-apply.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-tags/alert-tags-apply.component.html index 884f57e7b..1231f790e 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-tags/alert-tags-apply.component.html +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-tags/alert-tags-apply.component.html @@ -1,7 +1,8 @@ -
+ +
@@ -15,9 +16,19 @@ -
- +
+ - + + + + + +   + {{ action.label }} + + + + diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-tags/alert-tags-apply.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-tags/alert-tags-apply.component.ts index d427ea542..0ea329278 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-tags/alert-tags-apply.component.ts +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-actions/alert-apply-tags/alert-tags-apply.component.ts @@ -4,16 +4,19 @@ import {AlertTags} from '../../../../../../shared/types/alert/alert-tag.type'; import {UtmAlertType} from '../../../../../../shared/types/alert/utm-alert.type'; import {AlertUpdateTagBehavior} from '../../../behavior/alert-update-tag.behavior'; import {AlertRuleCreateComponent} from '../../alert-rule-create/alert-rule-create.component'; +import { FALSE_POSITIVE_OBJECT } from 'src/app/shared/constants/alert/alert-field.constant'; @Component({ selector: 'app-alert-tags-apply', templateUrl: './alert-tags-apply.component.html', - styleUrls: ['./alert-tags-apply.component.scss'] + styleUrls: ['./alert-tags-apply.component.scss'], }) export class AlertTagsApplyComponent implements OnInit, OnChanges { @Input() showTagsLabel: boolean; @Input() alert: UtmAlertType; @Input() tags: AlertTags[]; + @Input() template: 'default' | 'menu-item' = 'default'; + @Input() action: any; selected: string[] = []; select: any; @Output() updateTagsEvent = new EventEmitter(); @@ -49,10 +52,15 @@ export class AlertTagsApplyComponent implements OnInit, OnChanges { } } - addNewTagRule() { - const modalRef = this.modalService.open(AlertRuleCreateComponent, {centered: true, size: 'lg'}); + addNewTagRule(isFalsePositive: boolean = false) { + const modalRef = this.modalService.open(AlertRuleCreateComponent, { + centered: true, + size: 'lg', + windowClass: 'alert-rule-modal' + }); modalRef.componentInstance.alert = this.alert; modalRef.componentInstance.action = 'select'; + modalRef.componentInstance.isFalsePositiveRule = isFalsePositive; modalRef.componentInstance.ruleAdd.subscribe((created) => { this.icon = this.getTagIcon(); this.color = this.getColor(); diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-badge-field/alert-badge-field.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-badge-field/alert-badge-field.component.html index 8257f06b5..c4640208d 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-badge-field/alert-badge-field.component.html +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-badge-field/alert-badge-field.component.html @@ -1,6 +1,6 @@
- + {{ value }}
diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-child-column/alert-child-column.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-child-column/alert-child-column.component.html new file mode 100644 index 000000000..5e5216f06 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-child-column/alert-child-column.component.html @@ -0,0 +1,15 @@ + + + + diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-child-column/alert-child-column.component.scss b/frontend/src/app/data-management/alert-management/shared/components/alert-child-column/alert-child-column.component.scss new file mode 100644 index 000000000..8700ead70 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-child-column/alert-child-column.component.scss @@ -0,0 +1,54 @@ +.btn-expand { + // border: 1px solid #004b8b; + background-color: white; + color: #004b8b ; + cursor: pointer; + transition: all 0.2s ease-in-out; + gap:0.3rem; +} + +.text-xs{ + font-size:0.58rem !important; +} + + + +.btn-expand:hover:not(:disabled) { + border-color: #004b8b ; +} + +.btn-expand:active:not(:disabled) { + background-color: #004b8b ; + // border-color: #004b8a; + transform: scale(0.98); +} + +.btn-expand:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.btn-expand i { + font-size: 12px; + display: flex; + align-items: center; + align-content: right; +} + +.btn-expand .spinner { + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.no-children-spacer { + display: inline-block; + width: 70px; + height: 32px; +} +.echoes-icons{ + filter: invert(30%) sepia(94%) saturate(2000%) hue-rotate(190deg) brightness(95%) contrast(101%); +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-child-column/alert-child-column.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-child-column/alert-child-column.component.ts new file mode 100644 index 000000000..9e6316920 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-child-column/alert-child-column.component.ts @@ -0,0 +1,23 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {UtmAlertType} from 'src/app/shared/types/alert/utm-alert.type'; + +@Component({ + selector: 'app-alert-child-column', + templateUrl: './alert-child-column.component.html', + styleUrls: ['./alert-child-column.component.scss'] +}) +export class AlertChildColumnComponent { + + @Input() alert: UtmAlertType; + @Input() loadingChildren = false; + + @Output() toggleExpand = new EventEmitter(); + + onToggleExpand(): void { + if (this.alert.hasChildren) { + this.alert.expanded = !this.alert.expanded; + this.toggleExpand.emit(this.alert); + } + } + +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-complete/alert-complete.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-complete/alert-complete.component.html index a0471b5ac..210047cd0 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-complete/alert-complete.component.html +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-complete/alert-complete.component.html @@ -8,7 +8,7 @@
Warning! - Enter observations or step you do to complete this alert(s) + Enter the observations or actions you performed to resolve this alert.
diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-complete/alert-complete.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-complete/alert-complete.component.ts index e82d086da..87881bbfb 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-complete/alert-complete.component.ts +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-complete/alert-complete.component.ts @@ -20,6 +20,7 @@ export class AlertCompleteComponent implements OnInit { @Input() status: number; @Input() alert: any; @Input() canCreateRule = true; + @Input() asFalsePositive = false; CLOSED = CLOSED; IGNORED = IGNORED; @Output() statusChange: EventEmitter = new EventEmitter(); @@ -43,7 +44,7 @@ export class AlertCompleteComponent implements OnInit { changeStatus() { this.creating = true; this.alertServiceManagement.updateAlertStatus(this.alertsIDs, - this.status, this.observations).subscribe(al => { + this.status, this.observations, this.asFalsePositive).subscribe(al => { if (this.canCreateRule && this.rule) { this.alertRulesService.create(this.rule).subscribe(value => { this.utmToastService.showSuccessBottom('Rule ' + this.rule.name + ' created successfully'); @@ -61,7 +62,11 @@ export class AlertCompleteComponent implements OnInit { } createRule() { - const modal = this.modalService.open(AlertRuleCreateComponent, {centered: true, size: 'lg'}); + const modal = this.modalService.open(AlertRuleCreateComponent, { + centered: true, + size: 'lg', + windowClass: 'alert-rule-modal' + }); const falsePositive: AlertTags[] = [FALSE_POSITIVE_OBJECT]; if (this.rule) { modal.componentInstance.rule = this.rule; diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.html new file mode 100644 index 000000000..be09b391e --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.html @@ -0,0 +1,30 @@ +
+ +
+
+ + +
+ + + + Page {{ page }} of {{ total < pageSize ? 1 : Math.ceil(total / pageSize) }} + + + +
+ +
diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.scss b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.scss new file mode 100644 index 000000000..489d37791 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.scss @@ -0,0 +1,49 @@ +.timeline-container { + width: 100%; + margin: 0 auto; + padding: 10px; + + .timeline-overview { + display: flex; + align-items: flex-end; + justify-content: center; + margin-bottom: 15px; + height: 60px; // fixed height for bars + + .overview-bar { + width: 5px; + margin: 0 1px; + background-color: #0277bd; + border-radius: 2px; + transition: height 0.3s ease; // smooth animation when data changes + } + } + + .timeline-chart { + width: 100%; + height: 500px; + margin-bottom: 15px; + border: 1px solid #ddd; + border-radius: 4px; + background: #fff; + } + + .timeline-pagination { + margin-top: 10px; + + button { + margin: 0 5px; + + i { + font-size: 12px; // IcoMoon icons + } + } + + span { + font-weight: bold; + font-size: 13px; + margin: 0 10px; + display: inline-block; + } + } +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.ts new file mode 100644 index 000000000..a3d73ca80 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component.ts @@ -0,0 +1,209 @@ +import {HttpResponse} from '@angular/common/http'; +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; +import { + ALERT_PARENT_ID, + ALERT_STATUS_FIELD_AUTO, + ALERT_TAGS_FIELD, ALERT_TIMESTAMP_FIELD, FALSE_POSITIVE_OBJECT +} from '../../../../../shared/constants/alert/alert-field.constant'; +import {AUTOMATIC_REVIEW} from '../../../../../shared/constants/alert/alert-status.constant'; +import {ElasticOperatorsEnum} from '../../../../../shared/enums/elastic-operators.enum'; +import {DataNatureTypeEnum} from '../../../../../shared/enums/nature-data.enum'; +import {ElasticDataService} from '../../../../../shared/services/elasticsearch/elastic-data.service'; +import {UtmAlertType} from '../../../../../shared/types/alert/utm-alert.type'; +import {ElasticFilterType} from '../../../../../shared/types/filter/elastic-filter.type'; +import {TimelineItem} from '../../../../../shared/types/utm-timeline-item'; +import {sanitizeFilters} from '../../../../../shared/util/elastic-filter.util'; +import {AlertEchoesTimelineService, TimelineGroup} from './alert-echoes-timeline.service'; + + +@Component({ + selector: 'app-alert-echoes-timeline', + templateUrl: './alert-echoes-timeline.component.html', + styleUrls: ['./alert-echoes-timeline.component.scss'] +}) +export class AlertEchoesTimelineComponent implements OnInit { + + @Input() alert: UtmAlertType; + @Input() page = 0; + @Input() pageSize = 100; + @Input() total = 0; + @Input() title = ''; + @Output() itemClick = new EventEmitter(); + + chartInstance: any; + + sortBy = ALERT_TIMESTAMP_FIELD + ',desc'; + alerts: UtmAlertType[] = []; + filters: ElasticFilterType[] = [ + {field: ALERT_STATUS_FIELD_AUTO, operator: ElasticOperatorsEnum.IS_NOT, value: AUTOMATIC_REVIEW}, + {field: ALERT_TAGS_FIELD, operator: ElasticOperatorsEnum.IS_NOT, value: FALSE_POSITIVE_OBJECT.tagName}, + ]; + loading = false; + chartOption: any = {}; + intervalMs = 60 * 1000; + groups: TimelineGroup[] = []; + readonly Math = Math; + + ngOnInit(): void { + this.filters.push({ + field: ALERT_PARENT_ID, + operator: ElasticOperatorsEnum.IS, + value: this.alert.id + }); + this.loadData(); + } + + constructor(private timelineService: AlertEchoesTimelineService, + private elasticDataService: ElasticDataService, + private utmToastService: UtmToastService, ) { + } + + onChartInit(ec: any) { + this.chartInstance = ec; + } + + refreshChart() { + if (this.chartInstance && this.chartOption) { + this.chartInstance.clear(); // limpia todo el canvas + this.chartInstance.setOption(this.chartOption, true); // redibuja + } + } + + buildChart() { + + const items = this.timelineService.buildTimelineFromAlerts(this.alerts); + this.groups = this.timelineService.generateTimelineGroups(this.alerts, this.intervalMs); + + const seriesData = []; + const cardHeight = 60; + const spacing = 10; + + + this.groups.forEach((group, index) => { + const timestamps = group.items.map(i => new Date(i.startDate).getTime()); + group.startTimestamp = Math.floor(timestamps.reduce((sum, t) => sum + t, 0) / timestamps.length); + + const rep = group.items[0] || ({} as any); // representative item + console.log('group', group, rep); + seriesData.push({ + value: [ + group.startTimestamp, // 0: timestamp (start of minute) + 0, // 1: y coordinate (not used) + rep.name || `Echoes`, // 2: representative name/title + new Date(group.startTimestamp).toISOString(), // 3: formatted minute + rep.iconUrl || 'assets/images/default-echo.png', // 4: icon url + group.items.length, // 5: count of echoes + index, + group.yOffset || 0, + rep.metadata + ], + groupData: group.items, // full list for drill-down + }); + }); + + + const allTimestamps = items.map(i => new Date(i.startDate).getTime()); + const minTimestamp = Math.min(...allTimestamps); + const maxTimestamp = Math.max(...allTimestamps); + const padding = (maxTimestamp - minTimestamp) * 0.1; + + const expand = (allTimestamps.length === 1) ? 30 * 60 * 1000 : (maxTimestamp - minTimestamp) * 0.1; + + this.chartOption = { + title: {text: this.title, left: 'center', textStyle: {fontSize: 16, fontWeight: 'bold'}}, + tooltip: { + trigger: 'item', + formatter: (params: any) => + `Echoes: ${params.data.value[2]}
Minute: ${params.data.value[3]}
Total: ${params.data.value[5]}` + }, + grid: { + left: 0, + right: 0, + top: 0, + bottom: 20, + containLabel: true + }, + xAxis: { + type: 'time', + min: minTimestamp - expand, + max: maxTimestamp + expand, + axisLabel: { + formatter: (val: number) => { + const d = new Date(val); + const year = d.getUTCFullYear(); + const month = (d.getUTCMonth() + 1).toString().padStart(2, '0'); + const day = d.getUTCDate().toString().padStart(2, '0'); + const hours = d.getUTCHours().toString().padStart(2, '0'); + const minutes = d.getUTCMinutes().toString().padStart(2, '0'); + const seconds = d.getUTCSeconds().toString().padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + }, + }, + splitLine: { + show: true, + lineStyle: { + type: 'dashed', + color: '#ccc', + width: 1 + } + } + }, + yAxis: { + type: 'value', + min: 0, + max: (cardHeight + spacing) * this.groups.length + 100, + show: false + }, + dataZoom: [ + {type: 'slider', xAxisIndex: 0, start: 0, end: 100}, + {type: 'inside', xAxisIndex: 0, zoomLock: false} + ], + series: [ + { + type: 'custom', + data: seriesData, + renderItem: (params: any, api: any) => this.timelineService.renderItem(params, api), + encode: {x: 0, y: 1} + } + ] + }; + } + + onChartClick(event: any) { + if (event.data && event.data.value) { + this.itemClick.emit(event.data.value[8] || {} as UtmAlertType); + } + } + + loadData() { + this.loading = true; + this.elasticDataService.search(this.page, this.pageSize, + 100000000, DataNatureTypeEnum.ALERT, + sanitizeFilters(this.filters), this.sortBy, true) + .subscribe( + (res: HttpResponse) => { + this.total = Number(res.headers.get('X-Total-Count')); + this.alerts = res.body; + this.loading = false; + this.buildChart(); + this.refreshChart(); + }, + (res: HttpResponse) => { + this.utmToastService.showError('Error', 'An error occurred while listing the alerts. Please try again later.'); + this.loading = false; + } + ); + } + + prevPage() { + this.page = this.page - 1; + this.loadData(); + } + + nextPage() { + this.page = this.page + 1; + this.loadData(); + } + +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.service.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.service.ts new file mode 100644 index 000000000..a35546e89 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.service.ts @@ -0,0 +1,200 @@ +import { Injectable } from '@angular/core'; +import {UtmAlertType} from '../../../../../shared/types/alert/utm-alert.type'; +import {TimelineItem} from '../../../../../shared/types/utm-timeline-item'; + +export interface TimelineGroup { + startTimestamp: number; + items: TimelineItem[]; + yOffset?: number; +} + +const cardWidth = 240; +const cardHeight = 62; +const baseOffset = 80; +const spacing = 10; + + +@Injectable() +export class AlertEchoesTimelineService { + + renderItem(params: any, api: any) { + const ts = api.value(0); + const coord = api.coord([ts, 0]) as number[]; + const chartWidth = params.coordSys.width; + api.getHeight(); + + // Horizontal position (centered, respecting canvas borders) + let x = coord[0] - cardWidth / 2; + x = Math.max(0, Math.min(x, chartWidth - cardWidth)); + + // Vertical stacking offset + const level = api.value(7) || 0; // stack level + const levelOffset = level * (cardHeight + spacing); + let yCard = coord[1] - baseOffset - levelOffset - cardHeight; + + // Ensure card stays within canvas + if (yCard < 0) { yCard = 0; } + + // Generic type for children to satisfy TS + const children: { + type: string; + shape?: Record; + style?: Record; + }[] = []; + + // Card background + children.push({ + type: 'rect', + shape: { x, y: yCard, width: cardWidth, height: cardHeight, r: 10 }, + style: { + fill: '#ffffff', + stroke: '#0277bd', + lineWidth: 1, + shadowBlur: 8, + shadowColor: 'rgba(0,0,0,0.2)', + cursor: 'pointer' + } + }); + + // Icon + children.push({ + type: 'image', + style: { + image: api.value(4), + x: x + 5, + y: yCard + 5, + width: cardHeight - 10, + height: cardHeight - 10 + } + }); + + // Title + children.push({ + type: 'text', + style: { + x: x + (cardHeight - 2.5) + 15, + y: yCard + 5, + text: this.truncateText(api.value(2) || '', 150), + textAlign: 'left', + fill: '#000', + fontSize: 14, + fontWeight: 600, + width: cardWidth - (cardHeight - 2.5) - 25, + overflow: 'break', + ellipsis: '...' + } + }); + + // Subtitle / date + children.push({ + type: 'text', + style: { + x: x + (cardHeight - 2.5) + 15, + y: yCard + 25, + text: api.value(3), + textAlign: 'left', + fill: '#666', + fontSize: 12 + } + }); + + // Total echoes + if (api.value(5) > 1) { + children.push({ + type: 'text', + style: { + x: x + (cardHeight - 2.5) + 15, + y: yCard + cardHeight - 18, + text: `Total: ${api.value(5)} echoes`, + textAlign: 'left', + fill: '#444', + fontSize: 12, + fontWeight: 'bold' + } + }); + } + + // Line connecting to timeline + children.push({ + type: 'line', + shape: { + x1: coord[0], + y1: yCard + cardHeight, + x2: coord[0], + y2: coord[1] + }, + style: { + stroke: '#0277bd', + lineWidth: 1.5 + } + }); + + return { type: 'group', children }; + } + + + private groupByInterval(items: TimelineItem[], intervalMs: number): TimelineGroup[] { + const sorted = [...items].sort( + (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime() + ); + const groups: TimelineGroup[] = []; + let currentGroup: TimelineGroup = null; + + sorted.forEach(item => { + const ts = new Date(item.startDate).getTime(); + if (!currentGroup || ts > currentGroup.startTimestamp + intervalMs) { + currentGroup = { startTimestamp: ts, items: [] }; + groups.push(currentGroup); + } + currentGroup.items.push(item); + + if (ts > currentGroup.startTimestamp) { + currentGroup.startTimestamp = ts; + } + }); + + return groups; + } + + truncateText(text: string, maxWidth: number) { + const avgCharWidth = 7; + const maxChars = Math.floor(maxWidth / avgCharWidth); + return text.length > maxChars ? text.substring(0, maxChars - 3) + '...' : text; + } + + buildTimelineFromAlerts(alerts: UtmAlertType[]): TimelineItem[] { + return alerts.map(cha => ({ + startDate: cha['@timestamp'], + name: cha.name, + metadata: cha, + iconUrl: 'assets/icons/echoes/echoes_default.png' + })); + } + + private assignYOffsetToGroups(groups: TimelineGroup[]): TimelineGroup[] { + const FIVE_MINUTE = 5 * 60 * 1000; + const sorted = [...groups].sort((a, b) => a.startTimestamp - b.startTimestamp); + + for (let i = 0; i < sorted.length; i++) { + const group = sorted[i]; + group.yOffset = 0; // default base + + for (let j = 0; j < i; j++) { + const prev = sorted[j]; + const dx = group.startTimestamp - prev.startTimestamp; + if (dx < FIVE_MINUTE) { + group.yOffset = Math.max(group.yOffset, (prev.yOffset || 0) + 1); + } + } + } + + return sorted; + } + + generateTimelineGroups(alerts: UtmAlertType[], intervalMs: number): TimelineGroup[] { + const items = this.buildTimelineFromAlerts(alerts); + const groups = this.groupByInterval(items, intervalMs); + return this.assignYOffsetToGroups(groups); + } + +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes/alert-echoes.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes/alert-echoes.component.html new file mode 100644 index 000000000..40d8f6292 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes/alert-echoes.component.html @@ -0,0 +1,71 @@ +
+ + + + + + + + + + + + + + + + + + + + + +
+ {{item.label}} +
+ +
+
+ + +
+
+
+
+
+ + + +
+
+ + + + + + + + + + + diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes/alert-echoes.component.scss b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes/alert-echoes.component.scss new file mode 100644 index 000000000..5a0a07cad --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes/alert-echoes.component.scss @@ -0,0 +1,7 @@ +:host { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + height: 100%; +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-echoes/alert-echoes.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes/alert-echoes.component.ts new file mode 100644 index 000000000..581be64eb --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-echoes/alert-echoes.component.ts @@ -0,0 +1,97 @@ +import {HttpResponse} from '@angular/common/http'; +import {Component, Input, OnInit} from '@angular/core'; +import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; +import { + ALERT_ADVERSARY_FIELD, ALERT_ECHOES_FIELDS, ALERT_PARENT_ID, + ALERT_STATUS_FIELD_AUTO, ALERT_TAGS_FIELD, + ALERT_TARGET_FIELD, ALERT_TIMESTAMP_FIELD, FALSE_POSITIVE_OBJECT +} from '../../../../../shared/constants/alert/alert-field.constant'; +import {AUTOMATIC_REVIEW} from '../../../../../shared/constants/alert/alert-status.constant'; +import {ITEMS_PER_PAGE} from '../../../../../shared/constants/pagination.constants'; +import {SortDirection} from '../../../../../shared/directives/sortable/type/sort-direction.type'; +import {SortEvent} from '../../../../../shared/directives/sortable/type/sort-event'; +import {ElasticOperatorsEnum} from '../../../../../shared/enums/elastic-operators.enum'; +import {DataNatureTypeEnum} from '../../../../../shared/enums/nature-data.enum'; +import {ElasticDataService} from '../../../../../shared/services/elasticsearch/elastic-data.service'; +import {UtmAlertType} from '../../../../../shared/types/alert/utm-alert.type'; +import {ElasticFilterType} from '../../../../../shared/types/filter/elastic-filter.type'; +import {sanitizeFilters} from '../../../../../shared/util/elastic-filter.util'; +import {EventDataTypeEnum} from '../../enums/event-data-type.enum'; + +@Component({ + selector: 'app-alert-echoes', + templateUrl: './alert-echoes.component.html', + styleUrls: ['./alert-echoes.component.scss'] +}) +export class AlertEchoesComponent implements OnInit { + + @Input() alert: UtmAlertType = {} as UtmAlertType; + + page = 1; + totalItems = 0; + readonly fields = ALERT_ECHOES_FIELDS; + readonly ALERT_ADVERSARY_FIELD = ALERT_ADVERSARY_FIELD; + readonly ALERT_TARGET_FIELD = ALERT_TARGET_FIELD; + itemsPerPage = ITEMS_PER_PAGE; + dataType = EventDataTypeEnum.ALERT; + dataNature = DataNatureTypeEnum.ALERT; + loading = false; + sortEvent: SortEvent; + alerts: UtmAlertType[] = []; + filters: ElasticFilterType[] = [ + {field: ALERT_STATUS_FIELD_AUTO, operator: ElasticOperatorsEnum.IS_NOT, value: AUTOMATIC_REVIEW}, + {field: ALERT_TAGS_FIELD, operator: ElasticOperatorsEnum.IS_NOT, value: FALSE_POSITIVE_OBJECT.tagName}, + ]; + direction: SortDirection = ''; + sortBy = ALERT_TIMESTAMP_FIELD + ',desc'; + + constructor(private elasticDataService: ElasticDataService, + private utmToastService: UtmToastService) { } + + ngOnInit() { + + this.filters.push({ + field: ALERT_PARENT_ID, + operator: ElasticOperatorsEnum.IS, + value: this.alert.id + }); + this.loadChildrenAlerts(); + } + + loadChildrenAlerts() { + this.loading = true; + this.elasticDataService.search(this.page, this.itemsPerPage, + 100000000, this.dataNature, + sanitizeFilters(this.filters), this.sortBy, true).subscribe( + (res: HttpResponse) => { + this.totalItems = Number(res.headers.get('X-Total-Count')); + this.alerts = res.body; + this.loading = false; + }, + (res: HttpResponse) => { + this.utmToastService.showError('Error', 'An error occurred while listing the alerts. Please try again later.'); + this.loading = false; + } + ); + } + + onSortBy($event: SortEvent) { + this.sortBy = $event.column + ',' + $event.direction; + this.loadChildrenAlerts(); + } + + loadPage($event: number) { + this.page = $event; + this.loadChildrenAlerts(); + } + + onRefreshData($event: boolean) { + + } + + onItemsPerPageChange($event: number) { + this.page = 1; + this.itemsPerPage = $event; + this.loadChildrenAlerts(); + } +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-entity-display/alert-entity-display.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-entity-display/alert-entity-display.component.html index c5e6a384b..eb3b1286a 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-entity-display/alert-entity-display.component.html +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-entity-display/alert-entity-display.component.html @@ -5,6 +5,12 @@
+
+ +
+
+ +
diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-entity-display/alert-entity-display.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-entity-display/alert-entity-display.component.ts index 3eeea4588..f4a52ee6d 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-entity-display/alert-entity-display.component.ts +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-entity-display/alert-entity-display.component.ts @@ -2,16 +2,16 @@ import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core' import { ALERT_ADVERSARY_BYTES_SENT_FIELD, ALERT_ADVERSARY_DOMAIN_FIELD, ALERT_ADVERSARY_GEOLOCATION_ASN_FIELD, ALERT_ADVERSARY_GEOLOCATION_ASO_FIELD, - ALERT_ADVERSARY_GEOLOCATION_LATITUDE_FIELD, ALERT_ADVERSARY_GEOLOCATION_LONGITUDE_FIELD, + ALERT_ADVERSARY_GEOLOCATION_LATITUDE_FIELD, ALERT_ADVERSARY_GEOLOCATION_LONGITUDE_FIELD, ALERT_ADVERSARY_HOST_FIELD, ALERT_ADVERSARY_IP_FIELD, - ALERT_ADVERSARY_URL_FIELD, + ALERT_ADVERSARY_URL_FIELD, ALERT_ADVERSARY_USER_FIELD, ALERT_TARGET_BYTES_SENT_FIELD, ALERT_TARGET_DOMAIN_FIELD, ALERT_TARGET_FIELD, ALERT_TARGET_GEOLOCATION_ASN_FIELD, ALERT_TARGET_GEOLOCATION_ASO_FIELD, ALERT_TARGET_GEOLOCATION_LATITUDE_FIELD, - ALERT_TARGET_GEOLOCATION_LONGITUDE_FIELD, + ALERT_TARGET_GEOLOCATION_LONGITUDE_FIELD, ALERT_TARGET_HOST_FIELD, ALERT_TARGET_IP_FIELD, - ALERT_TARGET_URL_FIELD + ALERT_TARGET_URL_FIELD, ALERT_TARGET_USER_FIELD } from '../../../../../shared/constants/alert/alert-field.constant'; import {UtmAlertType} from '../../../../../shared/types/alert/utm-alert.type'; import {UtmFieldType} from '../../../../../shared/types/table/utm-field.type'; @@ -32,6 +32,10 @@ export class AlertEntityDisplayComponent implements OnInit, OnChanges { ALERT_TARGET_URL_FIELD = ALERT_TARGET_URL_FIELD; ALERT_TARGET_DOMAIN_FIELD = ALERT_TARGET_DOMAIN_FIELD; ALERT_ADVERSARY_DOMAIN_FIELD = ALERT_ADVERSARY_DOMAIN_FIELD; + ALERT_ADVERSARY_HOST_FIELD = ALERT_ADVERSARY_HOST_FIELD; + ALERT_ADVERSARY_USER_FIELD = ALERT_ADVERSARY_USER_FIELD; + ALERT_TARGET_USER_FIELD = ALERT_TARGET_USER_FIELD; + ALERT_TARGET_HOST_FIELD = ALERT_TARGET_HOST_FIELD; ALERT_TARGET_GEOLOCATION_LATITUDE_FIELD = ALERT_TARGET_GEOLOCATION_LATITUDE_FIELD; ALERT_ADVERSARY_GEOLOCATION_LATITUDE_FIELD = ALERT_ADVERSARY_GEOLOCATION_LATITUDE_FIELD; ALERT_TARGET_GEOLOCATION_LONGITUDE_FIELD = ALERT_TARGET_GEOLOCATION_LONGITUDE_FIELD; diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-events-related/alert-events-related.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-events-related/alert-events-related.component.html new file mode 100644 index 000000000..f57a6dc10 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-events-related/alert-events-related.component.html @@ -0,0 +1,15 @@ + + diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-events-related/alert-events-related.component.scss b/frontend/src/app/data-management/alert-management/shared/components/alert-events-related/alert-events-related.component.scss new file mode 100644 index 000000000..5a0a07cad --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-events-related/alert-events-related.component.scss @@ -0,0 +1,7 @@ +:host { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + height: 100%; +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-events-related/alert-events-related.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-events-related/alert-events-related.component.ts new file mode 100644 index 000000000..cdf3a6b61 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-events-related/alert-events-related.component.ts @@ -0,0 +1,71 @@ +import {Component, Input, OnInit} from '@angular/core'; +import { + UtmTableDetailComponent +} from '../../../../../shared/components/utm/table/utm-table/utm-table-detail/utm-table-detail.component'; +import {LOG_ANALYZER_TOTAL_ITEMS} from '../../../../../shared/constants/log-analyzer.constant'; +import {ITEMS_PER_PAGE} from '../../../../../shared/constants/pagination.constants'; +import {ElasticDataTypesEnum} from '../../../../../shared/enums/elastic-data-types.enum'; +import {UtmFieldType} from '../../../../../shared/types/table/utm-field.type'; + +@Component({ + selector: 'app-alert-events-related', + templateUrl: './alert-events-related.component.html', + styleUrls: ['./alert-events-related.component.scss'] +}) +export class AlertEventsRelatedComponent implements OnInit { + + fields: UtmFieldType[] = [ + {field: 'timestamp', label: '@timestamp', visible: true, type: ElasticDataTypesEnum.DATE}, + ]; + @Input() events: any[] = []; + displayedLogs: any[] = []; + page = 1; + readonly totalItems = LOG_ANALYZER_TOTAL_ITEMS; + itemsPerPage = ITEMS_PER_PAGE; + readonly componentDetail = UtmTableDetailComponent; + sortField = ''; + sortDirection: 'asc' | 'desc' = 'asc'; + + constructor() { } + + ngOnInit() { + this.applyFilters(); + } + + + onPageChange($event: number) { + this.page = $event; + this.applyFilters(); + } + + onRemoveColumn($event: UtmFieldType) { + + } + + onSizeChange($event: number) { + this.itemsPerPage = $event; + this.applyFilters(); + } + + onSortBy($event: string) { + + } + + applyFilters() { + const size = this.itemsPerPage; + const filtered = [...this.events]; + + // Sorting + if (this.sortField) { + filtered.sort((a, b) => { + if (a[this.sortField] < b[this.sortField]) { return this.sortDirection === 'asc' ? -1 : 1; } + if (a[this.sortField] > b[this.sortField]) { return this.sortDirection === 'asc' ? 1 : -1; } + return 0; + }); + } + + // Pagination + const start = (this.page - 1) * size; + this.displayedLogs = filtered.slice(start, start + size); + } +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-host-detail/alert-host-detail.component.scss b/frontend/src/app/data-management/alert-management/shared/components/alert-host-detail/alert-host-detail.component.scss index 1b3af13b3..dbb2c4d8a 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-host-detail/alert-host-detail.component.scss +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-host-detail/alert-host-detail.component.scss @@ -1,12 +1,11 @@ :host { display: flex; - flex: 1 1 auto; } .container-alert-detail { display: flex; flex-direction: column; - flex: 1 1 auto; + width: 100%; } .alert-details { diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-logs-related-button/alert-logs-related-button.component.css b/frontend/src/app/data-management/alert-management/shared/components/alert-logs-related-button/alert-logs-related-button.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-logs-related-button/alert-logs-related-button.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-logs-related-button/alert-logs-related-button.component.html new file mode 100644 index 000000000..417d284f6 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-logs-related-button/alert-logs-related-button.component.html @@ -0,0 +1,15 @@ + + + + + + + (View in Log Explorer) + + + + + diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-logs-related-button/alert-logs-related-button.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-logs-related-button/alert-logs-related-button.component.ts new file mode 100644 index 000000000..2ca6b1482 --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-logs-related-button/alert-logs-related-button.component.ts @@ -0,0 +1,59 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {Router} from '@angular/router'; +import {NgxSpinnerService} from 'ngx-spinner'; +import {of} from "rxjs"; +import {catchError, tap} from 'rxjs/operators'; +import {ALERT_TIMESTAMP_FIELD} from '../../../../../shared/constants/alert/alert-field.constant'; +import {LOG_ROUTE} from '../../../../../shared/constants/app-routes.constant'; +import {LOG_INDEX_PATTERN, LOG_INDEX_PATTERN_ID} from '../../../../../shared/constants/main-index-pattern.constant'; +import {ElasticOperatorsEnum} from '../../../../../shared/enums/elastic-operators.enum'; +import {ElasticDataService} from '../../../../../shared/services/elasticsearch/elastic-data.service'; + +const LOG_ID_FIELD = 'id'; + +@Component({ + selector: 'app-alert-logs-related-action', + templateUrl: './alert-logs-related-button.component.html', + styleUrls: ['./alert-logs-related-button.component.css'] +}) +export class AlertLogsRelatedButtonComponent implements OnInit { + + @Input() logs: any[] = []; + @Input() template: 'btn' | 'span' = 'btn'; + showButton = false; + + constructor(private router: Router, + private spinner: NgxSpinnerService, + private elasticDataService: ElasticDataService) { } + + ngOnInit() { + const ids = this.logs.map(log => log.id); + const filters = [ + {field: LOG_ID_FIELD, operator: ElasticOperatorsEnum.CONTAIN_ONE_OF, value: ids}, + {field: ALERT_TIMESTAMP_FIELD, operator: ElasticOperatorsEnum.IS_BETWEEN, value: ['now-1y', 'now']} + ]; + + + this.elasticDataService.exists(LOG_INDEX_PATTERN, filters).pipe( + tap((exists: boolean) => this.showButton = exists), + catchError(err => { + console.error('Error checking related logs:', err); + this.showButton = false; + return of(false); + }) + ).subscribe(); + + } + + navigateToEvents() { + const queryParams = {patternId: LOG_INDEX_PATTERN_ID, indexPattern: LOG_INDEX_PATTERN}; + queryParams[LOG_ID_FIELD] = ElasticOperatorsEnum.IS_ONE_OF + '->' + this.logs.map(log => log.id).slice(0, 100); + queryParams[ALERT_TIMESTAMP_FIELD] = ElasticOperatorsEnum.IS_BETWEEN + '->' + 'now-1y' + ',' + 'now'; + this.spinner.show('loadingSpinner'); + this.router.navigate([LOG_ROUTE], { + queryParams + }).then(() => { + this.spinner.hide('loadingSpinner'); + }); + } +} diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-rule-create/alert-rule-create.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-rule-create/alert-rule-create.component.ts index 9e2dd34ea..94c79dce9 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-rule-create/alert-rule-create.component.ts +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-rule-create/alert-rule-create.component.ts @@ -53,6 +53,7 @@ import {AlertManagementService} from '../../services/alert-management.service'; import {AlertRulesService} from '../../services/alert-rules.service'; import {AlertTagService} from '../../services/alert-tag.service'; import {setAlertPropertyValue} from '../../util/alert-util-function'; +import {AlertActionRefreshService} from "../../services/alert-action-refresh.service"; @Component({ selector: 'app-alert-rule-create', @@ -62,6 +63,7 @@ import {setAlertPropertyValue} from '../../util/alert-util-function'; export class AlertRuleCreateComponent implements OnInit, OnDestroy { @Input() alert: UtmAlertType; @Input() isForComplete = false; + @Input() isFalsePositiveRule = false; @Input() action: 'create' | 'update' | 'select' = 'create'; @Input() rule: AlertRuleType; @Output() ruleAdd = new EventEmitter(); @@ -101,7 +103,7 @@ export class AlertRuleCreateComponent implements OnInit, OnDestroy { formRule: FormGroup; exist: boolean; typing = false; - viewFieldDetail = false; + viewFieldDetail = true; uuid = UUID.UUID(); tagging = false; ElasticOperatorsEnum = ElasticOperatorsEnum; @@ -133,7 +135,8 @@ export class AlertRuleCreateComponent implements OnInit, OnDestroy { private alertTagService: AlertTagService, private operatorService: OperatorService, private elasticDataService: ElasticDataService, - private alertService: AlertService) { + private alertService: AlertService, + private alertActionRefreshService: AlertActionRefreshService) { this.fields = ALERT_FIELDS.filter(value => !this.excludeFields.includes(value.field)); this.fields = this.fields.reduce((acc: any[], field) => { @@ -157,10 +160,14 @@ export class AlertRuleCreateComponent implements OnInit, OnDestroy { }); if (this.rule) { - this.filters = [... this.rule.conditions]; + this.filters = [...this.rule.conditions]; this.selected = this.rule.tags.length > 0 ? [...this.rule.tags] : []; } + if (this.isFalsePositiveRule) { + this.selectValue(FALSE_POSITIVE_OBJECT); + } + this.alerts$ = this.alertService.onRefresh$ .pipe( takeUntil(this.destroy$), @@ -241,6 +248,7 @@ export class AlertRuleCreateComponent implements OnInit, OnDestroy { request$.subscribe(() => { const action = this.action === 'update' ? 'updated' : 'created'; this.utmToastService.showSuccessBottom(`Rule ${this.formRule.get('name').value} ${action} successfully`); + this.alertActionRefreshService.alertTagRuleCreated(true); if (this.alert) { const alertId = this.alert.id; @@ -359,7 +367,7 @@ export class AlertRuleCreateComponent implements OnInit, OnDestroy { } isFalsePositive() { - return this.selected.findIndex(value => value.tagName.includes('False positive')) !== -1; + return this.isFalsePositiveRule || this.selected.findIndex(value => value.tagName.includes('False positive')) !== -1; } getOperators(conditionField: string) { diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.html b/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.html index fbad3b4a1..201fcd787 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.html +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.html @@ -1,4 +1,4 @@ -
+
+ +
@@ -65,10 +71,7 @@
ID:  - +
Status:  @@ -155,8 +158,8 @@
- - + +
@@ -164,9 +167,9 @@
-
-
-
+
+
+ +
+
+ +
+
diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.scss b/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.scss index c0942bce4..645f1f19f 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.scss +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.scss @@ -1,3 +1,11 @@ +:host { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + height: 100%; +} + .has-minimum-width { min-width: 90px; } @@ -35,4 +43,10 @@ a { gap: 0.5rem } +.badge{ + position: absolute; + right: 0%; + +} + diff --git a/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.ts b/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.ts index e1c4cd92f..59fd58602 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.ts +++ b/frontend/src/app/data-management/alert-management/shared/components/alert-view-detail/alert-view-detail.component.ts @@ -1,10 +1,11 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; -import {Router} from '@angular/router'; -import {NgxSpinnerService} from 'ngx-spinner'; import {UtmModulesEnum} from '../../../../../app-module/shared/enum/utm-module.enum'; import {UtmModulesService} from '../../../../../app-module/shared/services/utm-modules.service'; -import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; import { + UtmTableDetailComponent +} from '../../../../../shared/components/utm/table/utm-table/utm-table-detail/utm-table-detail.component'; +import { + ALERT_ADVERSARY_FIELD, ALERT_CASE_ID_FIELD, ALERT_CATEGORY_FIELD, ALERT_FIELDS, @@ -16,11 +17,7 @@ import { ALERT_SEVERITY_FIELD_LABEL, ALERT_STATUS_FIELD, ALERT_TACTIC_FIELD, ALERT_TECHNIQUE_FIELD, ALERT_TIMESTAMP_FIELD } from '../../../../../shared/constants/alert/alert-field.constant'; -import {LOG_ROUTE} from '../../../../../shared/constants/app-routes.constant'; -import {LOG_INDEX_PATTERN, LOG_INDEX_PATTERN_ID} from '../../../../../shared/constants/main-index-pattern.constant'; -import {ElasticOperatorsEnum} from '../../../../../shared/enums/elastic-operators.enum'; import {IncidentOriginTypeEnum} from '../../../../../shared/enums/incident-response/incident-origin-type.enum'; -import {ElasticDataService} from '../../../../../shared/services/elasticsearch/elastic-data.service'; import {AlertTags} from '../../../../../shared/types/alert/alert-tag.type'; import {AlertStatusEnum, UtmAlertType} from '../../../../../shared/types/alert/utm-alert.type'; import {ElasticFilterType} from '../../../../../shared/types/filter/elastic-filter.type'; @@ -78,11 +75,7 @@ export class AlertViewDetailComponent implements OnInit { constructor(private alertUpdateHistoryBehavior: AlertUpdateHistoryBehavior, private alertUpdateTagBehavior: AlertUpdateTagBehavior, - private spinner: NgxSpinnerService, - private elasticDataService: ElasticDataService, private moduleService: UtmModulesService, - private router: Router, - private toastService: UtmToastService, private alertFieldService: AlertFieldService) { } @@ -124,19 +117,6 @@ export class AlertViewDetailComponent implements OnInit { this.refreshData.emit(true); } - navigateToEvents() { - const queryParams = {patternId: LOG_INDEX_PATTERN_ID, indexPattern: LOG_INDEX_PATTERN}; - const LOG_ID_FIELD = 'id'; - queryParams[LOG_ID_FIELD] = ElasticOperatorsEnum.IS_ONE_OF + '->' + this.logs.map(log => log.id).slice(0, 100); - queryParams[this.TIMESTAMP_FIELD] = ElasticOperatorsEnum.IS_BETWEEN + '->' + 'now-1y' + ',' + 'now'; - this.spinner.show('loadingSpinner'); - this.router.navigate([LOG_ROUTE], { - queryParams - }).then(() => { - this.spinner.hide('loadingSpinner'); - }); - } - showMap(): boolean { return (this.alert && this.alert.target && this.alert.target.geolocation && 'latitude' in this.alert.target.geolocation && 'longitude' in this.alert.target.geolocation) && (this.alert && this.alert.adversary && this.alert.adversary.geolocation && 'latitude' in this.alert.adversary.geolocation && 'longitude' in this.alert.adversary.geolocation); @@ -179,6 +159,8 @@ export class AlertViewDetailComponent implements OnInit { isEmptyResponse() { return Object.entries(this.log).length === 0; } + + protected readonly ALERT_ADVERSARY_FIELD = ALERT_ADVERSARY_FIELD; } export enum AlertDetailTabEnum { @@ -189,4 +171,5 @@ export enum AlertDetailTabEnum { MAP = 'map', TAGS = 'tags', INCIDENT = 'incident', + ECHOES = 'echoes', } diff --git a/frontend/src/app/data-management/alert-management/shared/components/data-field-render/data-field-render.component.html b/frontend/src/app/data-management/alert-management/shared/components/data-field-render/data-field-render.component.html index e79c604c1..a03282a29 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/data-field-render/data-field-render.component.html +++ b/frontend/src/app/data-management/alert-management/shared/components/data-field-render/data-field-render.component.html @@ -1,11 +1,24 @@
- + - + + + + + + + diff --git a/frontend/src/app/data-management/alert-management/shared/components/data-field-render/data-field-render.component.ts b/frontend/src/app/data-management/alert-management/shared/components/data-field-render/data-field-render.component.ts index 5bbadad9d..cf5819b8b 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/data-field-render/data-field-render.component.ts +++ b/frontend/src/app/data-management/alert-management/shared/components/data-field-render/data-field-render.component.ts @@ -1,13 +1,15 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; import { ALERT_ADVERSARY_FIELD, ALERT_ADVERSARY_IP_FIELD, - ALERT_DESTINATION_IP_FIELD, ALERT_IMPACT_FIELD, + ALERT_DESTINATION_IP_FIELD, ALERT_ECHOES_FIELD, + ALERT_IMPACT_FIELD, ALERT_NOTE_FIELD, ALERT_SEVERITY_FIELD_LABEL, ALERT_SOURCE_IP_FIELD, ALERT_STATUS_FIELD, ALERT_TAGS_FIELD, - ALERT_TARGET_FIELD, ALERT_TARGET_IP_FIELD, + ALERT_TARGET_FIELD, + ALERT_TARGET_IP_FIELD } from '../../../../../shared/constants/alert/alert-field.constant'; import {UtmDateFormatEnum} from '../../../../../shared/enums/utm-date-format.enum'; import {UtmAlertType} from '../../../../../shared/types/alert/utm-alert.type'; @@ -25,6 +27,8 @@ export class DataFieldRenderComponent implements OnInit { @Input() field: UtmFieldType; @Input() showStatusChange: boolean; @Input() dataType: EventDataTypeEnum; + @Input() tags: any[]; + @Input() isEcho = false; @Output() refreshData = new EventEmitter(); STATUS_FIELD = ALERT_STATUS_FIELD; SEVERITY_LABEL_FIELD = ALERT_SEVERITY_FIELD_LABEL; @@ -37,6 +41,7 @@ export class DataFieldRenderComponent implements OnInit { utmFormatDate = UtmDateFormatEnum.UTM_SHORT_UTC; ALERT_TARGET_IP_FIELD = ALERT_TARGET_IP_FIELD; ALERT_ADVERSARY_IP_FIELD = ALERT_ADVERSARY_IP_FIELD; + ALERT_ECHOES_FIELD = ALERT_ECHOES_FIELD; constructor() { } diff --git a/frontend/src/app/data-management/alert-management/shared/components/filters/status-filter/status-filter.component.ts b/frontend/src/app/data-management/alert-management/shared/components/filters/status-filter/status-filter.component.ts index 2c99266f4..59da9addd 100644 --- a/frontend/src/app/data-management/alert-management/shared/components/filters/status-filter/status-filter.component.ts +++ b/frontend/src/app/data-management/alert-management/shared/components/filters/status-filter/status-filter.component.ts @@ -85,8 +85,6 @@ export class StatusFilterComponent implements OnInit, OnDestroy { const staIndex = this.statusValueArray.findIndex(arr => arr.key === statusNum); this.statusValueArray[staIndex].value = Number(value.body[keys]); } - - console.log('Response:', value.body); }); }); }); diff --git a/frontend/src/app/data-management/alert-management/shared/services/alert-action-refresh.service.ts b/frontend/src/app/data-management/alert-management/shared/services/alert-action-refresh.service.ts new file mode 100644 index 000000000..c0f99b87c --- /dev/null +++ b/frontend/src/app/data-management/alert-management/shared/services/alert-action-refresh.service.ts @@ -0,0 +1,33 @@ +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from 'rxjs'; +import {UtmIncidentType} from '../../../../shared/types/incident/utm-incident.type'; + + +@Injectable({ + providedIn: 'root' +}) +export class AlertActionRefreshService { + + private incidentCreatedBehavior = new BehaviorSubject(null); + incidentCreated$ = this.incidentCreatedBehavior.asObservable(); + + private alertTagRuleCreatedBehavior = new BehaviorSubject(false); + alertTagRuleCreated$ = this.alertTagRuleCreatedBehavior.asObservable(); + + constructor() { + } + + incidentCreated(incident: UtmIncidentType) { + this.incidentCreatedBehavior.next(incident); + } + + alertTagRuleCreated(refresh: boolean) { + this.alertTagRuleCreatedBehavior.next(refresh); + } + + clearValues() { + this.incidentCreatedBehavior.next(null); + this.alertTagRuleCreatedBehavior.next(false); + } + +} diff --git a/frontend/src/app/data-management/alert-management/shared/services/alert-action.service.ts b/frontend/src/app/data-management/alert-management/shared/services/alert-action.service.ts deleted file mode 100644 index 4e397c1a9..000000000 --- a/frontend/src/app/data-management/alert-management/shared/services/alert-action.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {HttpClient, HttpResponse} from '@angular/common/http'; -import {Injectable} from '@angular/core'; -import {Observable} from 'rxjs'; -import {SERVER_API_URL} from '../../../../app.constants'; - - -@Injectable({ - providedIn: 'root' -}) -export class AlertActionService { - - public resourceUrl = SERVER_API_URL + 'api/utm-alerts'; - - constructor(private http: HttpClient) { - } - - updateAlertStatus(alertId: string[], status: number, statusObservation: string = ''): Observable> { - const req = { - alertIds: alertId, - status, - statusObservation - }; - return this.http.post>(this.resourceUrl + '/status', req, {observe: 'response'}); - } - -} diff --git a/frontend/src/app/data-management/alert-management/shared/services/alert-management.service.ts b/frontend/src/app/data-management/alert-management/shared/services/alert-management.service.ts index a761290b8..da619f02b 100644 --- a/frontend/src/app/data-management/alert-management/shared/services/alert-management.service.ts +++ b/frontend/src/app/data-management/alert-management/shared/services/alert-management.service.ts @@ -1,6 +1,7 @@ import {HttpClient, HttpResponse} from '@angular/common/http'; import {Injectable} from '@angular/core'; import {Observable} from 'rxjs'; +import {AlertStatusEnum} from 'src/app/shared/types/alert/utm-alert.type'; import {SERVER_API_URL} from '../../../../app.constants'; import {QueryType} from '../../../../shared/types/query-type'; @@ -15,12 +16,18 @@ export class AlertManagementService { constructor(private http: HttpClient) { } - updateAlertStatus(alertId: string[], status: number, statusObservation: string = ''): Observable> { - const req = { + updateAlertStatus(alertId: string[], status: number, statusObservation: string = '', asFalsePositive = false) + : Observable> { + + const req: any = { alertIds: alertId, status, statusObservation }; + + if (status === AlertStatusEnum.COMPLETED) { + req.addFalsePositiveTag = asFalsePositive; + } return this.http.post>(this.resourceUrl + '/status', req, {observe: 'response'}); } diff --git a/frontend/src/app/data-management/data-management.module.ts b/frontend/src/app/data-management/data-management.module.ts index fab2a8bdb..90fd659df 100644 --- a/frontend/src/app/data-management/data-management.module.ts +++ b/frontend/src/app/data-management/data-management.module.ts @@ -2,12 +2,14 @@ import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; import {NewAlertBehavior} from '../shared/behaviors/new-alert.behavior'; import {DataManagementRouting} from './data-management-routing.module'; +import {UtmSharedModule} from 'src/app/shared/utm-shared.module' @NgModule({ declarations: [], imports: [ CommonModule, - DataManagementRouting + DataManagementRouting, + UtmSharedModule ], providers: [NewAlertBehavior] }) diff --git a/frontend/src/app/graphic-builder/shared/components/viewer/chart-view/chart-view.component.html b/frontend/src/app/graphic-builder/shared/components/viewer/chart-view/chart-view.component.html index e3f763484..3c1bc8958 100644 --- a/frontend/src/app/graphic-builder/shared/components/viewer/chart-view/chart-view.component.html +++ b/frontend/src/app/graphic-builder/shared/components/viewer/chart-view/chart-view.component.html @@ -11,17 +11,9 @@ [invertContent]="true">
-
-
- - -
@@ -35,3 +27,13 @@ echarts>
+ + +
+ + +
+
diff --git a/frontend/src/app/graphic-builder/shared/components/viewer/chart-view/chart-view.component.ts b/frontend/src/app/graphic-builder/shared/components/viewer/chart-view/chart-view.component.ts index 2feada3cf..34da00e67 100644 --- a/frontend/src/app/graphic-builder/shared/components/viewer/chart-view/chart-view.component.ts +++ b/frontend/src/app/graphic-builder/shared/components/viewer/chart-view/chart-view.component.ts @@ -73,7 +73,6 @@ export class ChartViewComponent implements OnInit, OnDestroy { } ngOnInit() { - this.defaultTime = resolveDefaultVisualizationTime(this.visualization); this.refreshType = `${this.chartId}`; this.data$ = this.refreshService.refresh$ @@ -131,6 +130,7 @@ export class ChartViewComponent implements OnInit, OnDestroy { }); if (!this.defaultTime) { + this.defaultTime = resolveDefaultVisualizationTime(this.visualization); this.refreshService.sendRefresh(this.refreshType); } } diff --git a/frontend/src/app/graphic-builder/shared/components/viewer/goal-view/goal-view.component.html b/frontend/src/app/graphic-builder/shared/components/viewer/goal-view/goal-view.component.html index 5f39e30d5..fb3db4c9a 100644 --- a/frontend/src/app/graphic-builder/shared/components/viewer/goal-view/goal-view.component.html +++ b/frontend/src/app/graphic-builder/shared/components/viewer/goal-view/goal-view.component.html @@ -11,16 +11,8 @@ [invertContent]="true">
-
-
- - -
@@ -44,3 +36,13 @@
+ + +
+ + +
+
diff --git a/frontend/src/app/graphic-builder/shared/components/viewer/goal-view/goal-view.component.ts b/frontend/src/app/graphic-builder/shared/components/viewer/goal-view/goal-view.component.ts index 39b959093..af6d39e6d 100644 --- a/frontend/src/app/graphic-builder/shared/components/viewer/goal-view/goal-view.component.ts +++ b/frontend/src/app/graphic-builder/shared/components/viewer/goal-view/goal-view.component.ts @@ -93,8 +93,8 @@ export class GoalViewComponent implements OnInit, OnDestroy { } }); - this.defaultTime = resolveDefaultVisualizationTime(this.visualization); if (!this.defaultTime) { + this.defaultTime = resolveDefaultVisualizationTime(this.visualization); this.refreshService.sendRefresh(this.refreshType); } } diff --git a/frontend/src/app/graphic-builder/shared/components/viewer/map-view/map-view.component.html b/frontend/src/app/graphic-builder/shared/components/viewer/map-view/map-view.component.html index 4695bcbf7..acee49cad 100644 --- a/frontend/src/app/graphic-builder/shared/components/viewer/map-view/map-view.component.html +++ b/frontend/src/app/graphic-builder/shared/components/viewer/map-view/map-view.component.html @@ -13,15 +13,7 @@
-
- - -
- +
@@ -33,3 +25,13 @@
+ + +
+ + +
+
diff --git a/frontend/src/app/graphic-builder/shared/components/viewer/map-view/map-view.component.ts b/frontend/src/app/graphic-builder/shared/components/viewer/map-view/map-view.component.ts index bd9b6ab40..bbf2c48c0 100644 --- a/frontend/src/app/graphic-builder/shared/components/viewer/map-view/map-view.component.ts +++ b/frontend/src/app/graphic-builder/shared/components/viewer/map-view/map-view.component.ts @@ -2,14 +2,17 @@ import {AfterViewInit, Component, EventEmitter, Input, OnDestroy, OnInit, Output import {UUID} from 'angular2-uuid'; import * as L from 'leaflet'; import {Observable, Observer, of, Subject} from 'rxjs'; +import {catchError, filter, map, switchMap, takeUntil, tap} from 'rxjs/operators'; import {UtmToastService} from '../../../../../shared/alert/utm-toast.service'; import {DashboardBehavior} from '../../../../../shared/behaviors/dashboard.behavior'; +import {TimeFilterBehavior} from '../../../../../shared/behaviors/time-filter.behavior'; import {EchartClickAction} from '../../../../../shared/chart/types/action/echart-click-action'; import {UtmScatterMapOptionType} from '../../../../../shared/chart/types/charts/scatter/utm-scatter-map-option.type'; import {LeafletMapType} from '../../../../../shared/chart/types/map/leaflet/leaflet-map.type'; import {VisualizationType} from '../../../../../shared/chart/types/visualization.type'; import {ElasticFilterDefaultTime} from '../../../../../shared/components/utm/filters/elastic-filter-time/elastic-filter-time.component'; import {ChartTypeEnum} from '../../../../../shared/enums/chart-type.enum'; +import {RefreshService, RefreshType} from '../../../../../shared/services/util/refresh.service'; import {TimeFilterType} from '../../../../../shared/types/time-filter.type'; import {mergeParams, sanitizeFilters} from '../../../../../shared/util/elastic-filter.util'; import {getBucketLabel} from '../../../../chart-builder/chart-property-builder/shared/functions/visualization-util'; @@ -18,9 +21,6 @@ import {RunVisualizationService} from '../../../services/run-visualization.servi import {UtmChartClickActionService} from '../../../services/utm-chart-click-action.service'; import {rebuildVisualizationFilterTime} from '../../../util/chart-filter/chart-filter.util'; import {resolveDefaultVisualizationTime} from '../../../util/visualization/visualization-render.util'; -import {RefreshService, RefreshType} from "../../../../../shared/services/util/refresh.service"; -import {catchError, filter, map, switchMap, takeUntil, tap} from "rxjs/operators"; -import {TimeFilterBehavior} from "../../../../../shared/behaviors/time-filter.behavior"; @Component({ selector: 'app-map-view', @@ -54,168 +54,168 @@ export class MapViewComponent implements OnInit, AfterViewInit, OnDestroy { // tslint:disable-next-line:max-line-length res: { name: string, value: number[] }[] = [ { - "name": "57.135.189.63", - "value": [ + name: '57.135.189.63', + value: [ 26.2729, -80.26, 28.0 ] }, { - "name": "2601:58a:8984:9240:6463:35f3:3a5f:c61f", - "value": [ + name: '2601:58a:8984:9240:6463:35f3:3a5f:c61f', + value: [ 37.751, -97.822, 22.0 ] }, { - "name": "2601:58a:8984:9240:5c36:dc9f:f881:d87c", - "value": [ + name: '2601:58a:8984:9240:5c36:dc9f:f881:d87c', + value: [ 37.751, -97.822, 9.0 ] }, { - "name": "88.196.79.239", - "value": [ + name: '88.196.79.239', + value: [ 59.433, 24.7323, 7.0 ] }, { - "name": "2601:58a:8984:9240:c08d:edc:4c08:ca06", - "value": [ + name: '2601:58a:8984:9240:c08d:edc:4c08:ca06', + value: [ 37.751, -97.822, 6.0 ] }, { - "name": "2601:58a:8984:9240:a015:cc03:ee94:b65a", - "value": [ + name: '2601:58a:8984:9240:a015:cc03:ee94:b65a', + value: [ 37.751, -97.822, 4.0 ] }, { - "name": "2601:58a:8984:9240:b509:9ea9:cb8c:ba47", - "value": [ + name: '2601:58a:8984:9240:b509:9ea9:cb8c:ba47', + value: [ 37.751, -97.822, 4.0 ] }, { - "name": "2601:58a:8984:9240:1c73:b69d:5b2b:5b81", - "value": [ + name: '2601:58a:8984:9240:1c73:b69d:5b2b:5b81', + value: [ 37.751, -97.822, 3.0 ] }, { - "name": "2601:58a:8984:9240:5863:8f23:93eb:4f60", - "value": [ + name: '2601:58a:8984:9240:5863:8f23:93eb:4f60', + value: [ 37.751, -97.822, 3.0 ] }, { - "name": "2601:58a:8984:9240:6182:367b:f8f5:99cb", - "value": [ + name: '2601:58a:8984:9240:6182:367b:f8f5:99cb', + value: [ 37.751, -97.822, 3.0 ] }, { - "name": "2601:58a:8984:9240:6cca:ff0:72c2:98e1", - "value": [ + name: '2601:58a:8984:9240:6cca:ff0:72c2:98e1', + value: [ 37.751, -97.822, 3.0 ] }, { - "name": "2601:58a:8984:9240:916a:63e6:9196:57ee", - "value": [ + name: '2601:58a:8984:9240:916a:63e6:9196:57ee', + value: [ 37.751, -97.822, 3.0 ] }, { - "name": "2601:58a:8984:9240:9587:4412:99a:4169", - "value": [ + name: '2601:58a:8984:9240:9587:4412:99a:4169', + value: [ 37.751, -97.822, 3.0 ] }, { - "name": "2601:58a:8984:9240:9ddb:ce24:5ea2:2f84", - "value": [ + name: '2601:58a:8984:9240:9ddb:ce24:5ea2:2f84', + value: [ 37.751, -97.822, 3.0 ] }, { - "name": "2601:58a:8984:9240:b13f:97f3:2728:950", - "value": [ + name: '2601:58a:8984:9240:b13f:97f3:2728:950', + value: [ 37.751, -97.822, 3.0 ] }, { - "name": "2600:1700:234c:ac10:d0cb:fe60:ca36:8c59", - "value": [ + name: '2600:1700:234c:ac10:d0cb:fe60:ca36:8c59', + value: [ 25.6958, -80.3626, 2.0 ] }, { - "name": "2601:58a:8984:9240:217b:7bdc:ab48:e41a", - "value": [ + name: '2601:58a:8984:9240:217b:7bdc:ab48:e41a', + value: [ 37.751, -97.822, 2.0 ] }, { - "name": "2601:58a:8984:9240:9d3d:d2e1:d3f2:b458", - "value": [ + name: '2601:58a:8984:9240:9d3d:d2e1:d3f2:b458', + value: [ 37.751, -97.822, 2.0 ] }, { - "name": "107.202.154.128", - "value": [ + name: '107.202.154.128', + value: [ 25.7689, -80.1946, 1.0 ] }, { - "name": "2601:58a:8984:9240:a589:cf0c:9737:f4c6", - "value": [ + name: '2601:58a:8984:9240:a589:cf0c:9737:f4c6', + value: [ 37.751, -97.822, 1.0 ] }, { - "name": "2601:58a:8984:9240:ccbc:1190:7f84:c0b8", - "value": [ + name: '2601:58a:8984:9240:ccbc:1190:7f84:c0b8', + value: [ 37.751, -97.822, 1.0 @@ -322,8 +322,8 @@ export class MapViewComponent implements OnInit, AfterViewInit, OnDestroy { } }); - this.defaultTime = resolveDefaultVisualizationTime(this.visualization); - if(!this.defaultTime){ + if (!this.defaultTime) { + this.defaultTime = resolveDefaultVisualizationTime(this.visualization); this.refreshService.sendRefresh(this.refreshType); } } diff --git a/frontend/src/app/graphic-builder/shared/components/viewer/metric-view/metric-view.component.ts b/frontend/src/app/graphic-builder/shared/components/viewer/metric-view/metric-view.component.ts index 5f383fd82..c62ce7511 100644 --- a/frontend/src/app/graphic-builder/shared/components/viewer/metric-view/metric-view.component.ts +++ b/frontend/src/app/graphic-builder/shared/components/viewer/metric-view/metric-view.component.ts @@ -57,7 +57,6 @@ export class MetricViewComponent implements OnInit, OnDestroy { ngOnInit() { - this.defaultTime = resolveDefaultVisualizationTime(this.visualization); this.refreshType = `${this.chartId}`; this.data$ = this.refreshService.refresh$ @@ -100,6 +99,7 @@ export class MetricViewComponent implements OnInit, OnDestroy { }); if (!this.defaultTime) { + this.defaultTime = resolveDefaultVisualizationTime(this.visualization); this.refreshService.sendRefresh(this.refreshType); } } diff --git a/frontend/src/app/graphic-builder/shared/components/viewer/table-view/table-view.component.html b/frontend/src/app/graphic-builder/shared/components/viewer/table-view/table-view.component.html index f5fa5e626..a581a2c20 100644 --- a/frontend/src/app/graphic-builder/shared/components/viewer/table-view/table-view.component.html +++ b/frontend/src/app/graphic-builder/shared/components/viewer/table-view/table-view.component.html @@ -38,17 +38,8 @@
-
-
- - -
@@ -124,3 +115,13 @@ + + +
+ + +
+
diff --git a/frontend/src/app/graphic-builder/shared/components/viewer/table-view/table-view.component.ts b/frontend/src/app/graphic-builder/shared/components/viewer/table-view/table-view.component.ts index 3960996cb..02438d976 100644 --- a/frontend/src/app/graphic-builder/shared/components/viewer/table-view/table-view.component.ts +++ b/frontend/src/app/graphic-builder/shared/components/viewer/table-view/table-view.component.ts @@ -85,7 +85,6 @@ export class TableViewComponent implements OnInit, OnChanges, OnDestroy { } ngOnInit() { - this.defaultTime = resolveDefaultVisualizationTime(this.visualization); this.refreshType = `${this.chartId}`; this.data$ = this.refreshService.refresh$ @@ -127,6 +126,7 @@ export class TableViewComponent implements OnInit, OnChanges, OnDestroy { }); if (!this.defaultTime) { + this.defaultTime = resolveDefaultVisualizationTime(this.visualization); this.refreshService.sendRefresh(this.refreshType); } } diff --git a/frontend/src/app/graphic-builder/shared/components/viewer/utm-viewer/utm-viewer.component.ts b/frontend/src/app/graphic-builder/shared/components/viewer/utm-viewer/utm-viewer.component.ts index 8c717aa14..c5de565de 100644 --- a/frontend/src/app/graphic-builder/shared/components/viewer/utm-viewer/utm-viewer.component.ts +++ b/frontend/src/app/graphic-builder/shared/components/viewer/utm-viewer/utm-viewer.component.ts @@ -24,8 +24,7 @@ export class UtmViewerComponent implements OnInit { constructor() { } - ngOnInit() { - } + ngOnInit() {} onRunVisualization($event: string) { this.runned.emit($event); diff --git a/frontend/src/app/graphic-builder/visualization/visualization-list/visualization-list.component.html b/frontend/src/app/graphic-builder/visualization/visualization-list/visualization-list.component.html index 6d75ed79c..c5f96805c 100644 --- a/frontend/src/app/graphic-builder/visualization/visualization-list/visualization-list.component.html +++ b/frontend/src/app/graphic-builder/visualization/visualization-list/visualization-list.component.html @@ -101,7 +101,6 @@
Visualizations
- Add flow
diff --git a/frontend/src/app/incident-response/playbook-builder/playbook-builder.component.spec.ts b/frontend/src/app/incident-response/playbook-builder/playbook-builder.component.spec.ts deleted file mode 100644 index 915985b74..000000000 --- a/frontend/src/app/incident-response/playbook-builder/playbook-builder.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { PlaybookBuilderComponent } from './playbook-builder.component'; - -describe('PlaybookBuilderComponent', () => { - let component: PlaybookBuilderComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ PlaybookBuilderComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(PlaybookBuilderComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/incident-response/playbook-builder/playbook-builder.component.ts b/frontend/src/app/incident-response/playbook-builder/playbook-builder.component.ts index d20ec31e9..e2af5c73a 100644 --- a/frontend/src/app/incident-response/playbook-builder/playbook-builder.component.ts +++ b/frontend/src/app/incident-response/playbook-builder/playbook-builder.component.ts @@ -23,6 +23,7 @@ import {createElementPrefix, getElementPrefix} from '../../shared/util/string-ut import {IncidentResponseRuleService} from '../shared/services/incident-response-rule.service'; import {WorkflowActionsService} from '../shared/services/workflow-actions.service'; import {IncidentRuleType} from '../shared/type/incident-rule.type'; +import {minLengthArray} from "../../rule-management/custom-validators"; @Component({ selector: 'app-playbook-builder', @@ -61,7 +62,7 @@ export class PlaybookBuilderComponent implements OnInit, OnDestroy { id: [null], name: ['', Validators.required], description: ['', Validators.required], - conditions: this.fb.array([]), + conditions: this.fb.array([], minLengthArray(1)), command: ['', Validators.required], actions: [[]], active: [true], @@ -89,12 +90,26 @@ export class PlaybookBuilderComponent implements OnInit, OnDestroy { map(res => res.body)); }) ).subscribe(rule => { - console.log(rule.conditions); - this.rule = rule; + + const isTemplate = this.route.snapshot.queryParams.template === 'true'; + const ruleData = isTemplate ? { ...rule, id: null } : rule; + const actions = isTemplate ? ruleData.actions.map(item => { + return { + ...item, + id: null + }; + }) : ruleData.actions; + + this.rule = ruleData; + this.rule.actions = actions; + this.exist = false; this.typing = false; this.rulePrefix = getElementPrefix(this.rule.name); - this.formRule.patchValue(this.rule, {emitEvent: false}); + + + this.formRule.patchValue(ruleData, { emitEvent: false }); + const name = this.formRule.get('name').value; this.formRule.get('name').setValue(this.replacePrefixInName(name)); @@ -122,9 +137,9 @@ export class PlaybookBuilderComponent implements OnInit, OnDestroy { this.route.queryParams .pipe( filter(params => !!params && !!params.alertName), - map(params => params.alertName)).subscribe((alertName)=>{ + map(params => params.alertName)).subscribe((alertName) => { this.addRuleCondition(); - const rc = this.ruleConditions.at(this.ruleConditions.length-1); + const rc = this.ruleConditions.at(this.ruleConditions.length - 1); rc.patchValue({ field: 'name', value: alertName, @@ -180,7 +195,7 @@ export class PlaybookBuilderComponent implements OnInit, OnDestroy { } createRule() { - if (this.rule) { + if (this.rule.id) { this.editRule(); } else { this.saveRule(); diff --git a/frontend/src/app/incident-response/playbooks/playbooks.component.html b/frontend/src/app/incident-response/playbooks/playbooks.component.html index f639b4ddb..5eab287a4 100644 --- a/frontend/src/app/incident-response/playbooks/playbooks.component.html +++ b/frontend/src/app/incident-response/playbooks/playbooks.component.html @@ -102,26 +102,35 @@
FLOWS
- -
-
- - -

Start building

-

Begin with a template, or start from scratch.

- - - New Flow - + + +
+ +

Start building

+

Begin with a template, or start from scratch.

+ + + New Flow + +
- + -
+
+ + +
{ - let component: PlaybooksComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ PlaybooksComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(PlaybooksComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/incident-response/playbooks/playbooks.component.ts b/frontend/src/app/incident-response/playbooks/playbooks.component.ts index e7be5b1bd..6854445f5 100644 --- a/frontend/src/app/incident-response/playbooks/playbooks.component.ts +++ b/frontend/src/app/incident-response/playbooks/playbooks.component.ts @@ -11,13 +11,14 @@ import {UtmAlertType} from '../../shared/types/alert/utm-alert.type'; import {TimeFilterType} from '../../shared/types/time-filter.type'; import {IncidentResponseRuleService} from '../shared/services/incident-response-rule.service'; import {IncidentRuleType} from '../shared/type/incident-rule.type'; -import {PlaybookService} from './playbook.service'; +import {PlaybookService} from '../shared/services/playbook.service'; import {NewPlaybookComponent} from "../shared/component/new-playbook/new-playbook.component"; @Component({ selector: 'app-playbooks', templateUrl: './playbooks.component.html', - styleUrls: ['./playbooks.component.scss'] + styleUrls: ['./playbooks.component.scss'], + providers: [PlaybookService] }) export class PlaybooksComponent implements OnInit, AfterViewInit, OnDestroy { loading = true; diff --git a/frontend/src/app/incident-response/shared/component/action-builder/action-builder.component.html b/frontend/src/app/incident-response/shared/component/action-builder/action-builder.component.html index fda48e529..234498252 100644 --- a/frontend/src/app/incident-response/shared/component/action-builder/action-builder.component.html +++ b/frontend/src/app/incident-response/shared/component/action-builder/action-builder.component.html @@ -22,9 +22,15 @@

Flow Actions

Info! - Select the agent handling strategy for the automation. If not active, commands will run on specified platform agents if the trigger conditions and dataSource field value of the alert match. Alternatively, choose a default agent to run the automation if no other agent matches the criteria. If this option is active, commands will run only on specified platform agents if the trigger conditions and dataSource field value of the alert match, if not, the automation won't be executed. + + By default, the agent that detects an alert is responsible for executing its corresponding SOAR workflow. + However, you can designate an alternative agent to serve as a proxy for specific workflows. + This is particularly beneficial for workflows that require interaction with external systems, + such as firewall integrations for IP blocking or endpoint security platforms for threat containment. +
+
-
+
-
{ - let component: ActionSidebarComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ ActionSidebarComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ActionSidebarComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/incident-response/shared/component/action-sidebar/action-sidebar.component.ts b/frontend/src/app/incident-response/shared/component/action-sidebar/action-sidebar.component.ts index 7ae7fd0f2..93d0cc060 100644 --- a/frontend/src/app/incident-response/shared/component/action-sidebar/action-sidebar.component.ts +++ b/frontend/src/app/incident-response/shared/component/action-sidebar/action-sidebar.component.ts @@ -1,9 +1,9 @@ import {Component, OnDestroy, OnInit} from '@angular/core'; import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; +import {last} from 'rxjs/operators'; import {WorkflowActionsService} from '../../services/workflow-actions.service'; import {ActionTerminalComponent} from '../action-terminal/action-terminal.component'; import {ActionSidebarService} from './action-sidebar.service'; -import {last} from "rxjs/operators"; @Component({ selector: 'app-action-sidebar', @@ -12,17 +12,18 @@ import {last} from "rxjs/operators"; }) export class ActionSidebarComponent implements OnInit, OnDestroy { + constructor(private workFlowActionService: WorkflowActionsService, + public actionSidebarService: ActionSidebarService, + private modalService: NgbModal) { } + request = { page: 0, - size: 5, + size: 10, 'systemOwner.equals': true }; searching: any; - - constructor(private workFlowActionService: WorkflowActionsService, - public actionSidebarService: ActionSidebarService, - private modalService: NgbModal) { } + readonly last = last; ngOnInit() { this.actionSidebarService.loadData({ @@ -31,7 +32,11 @@ export class ActionSidebarComponent implements OnInit, OnDestroy { } addToWorkFlow(action: any) { - this.workFlowActionService.addActions(action); + const actionToAdd = { + ...action, + id: null + }; + this.workFlowActionService.addActions(actionToAdd); } searchReport($event: string ) { @@ -56,9 +61,14 @@ export class ActionSidebarComponent implements OnInit, OnDestroy { } onScroll() { - this.actionSidebarService.loadData({ + + this.request = { ...this.request, - size: this.request.size + 10, + size: this.request.size + 10 + }; + + this.actionSidebarService.loadData({ + ...this.request }); } @@ -69,6 +79,4 @@ export class ActionSidebarComponent implements OnInit, OnDestroy { ngOnDestroy() { this.actionSidebarService.reset(); } - - protected readonly last = last; } diff --git a/frontend/src/app/incident-response/shared/component/condition-builder/condition-builder.component.html b/frontend/src/app/incident-response/shared/component/condition-builder/condition-builder.component.html index 2097e4ae7..bfb99ffcd 100644 --- a/frontend/src/app/incident-response/shared/component/condition-builder/condition-builder.component.html +++ b/frontend/src/app/incident-response/shared/component/condition-builder/condition-builder.component.html @@ -39,7 +39,7 @@

Flow Trigger

-
+
Flow Trigger
-
+
You must define at least one condition.
@@ -67,3 +67,4 @@

Flow Trigger

+ diff --git a/frontend/src/app/incident-response/shared/component/condition-builder/condition-builder.component.spec.ts b/frontend/src/app/incident-response/shared/component/condition-builder/condition-builder.component.spec.ts deleted file mode 100644 index 72bc723e5..000000000 --- a/frontend/src/app/incident-response/shared/component/condition-builder/condition-builder.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ConditionBuilderComponent } from './condition-builder.component'; - -describe('ConditionBuilderComponent', () => { - let component: ConditionBuilderComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ ConditionBuilderComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ConditionBuilderComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/incident-response/shared/component/condition-item/condition-item.component.html b/frontend/src/app/incident-response/shared/component/condition-item/condition-item.component.html index 9fc72f1d2..ea3dbebf2 100644 --- a/frontend/src/app/incident-response/shared/component/condition-item/condition-item.component.html +++ b/frontend/src/app/incident-response/shared/component/condition-item/condition-item.component.html @@ -59,11 +59,12 @@
diff --git a/frontend/src/app/incident-response/shared/component/condition-item/condition-item.component.spec.ts b/frontend/src/app/incident-response/shared/component/condition-item/condition-item.component.spec.ts deleted file mode 100644 index 60fee3f56..000000000 --- a/frontend/src/app/incident-response/shared/component/condition-item/condition-item.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ConditionItemComponent } from './condition-item.component'; - -describe('ConditionItemComponent', () => { - let component: ConditionItemComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ ConditionItemComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ConditionItemComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/incident-response/shared/component/condition-item/condition-item.component.ts b/frontend/src/app/incident-response/shared/component/condition-item/condition-item.component.ts index 4c11faac2..926954785 100644 --- a/frontend/src/app/incident-response/shared/component/condition-item/condition-item.component.ts +++ b/frontend/src/app/incident-response/shared/component/condition-item/condition-item.component.ts @@ -21,6 +21,8 @@ export class ConditionItemComponent implements OnInit { @Input() index: number; @Input() fields: ElasticSearchFieldInfoType[]; @Output() delete: EventEmitter = new EventEmitter(); + filteredFields: ElasticSearchFieldInfoType[] = []; + protected readonly operatorEnum = ElasticOperatorsEnum; selectableOperators = [ElasticOperatorsEnum.IS_ONE_OF, ElasticOperatorsEnum.IS_NOT_ONE_OF, ElasticOperatorsEnum.CONTAIN_ONE_OF, @@ -40,6 +42,7 @@ export class ConditionItemComponent implements OnInit { private operatorsService: OperatorService) { } ngOnInit() { + this.filteredFields = this.fields.filter(field => field.name && !field.name.includes('.keyword')); } onScroll() { @@ -133,5 +136,22 @@ export class ConditionItemComponent implements OnInit { this.isMultipleSelectValue(); } - protected readonly operatorEnum = ElasticOperatorsEnum; + onSearch(term: { term: string }) { + this.filteredFields = this.fields.filter(field => field.name && !field.name.includes('.keyword')); + if (!term.term) { + return; + } + + const searchTerm = term.term.toLowerCase(); + this.filteredFields = this.filteredFields + .filter(field => field.name.toLowerCase().includes(searchTerm)) + .sort((a, b) => { + const aStarts = a.name.toLowerCase().startsWith(searchTerm); + const bStarts = b.name.toLowerCase().startsWith(searchTerm); + + if (aStarts && !bStarts) { return -1; } + if (!aStarts && bStarts) { return 1; } + return a.name.localeCompare(b.name); + }); + } } diff --git a/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.html b/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.html index 59fa084ed..6ac0fe4fd 100644 --- a/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.html +++ b/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.html @@ -1,10 +1,10 @@
-
+

Get started

Start with templates showcasing common response playbooks and workflows.

-
+
-
- -

Letโ€™s get started

-

No predefined flows were found. Create your first one to begin.

+
+
+ +
+ +
+ +
+
+ {{ item.name }} + +
+

+ {{ item.description }} +

+
+ + Edited {{ item.createdDate | relativeTime }} +
+
-
+
+
+ +

Let's get started

+

No predefined flows were found. Create your first one to begin.

+
+
+
+
-
+
-
Start from scratch
+
Start from scratch
Begin with a fresh flow to build from scratch.
- +
+ - diff --git a/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.scss b/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.scss index 23c1e07c5..8635772fd 100644 --- a/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.scss +++ b/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.scss @@ -64,3 +64,92 @@ .h-8 { height: 2rem; } + +.playbook-list-container { + padding-right: 8px; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 3px; + + &:hover { + background: #9ca3af; + } + } +} + +.playbook-item-card { + background: #f9fafb; + border-color: #e5e7eb; + transition: all 0.2s ease; + margin-bottom: 8px; // Separaciรณn entre cards + + &:hover { + background: #ffffff; + border-color: #cbd5e1; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + } + + &:last-child { + margin-bottom: 0; + } +} + +.playbook-icon-small { + width: 36px; + height: 36px; + color: #6b7280; + font-size: 16px; + margin-right: 16px; // Mejor separaciรณn del icono al contenido + flex-shrink: 0; +} + +.playbook-content { + min-width: 0; +} + +.playbook-title { + font-size: 14px; + font-weight: 500; + color: #111827; + line-height: 1.5; +} + +.playbook-description { + font-size: 13px; + color: #6b7280; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + margin-bottom: 8px; +} + +.playbook-date { + font-size: 12px; + color: #9ca3af; + display: flex; + align-items: center; + gap: 6px; // Separaciรณn entre icono y texto + + i { + font-size: 11px; + } +} + +.playbook-arrow { + color: #9ca3af; + font-size: 14px; + flex-shrink: 0; + margin-left: 8px; +} + diff --git a/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.ts b/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.ts index 6cf9dba80..7427c9351 100644 --- a/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.ts +++ b/frontend/src/app/incident-response/shared/component/new-playbook/new-playbook.component.ts @@ -1,33 +1,65 @@ -import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; -import {Router} from "@angular/router"; +import {AfterViewInit, Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {Router} from '@angular/router'; import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; +import {Observable, of} from 'rxjs'; import {UtmToastService} from '../../../../shared/alert/utm-toast.service'; import {InputClassResolve} from '../../../../shared/util/input-class-resolve'; +import {IncidentResponseRuleService} from '../../services/incident-response-rule.service'; +import {PlaybookService} from '../../services/playbook.service'; import {IncidentActionType} from '../../type/incident-action.type'; @Component({ selector: 'app-incident-response-command-create', templateUrl: './new-playbook.component.html', - styleUrls: ['./new-playbook.component.scss'] + styleUrls: ['./new-playbook.component.scss'], + providers: [PlaybookService] }) -export class NewPlaybookComponent implements OnInit { +export class NewPlaybookComponent implements OnInit, AfterViewInit { @Input() action: IncidentActionType; @Output() actionCreated = new EventEmitter(); + request = { + page: 0, + size: 25, + sort: '', + 'active.equals': null, + 'agentPlatform.equals': null, + 'createdBy.equals': null, + 'systemOwner.equals': true + }; + platforms: string[]; + + platforms$: Observable; + loadingPlatform = false; + constructor(public activeModal: NgbActiveModal, public inputClass: InputClassResolve, - public utmToastService: UtmToastService, + private utmToastService: UtmToastService, + private incidentResponseRuleService: IncidentResponseRuleService, + public playbookService: PlaybookService, private router: Router) { } ngOnInit() {} - createNewPlaybook() { - this.router.navigate(['/soar/create-flow']); + ngAfterViewInit(): void { + this.playbookService.loadData({...this.request}); + } + + createNewPlaybook(params?: any) { + this.router.navigate(['/soar/create-flow'], { queryParams: params }); this.activeModal.close(); } searchByRule($event: string | number) { + this.request['name.contains'] = $event; + this.playbookService.loadData({ + ...this.request, + page: 0, + }); + } + trackByFn(index: number, item: any) { + return item.id || index; } } diff --git a/frontend/src/app/incident-response/shared/incident-response-shared.module.ts b/frontend/src/app/incident-response/shared/incident-response-shared.module.ts index a83ba7a6b..f5be9db2f 100644 --- a/frontend/src/app/incident-response/shared/incident-response-shared.module.ts +++ b/frontend/src/app/incident-response/shared/incident-response-shared.module.ts @@ -26,6 +26,8 @@ import {NewPlaybookComponent} from './component/new-playbook/new-playbook.compon import { AgentSidebarComponent } from './component/agent-sidebar/agent-sidebar.component'; import { AgentInfoComponent } from './component/agent-info/agent-info.component'; import { InteractiveConsoleComponent } from './component/interactive-console/interactive-console.component'; +import {PlaybookService} from "./services/playbook.service"; +import {RouterModule} from "@angular/router"; @NgModule({ declarations: [IrJobCreateComponent, @@ -84,7 +86,8 @@ import { InteractiveConsoleComponent } from './component/interactive-console/int InfiniteScrollModule, FormsModule, NgSelectModule, - UtmDashboardSharedModule + UtmDashboardSharedModule, + RouterModule ] }) export class IncidentResponseSharedModule { diff --git a/frontend/src/app/incident-response/playbooks/playbook.service.ts b/frontend/src/app/incident-response/shared/services/playbook.service.ts similarity index 80% rename from frontend/src/app/incident-response/playbooks/playbook.service.ts rename to frontend/src/app/incident-response/shared/services/playbook.service.ts index ec5ff0113..084aa8a66 100644 --- a/frontend/src/app/incident-response/playbooks/playbook.service.ts +++ b/frontend/src/app/incident-response/shared/services/playbook.service.ts @@ -1,12 +1,10 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, of, Subject } from 'rxjs'; import { catchError, filter, finalize, map, switchMap } from 'rxjs/operators'; -import { UtmToastService } from '../../shared/alert/utm-toast.service'; -import { IncidentResponseRuleService } from '../shared/services/incident-response-rule.service'; +import { UtmToastService } from '../../../shared/alert/utm-toast.service'; +import { IncidentResponseRuleService } from './incident-response-rule.service'; -@Injectable({ - providedIn: 'root' -}) +@Injectable() export class PlaybookService { private request$ = new Subject(); private loading = new BehaviorSubject(false); @@ -22,7 +20,7 @@ export class PlaybookService { playbooks$ = this.request$.pipe( filter(request => !!request), switchMap(request => { - this.loading.next(true); + setTimeout(() => this.loading.next(true), 300); return this.incidentResponseRuleService.query(request).pipe( map(response => { this.totalItems.next(Number(response.headers.get('X-Total-Count'))); @@ -32,7 +30,7 @@ export class PlaybookService { this.utmToastService.showError('Error', 'An error occurred while fetching playbooks.'); return of([]); }), - finalize(() => this.loading.next(false)) + finalize(() => setTimeout(() => this.loading.next(false), 200)) ); }) ); diff --git a/frontend/src/app/incident/incident-shared/component/add-alert-to-incident/add-alert-to-incident.component.ts b/frontend/src/app/incident/incident-shared/component/add-alert-to-incident/add-alert-to-incident.component.ts index 0e6d5ce17..66561df39 100644 --- a/frontend/src/app/incident/incident-shared/component/add-alert-to-incident/add-alert-to-incident.component.ts +++ b/frontend/src/app/incident/incident-shared/component/add-alert-to-incident/add-alert-to-incident.component.ts @@ -2,6 +2,9 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; import {FormBuilder, FormGroup, Validators} from '@angular/forms'; import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; import {AlertUpdateHistoryBehavior} from '../../../../data-management/alert-management/shared/behavior/alert-update-history.behavior'; +import { + AlertActionRefreshService +} from '../../../../data-management/alert-management/shared/services/alert-action-refresh.service'; import {AlertManagementService} from '../../../../data-management/alert-management/shared/services/alert-management.service'; import {UtmToastService} from '../../../../shared/alert/utm-toast.service'; import { @@ -16,11 +19,6 @@ import {NewIncidentAlert} from '../../../../shared/types/incident/new-incident.t import {UtmIncidentType} from '../../../../shared/types/incident/utm-incident.type'; import {getValueFromPropertyPath} from '../../../../shared/util/get-value-object-from-property-path.util'; import {InputClassResolve} from '../../../../shared/util/input-class-resolve'; -import { - ModalConfirmationComponent -} from "../../../../shared/components/utm/util/modal-confirmation/modal-confirmation.component"; -import {ModalService} from "../../../../core/modal/modal.service"; -import {Router} from "@angular/router"; @Component({ selector: 'app-add-alert-to-incident', @@ -46,6 +44,7 @@ export class AddAlertToIncidentComponent implements OnInit { private alertManagementService: AlertManagementService, private utmToastService: UtmToastService, private alertUpdateHistoryBehavior: AlertUpdateHistoryBehavior, + private alertActionRefreshService: AlertActionRefreshService, private fb: FormBuilder) { } @@ -114,6 +113,7 @@ export class AddAlertToIncidentComponent implements OnInit { this.alertManagementService.markAsIncident(alertIds, incident.incidentName, incident.id, 'INCIDENT').subscribe(response => { this.alertUpdateHistoryBehavior.$refreshHistory.next(true); this.incidentAdded.emit(incident); + this.alertActionRefreshService.incidentCreated(incident); this.activeModal.close(); }, error => { this.utmToastService.showError('Error adding incident', diff --git a/frontend/src/app/logstash/logstash-filters/logstash-filter-create/logstash-filter-create.component.html b/frontend/src/app/logstash/logstash-filters/logstash-filter-create/logstash-filter-create.component.html index 4d8c9863e..7b99ef166 100644 --- a/frontend/src/app/logstash/logstash-filters/logstash-filter-create/logstash-filter-create.component.html +++ b/frontend/src/app/logstash/logstash-filters/logstash-filter-create/logstash-filter-create.component.html @@ -1,13 +1,13 @@ -->
diff --git a/frontend/src/app/logstash/logstash-filters/logstash-filters.component.html b/frontend/src/app/logstash/logstash-filters/logstash-filters.component.html index 32c592c7d..f2e37d578 100644 --- a/frontend/src/app/logstash/logstash-filters/logstash-filters.component.html +++ b/frontend/src/app/logstash/logstash-filters/logstash-filters.component.html @@ -1,9 +1,9 @@ -
+
Pipeline filters
- +
-
+
@@ -56,6 +55,16 @@
+ + +
+ +
@@ -70,10 +79,12 @@
- + +
diff --git a/frontend/src/app/logstash/logstash-filters/logstash-filters.component.ts b/frontend/src/app/logstash/logstash-filters/logstash-filters.component.ts index f6bac9a84..2a7d5afcf 100644 --- a/frontend/src/app/logstash/logstash-filters/logstash-filters.component.ts +++ b/frontend/src/app/logstash/logstash-filters/logstash-filters.component.ts @@ -18,17 +18,17 @@ import {UtmPipeline} from '../shared/types/logstash-stats.type'; }) export class LogstashFiltersComponent implements OnInit { @Input() pipeline: UtmPipeline; + @Input() enableQuickCreate = false; filters: LogstashFilterType[] = []; - loading = true; + filter: LogstashFilterType = {} as LogstashFilterType; + loading = false; totalItems: any; requestParams = { page: 0, size: ITEMS_PER_PAGE, - 'filterName.contains': null, - 'isActive.equals': true, - pipelineId: null, - sort: 'id,asc' + sort: 'id,asc', + pipelineId: null }; openEditJson: any; filterEdit: LogstashFilterType; @@ -43,7 +43,11 @@ export class LogstashFiltersComponent implements OnInit { ngOnInit() { if (this.pipeline) { - this.requestParams.pipelineId = this.pipeline.id; + this.loading = true; + this.requestParams = { + ...this.requestParams, + pipelineId: this.pipeline.id + }; this.lineColor = this.pipeline.pipelineStatus === 'up' ? 'green' : 'red'; this.getLogsFilters(); } @@ -67,11 +71,17 @@ export class LogstashFiltersComponent implements OnInit { } onFilterEditClose() { + this.enableQuickCreate = false; this.requestParams.page = 0; this.getLogsFilters(); this.openEditJson = false; } + onFilterEditDismiss() { + this.enableQuickCreate = false; + this.openEditJson = false; + } + deleteFilter(filter: LogstashFilterType) { const modalFilter = this.modalService.open(ModalConfirmationComponent, {centered: true}); modalFilter.componentInstance.header = 'Delete asset'; @@ -98,7 +108,12 @@ export class LogstashFiltersComponent implements OnInit { private onSuccess(data: any[], headers) { this.totalItems = headers.get('X-Total-Count'); this.filters = data; + this.filter = data[0] || {}; this.loading = false; + + if (this.enableQuickCreate) { + this.addCustomFilter(); + } } private onError(error) { @@ -125,4 +140,9 @@ export class LogstashFiltersComponent implements OnInit { this.filterEdit = filter; this.openEditJson = true; } + + addCustomFilter() { + this.filterEdit = null; + this.openEditJson = true; + } } diff --git a/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.html b/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.html index 91f5aed06..527ea958f 100644 --- a/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.html +++ b/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.html @@ -13,15 +13,20 @@
*ngFor="let pipeline of logstashPipelines.pipelines" [ngClass]="{'card-pipeline-selected':pipelineDetail && pipeline.pipelineId === pipelineDetail.pipelineId}" (click)="viewPipeline(pipeline)"> -
+
+
- {{ pipeline.pipelineName }} -
+ {{ pipeline.pipelineName }} +
+ +
@@ -42,28 +47,28 @@
- +
-
+
-->
-
+
@@ -174,7 +179,10 @@
*ngTemplateOutlet="pipelineEventTemplate; context: { pipeline: pipelineDetail, showColum:false }">
- + +
diff --git a/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.scss b/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.scss index 585d69fdd..af3db2370 100644 --- a/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.scss +++ b/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.scss @@ -387,3 +387,15 @@ p { font-size: 0.88rem!important; } +.btn-outline-info { + color: #0277bd !important;; + border-color: #0277bd !important; + padding: 0.175rem .75rem!important; +} + +.btn-outline-info:hover { + color: #fff!important; + background-color: #0277bd!important; + border-color: #0277bd!important; +} + diff --git a/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.ts b/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.ts index 168a1dfec..324ed5730 100644 --- a/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.ts +++ b/frontend/src/app/logstash/logstash-pipelines/logstash-pipelines.component.ts @@ -24,6 +24,7 @@ export class LogstashPipelinesComponent implements OnInit, OnDestroy, AfterViewC img3: string; img4: string; img5: string; + enableQuickCreate = false; constructor(private elasticHealthService: ElasticHealthService, private logstashService: LogstashService, @@ -33,10 +34,10 @@ export class LogstashPipelinesComponent implements OnInit, OnDestroy, AfterViewC ngOnInit() { this.getHealth(); this.getLogstashStats(); - this.interval = setInterval(() => { + /*this.interval = setInterval(() => { this.getHealth(); this.getLogstashStats(); - }, 5000); + }, 5000);*/ this.imageChangeInterval = setInterval(() => this.setImages(), 8000); } @@ -71,6 +72,7 @@ export class LogstashPipelinesComponent implements OnInit, OnDestroy, AfterViewC closeDetail() { this.pipelineDetail = null; + this.enableQuickCreate = false; } getClusterHealth() { @@ -100,4 +102,9 @@ export class LogstashPipelinesComponent implements OnInit, OnDestroy, AfterViewC const icon = this.svgMap[module]; return icon ? icon : 'generic.svg'; } + + addCustomFilter(pipeline: UtmPipeline) { + this.enableQuickCreate = true; + this.viewPipeline(pipeline); + } } diff --git a/frontend/src/app/rule-management/app-rule/components/add-rule/add-rule.component.html b/frontend/src/app/rule-management/app-rule/components/add-rule/add-rule.component.html index 2d3b4cf21..f3091f0d4 100644 --- a/frontend/src/app/rule-management/app-rule/components/add-rule/add-rule.component.html +++ b/frontend/src/app/rule-management/app-rule/components/add-rule/add-rule.component.html @@ -225,6 +225,14 @@
+
+ + After Events + +

+ Define actions that will execute on all historically stored data when the correlation rule expression evaluates successfully. +

+
diff --git a/frontend/src/app/rule-management/app-rule/components/add-rule/add-rule.component.scss b/frontend/src/app/rule-management/app-rule/components/add-rule/add-rule.component.scss index 4b24416bf..79382b943 100644 --- a/frontend/src/app/rule-management/app-rule/components/add-rule/add-rule.component.scss +++ b/frontend/src/app/rule-management/app-rule/components/add-rule/add-rule.component.scss @@ -63,14 +63,6 @@ fieldset { height: 100%; } -.form-body { - min-height: 590px; - max-height: 700px; - overflow-y: auto!important; - overflow-x: hidden!important; - flex-grow: 1; -} - .type-select .ng-select-container { height: 36px!important; } diff --git a/frontend/src/app/rule-management/app-rule/components/rule-list/rule-list.component.ts b/frontend/src/app/rule-management/app-rule/components/rule-list/rule-list.component.ts index e7cbdff24..5e8982cd6 100644 --- a/frontend/src/app/rule-management/app-rule/components/rule-list/rule-list.component.ts +++ b/frontend/src/app/rule-management/app-rule/components/rule-list/rule-list.component.ts @@ -4,7 +4,7 @@ import {ActivatedRoute, Router} from '@angular/router'; import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap'; import {NgxSpinnerService} from 'ngx-spinner'; import {merge, Observable, of, Subject} from 'rxjs'; -import {catchError, filter, map, switchMap, takeUntil, tap, finalize} from 'rxjs/operators'; +import {catchError, filter, finalize, map, switchMap, takeUntil, tap} from 'rxjs/operators'; import { SortEvent } from 'src/app/shared/directives/sortable/type/sort-event'; import {UtmToastService} from '../../../../shared/alert/utm-toast.service'; import { @@ -19,7 +19,7 @@ import {FilterService} from '../../../services/filter.service'; import {RuleService} from '../../../services/rule.service'; import {AddRuleComponent} from '../add-rule/add-rule.component'; import {ImportRuleComponent} from '../import-rules/import-rule.component'; -import {SeeRuleComponent} from '../see-rule/see-rule.component' +import {RuleViewComponent} from '../see-rule/rule-view.component'; @Component({ @@ -50,7 +50,7 @@ export class RuleListComponent implements OnInit, OnDestroy { isInitialized = false; request = RULE_REQUEST; destroy$: Subject = new Subject(); - loadingRules=[] + loadingRules = []; constructor(private route: ActivatedRoute, private filterService: FilterService, @@ -154,10 +154,10 @@ export class RuleListComponent implements OnInit, OnDestroy { id: rule.id, active: !rule.ruleActive }; - this.loadingRules.push(rule.id) - const index = this.loadingRules.length-1 + this.loadingRules.push(rule.id); + const index = this.loadingRules.length - 1; this.ruleService.activeRule(params).pipe( - finalize(()=>this.loadingRules.splice(index,1)) + finalize(() => this.loadingRules.splice(index, 1)) ) .subscribe(() => this.ruleService.notifyRefresh(true), () => { @@ -201,7 +201,10 @@ export class RuleListComponent implements OnInit, OnDestroy { } editRule(rule: Rule) { - const modal = this.modalService.open(AddRuleComponent, {size: 'lg', centered: true}); + const modal = this.modalService.open(AddRuleComponent, { + size: 'lg', + centered: true, + windowClass: 'add-rule-modal'}); modal.componentInstance.rule = rule; modal.componentInstance.mode = 'EDIT'; @@ -209,8 +212,12 @@ export class RuleListComponent implements OnInit, OnDestroy { } - visualizeRule(rule:Rule){ - const modal = this.modalService.open(SeeRuleComponent, {size: 'lg', centered: true}); + visualizeRule(rule: Rule) { + const modal = this.modalService.open(RuleViewComponent, { + size: 'lg', + centered: true, + windowClass: 'view-rule-modal', + }); modal.componentInstance.rowDocument = rule; } diff --git a/frontend/src/app/rule-management/app-rule/components/see-rule/rule-view.component.html b/frontend/src/app/rule-management/app-rule/components/see-rule/rule-view.component.html new file mode 100644 index 000000000..d326d5738 --- /dev/null +++ b/frontend/src/app/rule-management/app-rule/components/see-rule/rule-view.component.html @@ -0,0 +1,17 @@ + + + + + + + +
+ +
+ +
+ + +
diff --git a/frontend/src/app/rule-management/app-rule/components/see-rule/rule-view.component.scss b/frontend/src/app/rule-management/app-rule/components/see-rule/rule-view.component.scss new file mode 100644 index 000000000..2f8409575 --- /dev/null +++ b/frontend/src/app/rule-management/app-rule/components/see-rule/rule-view.component.scss @@ -0,0 +1,36 @@ +app-utm-modal-header { + position: sticky; + top: 0; + z-index: 10; +} +.rule-view-container { + max-height: 650px; +} +.copy-btn { + border: none; + background: transparent; + cursor: pointer; + font-size: 18px; +} + +.copied-msg { + color: green; + font-size: 12px; +} + +.floating-btn { + position: absolute; + top: 4.5rem; + right: 1.5rem; + z-index: 20; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + color: #fff; + transition: all 0.2s ease; + + &:hover { + transform: scale(1.05); + } +} diff --git a/frontend/src/app/rule-management/app-rule/components/see-rule/see-rule.component.ts b/frontend/src/app/rule-management/app-rule/components/see-rule/rule-view.component.ts similarity index 78% rename from frontend/src/app/rule-management/app-rule/components/see-rule/see-rule.component.ts rename to frontend/src/app/rule-management/app-rule/components/see-rule/rule-view.component.ts index c963b9751..126886e59 100644 --- a/frontend/src/app/rule-management/app-rule/components/see-rule/see-rule.component.ts +++ b/frontend/src/app/rule-management/app-rule/components/see-rule/rule-view.component.ts @@ -5,11 +5,11 @@ import { Rule } from '../../../models/rule.model'; import * as yaml from 'js-yaml'; @Component({ - selector: 'app-see-rule', - templateUrl: './see-rule.component.html', - styleUrls: ['./see-rule.component.scss'], + selector: 'app-rule-view', + templateUrl: './rule-view.component.html', + styleUrls: ['./rule-view.component.scss'], }) -export class SeeRuleComponent { +export class RuleViewComponent { @Input() rowDocument: Rule; copied = false; @@ -18,7 +18,6 @@ export class SeeRuleComponent { try { return yaml.dump(this.rowDocument, { indent: 2 }); } catch (e) { - console.log(e) return 'Error parsing YAML'; } } diff --git a/frontend/src/app/rule-management/app-rule/components/see-rule/see-rule.component.html b/frontend/src/app/rule-management/app-rule/components/see-rule/see-rule.component.html deleted file mode 100644 index f318a0333..000000000 --- a/frontend/src/app/rule-management/app-rule/components/see-rule/see-rule.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - -
-
- - โœ… Copied! - -
- -
diff --git a/frontend/src/app/rule-management/app-rule/components/see-rule/see-rule.component.scss b/frontend/src/app/rule-management/app-rule/components/see-rule/see-rule.component.scss deleted file mode 100644 index 24b2951e2..000000000 --- a/frontend/src/app/rule-management/app-rule/components/see-rule/see-rule.component.scss +++ /dev/null @@ -1,20 +0,0 @@ - -/* utm-json-detail-view.component.scss */ -.json-detail-container { - display: flex; - flex-direction: column; - gap: 8px; -} - - -.copy-btn { - border: none; - background: transparent; - cursor: pointer; - font-size: 18px; -} - -.copied-msg { - color: green; - font-size: 12px; -} diff --git a/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts b/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts new file mode 100644 index 000000000..b82b574d6 --- /dev/null +++ b/frontend/src/app/rule-management/app-rule/validators/ip.forms.validators.ts @@ -0,0 +1,86 @@ +import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms'; + +export class IpFormsValidators { + + static ipOrCidr(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (!control.value) { + return null; + } + + const value = control.value.trim(); + + if (value.includes('/')) { + return IpFormsValidators.validateCIDR(value) ? null : { invalidCidr: true }; + } + + const isValidIPv4 = IpFormsValidators.validateIPv4(value); + const isValidIPv6 = IpFormsValidators.validateIPv6(value); + + return (isValidIPv4 || isValidIPv6) ? null : { invalidIp: true }; + }; + } + + private static validateIPv4(ip: string): boolean { + const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; + const match = ip.match(ipv4Regex); + + if (!match) { + return false; + } + + for (let i = 1; i <= 4; i++) { + const octet = parseInt(match[i], 10); + if (octet < 0 || octet > 255) { + return false; + } + } + + return true; + } + + private static validateIPv6(ip: string): boolean { + // tslint:disable-next-line:max-line-length + const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; + + return ipv6Regex.test(ip); + } + + private static validateCIDR(cidr: string): boolean { + const parts = cidr.split('/'); + + if (parts.length !== 2) { + return false; + } + + const [ip, prefix] = parts; + const prefixNum = parseInt(prefix, 10); + + const isIPv4 = ip.includes('.') && !ip.includes(':'); + const isIPv6 = ip.includes(':'); + + if (isIPv4) { + if (!IpFormsValidators.validateIPv4(ip)) { + return false; + } + + if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 32) { + return false; + } + } else if (isIPv6) { + + if (!IpFormsValidators.validateIPv6(ip)) { + return false; + } + + if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) { + return false; + } + } else { + return false; + } + + return true; + } + +} diff --git a/frontend/src/app/rule-management/rule-management.module.ts b/frontend/src/app/rule-management/rule-management.module.ts index 96e61b916..84b990677 100644 --- a/frontend/src/app/rule-management/rule-management.module.ts +++ b/frontend/src/app/rule-management/rule-management.module.ts @@ -35,9 +35,9 @@ import {ImportRuleComponent} from './app-rule/components/import-rules/import-rul import {ImportRuleService} from './app-rule/components/import-rules/import-rule.service'; import {AddReferenceComponent} from './app-rule/components/reference/add-reference.component'; import {RuleGenericFilterComponent} from './app-rule/components/rule-generic-filter/rule-generic-filter.component'; -import {RuleDetailComponent} from './app-rule/components/rule-list/components/rule-detail/rule-detail.component'; import {RuleFieldComponent} from './app-rule/components/rule-list/components/rule-field/rule-field.component'; import {RuleListComponent} from './app-rule/components/rule-list/rule-list.component'; +import {RuleViewComponent} from './app-rule/components/see-rule/rule-view.component' import {RuleManagementRoutingModule} from './rule-management.routing.module'; import { DataTypeService } from './services/data-type.service'; import {FilterService} from './services/filter.service'; @@ -45,16 +45,16 @@ import {RuleResolverService} from './services/rule.resolver.service'; import {RuleService} from './services/rule.service'; import {RulesResolverService} from './services/rules.resolver.service'; import {GenericFilterComponent} from './share/generic-filter/generic-filter.component'; -import {SeeRuleComponent} from './app-rule/components/see-rule/see-rule.component' +import {RuleDetailComponent} from "./app-rule/components/rule-list/components/rule-detail/rule-detail.component"; @NgModule({ declarations: [ AppRuleComponent, - SeeRuleComponent, + RuleViewComponent, RuleListComponent, RuleFieldComponent, - RuleDetailComponent, + RuleViewComponent, AddRuleComponent, ImportRuleComponent, SidebarComponent, @@ -72,7 +72,8 @@ import {SeeRuleComponent} from './app-rule/components/see-rule/see-rule.componen AddVariableComponent, AddAfterEventComponent, DeduplicateFieldsComponent, - ExpressionConsoleComponent + ExpressionConsoleComponent, + RuleDetailComponent ], imports: [ @@ -107,7 +108,7 @@ import {SeeRuleComponent} from './app-rule/components/see-rule/see-rule.componen AddPatternComponent, AddAssetComponent, ImportRuleComponent, - SeeRuleComponent, + RuleViewComponent, ] }) export class RuleManagementModule { } diff --git a/frontend/src/app/shared/components/app-filter/app-filter.component.scss b/frontend/src/app/shared/components/app-filter/app-filter.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/shared/components/app-filter/app-filter.component.ts b/frontend/src/app/shared/components/app-filter/app-filter.component.ts index 5bc1951d6..af7c27966 100644 --- a/frontend/src/app/shared/components/app-filter/app-filter.component.ts +++ b/frontend/src/app/shared/components/app-filter/app-filter.component.ts @@ -4,7 +4,7 @@ import {TemplateSelectorDirective} from '../../directives/template-selector/temp @Component({ selector: 'app-filter', templateUrl: './app-filter.component.html', - styleUrls: ['./app-filter.component.css'] + styleUrls: ['./app-filter.component.scss'] }) export class AppFilterComponent { @Input() list: T[] | null = null; diff --git a/frontend/src/app/shared/components/auth/login-providers/login-providers.component.html b/frontend/src/app/shared/components/auth/login-providers/login-providers.component.html new file mode 100644 index 000000000..5f0f6891c --- /dev/null +++ b/frontend/src/app/shared/components/auth/login-providers/login-providers.component.html @@ -0,0 +1,19 @@ + +
+
+ OR +
+
+ +
+ +
+
diff --git a/frontend/src/app/shared/components/auth/login-providers/login-providers.component.scss b/frontend/src/app/shared/components/auth/login-providers/login-providers.component.scss new file mode 100644 index 000000000..b07261fc7 --- /dev/null +++ b/frontend/src/app/shared/components/auth/login-providers/login-providers.component.scss @@ -0,0 +1,282 @@ +@import "../../../../../assets/styles/theme"; +@import "../../../../../assets/styles/var"; + +// Divider +.providers-divider { + display: flex; + align-items: center; + margin: 1rem; +} + +.divider-line { + flex: 1; + height: 1px; + background: linear-gradient(to right, transparent, #e0e0e0, transparent); +} + +.divider-text { + padding: 0 1rem; + color: #999; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +// Providers List +.providers-list { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0 1rem; + margin-bottom: 1rem; +} + +// Provider Button Base +.provider-button { + height: 48px; + border-radius: 8px; + border: 1px solid #e0e0e0; + background-color: #ffffff; + color: #333; + font-weight: 500; + font-size: 14px; + padding: 0 1rem; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(0, 0, 0, 0.05); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; + } + + &:hover:not(:disabled)::before { + width: 300px; + height: 300px; + } + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); + } + + &:active:not(:disabled) { + transform: translateY(0); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + } + + &:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.1); + } +} + +.provider-icon { + font-size: 18px; + margin-right: 0.75rem; + position: relative; + z-index: 1; + transition: color 0.3s ease; +} + +.provider-text { + position: relative; + z-index: 1; +} + +// Provider Specific Styles +.provider-google { + border-color: #db4437; + + .provider-icon { + color: #db4437; + } + + &:hover:not(:disabled) { + background-color: #db4437; + border-color: #db4437; + color: white; + + .provider-icon { + color: white; + } + } +} + +.provider-azure { + border-color: #00a4ef; + + .provider-icon { + color: #00a4ef; + } + + &:hover:not(:disabled) { + background-color: #00a4ef; + border-color: #00a4ef; + color: white; + + .provider-icon { + color: white; + } + } +} + +.provider-github { + border-color: #333; + + .provider-icon { + color: #333; + } + + &:hover:not(:disabled) { + background-color: #333; + border-color: #333; + color: white; + + .provider-icon { + color: white; + } + } +} + +.provider-facebook { + border-color: #1877f2; + + .provider-icon { + color: #1877f2; + } + + &:hover:not(:disabled) { + background-color: #1877f2; + border-color: #1877f2; + color: white; + + .provider-icon { + color: white; + } + } +} + +.provider-linkedin { + border-color: #0a66c2; + + .provider-icon { + color: #0a66c2; + } + + &:hover:not(:disabled) { + background-color: #0a66c2; + border-color: #0a66c2; + color: white; + + .provider-icon { + color: white; + } + } +} + +.provider-okta { + border-color: #007dc1; + + .provider-icon { + color: #007dc1; + } + + &:hover:not(:disabled) { + background-color: #007dc1; + border-color: #007dc1; + color: white; + + .provider-icon { + color: white; + } + } +} + +.provider-auth0 { + border-color: #eb5424; + + .provider-icon { + color: #eb5424; + } + + &:hover:not(:disabled) { + background-color: #eb5424; + border-color: #eb5424; + color: white; + + .provider-icon { + color: white; + } + } +} + +// Animaciรณn de entrada +@keyframes slideInProvider { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.provider-button { + animation: slideInProvider 0.3s ease forwards; + opacity: 0; + + @for $i from 1 through 7 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 0.05}s; + } + } +} + +// Responsive +@media (max-width: 576px) { + .provider-button { + height: 44px; + font-size: 13px; + } + + .provider-icon { + font-size: 16px; + margin-right: 0.5rem; + } + + .providers-list { + padding: 0 0.5rem; + gap: 0.5rem; + } + + .providers-divider { + margin: 1rem 0.5rem; + } +} + +// Smooth rendering +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/frontend/src/app/shared/components/auth/login-providers/login-providers.component.ts b/frontend/src/app/shared/components/auth/login-providers/login-providers.component.ts new file mode 100644 index 000000000..f0550b2cd --- /dev/null +++ b/frontend/src/app/shared/components/auth/login-providers/login-providers.component.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { + PROVIDER_ICONS, + ProviderType, + UtmIdentityProvider +} from '../../../../app-management/identity-provider/shared/models/utm-identity-provider.model'; +import { + LoginProviderService, +} from '../../../services/login-provider.service'; + +@Component({ + selector: 'app-login-providers', + templateUrl: './login-providers.component.html', + styleUrls: ['./login-providers.component.scss'] +}) +export class LoginProvidersComponent implements OnInit { + + request = { + 'active.equals': true, + page: 0, + size: 10 + }; + + providers: UtmIdentityProvider[] = [] as UtmIdentityProvider[]; + + constructor(private loginProviderService: LoginProviderService) { } + + ngOnInit() { + this.loadAllProviders(); + } + + loadAllProviders() { + this.loginProviderService.getProviders(this.request).subscribe( + response => { + this.providers = response.body || []; + }, + error => { + console.error('Error fetching login providers:', error); + this.providers = []; + } + ); + } + + getProviderIcon(providerType: ProviderType) { + return PROVIDER_ICONS[providerType] || 'bi-shield-lock'; + } + + loginWithProvider(provider: UtmIdentityProvider) { + if (!provider.active) { + console.warn(`Provider ${provider.name} is inactive.`); + return; + } + + this.loginProviderService.loginWithProvider(provider.providerType.toLowerCase()); + } +} diff --git a/frontend/src/app/shared/components/auth/login/login.component.html b/frontend/src/app/shared/components/auth/login/login.component.html index 2c9cdf0e7..c60066e2f 100644 --- a/frontend/src/app/shared/components/auth/login/login.component.html +++ b/frontend/src/app/shared/components/auth/login/login.component.html @@ -60,6 +60,7 @@
+
diff --git a/frontend/src/app/shared/components/auth/login/login.component.scss b/frontend/src/app/shared/components/auth/login/login.component.scss index ac0bb667e..707288609 100644 --- a/frontend/src/app/shared/components/auth/login/login.component.scss +++ b/frontend/src/app/shared/components/auth/login/login.component.scss @@ -21,7 +21,6 @@ flex-wrap: wrap; justify-content: center; align-items: center; - //padding: 100px 39%; } .wrap-login { @@ -29,7 +28,6 @@ background: #fff; border-radius: 10px; overflow: hidden; - display: -webkit-box; display: -webkit-flex; display: -moz-box; @@ -39,7 +37,6 @@ justify-content: space-around; align-items: center; padding: 20px 0; - } /*------------------------------------------------------------------ @@ -47,11 +44,17 @@ .login-pic { width: 100px; text-align: center; + padding: 20px 0; } .login-pic img { max-width: 100%; max-height: 100px; + transition: transform 0.3s ease; +} + +.login-pic img:hover { + transform: scale(1.05); } /*------------------------------------------------------------------ @@ -66,7 +69,6 @@ color: #333333; line-height: 1.2; text-align: center; - width: 100%; display: block; padding-bottom: 54px; @@ -103,7 +105,6 @@ justify-content: center; align-items: center; padding: 0 25px; - -webkit-transition: all 0.4s; -o-transition: all 0.4s; -moz-transition: all 0.4s; @@ -118,10 +119,6 @@ [ Responsive ]*/ @media (max-width: 992px) { - .wrap-login { - // padding: 177px 90px 33px 85px; - } - .login-pic { width: 35%; } @@ -132,10 +129,6 @@ } @media (max-width: 768px) { - .wrap-login { - // padding: 100px 80px 33px 80px; - } - .login-pic { display: none; } @@ -146,8 +139,9 @@ } @media (max-width: 576px) { - .wrap-login { - //padding: 100px 15px 33px 15px; + .w-350px { + width: 90% !important; + max-width: 350px; } } @@ -174,16 +168,13 @@ transform: translateY(-50%); right: 8px; pointer-events: none; - font-family: Poppins-Medium; color: #c80000; font-size: 13px; line-height: 1.4; text-align: left; - visibility: hidden; opacity: 0; - -webkit-transition: opacity 0.4s; -o-transition: opacity 0.4s; -moz-transition: opacity 0.4s; @@ -218,80 +209,231 @@ } } +// ============================================ +// MEJORAS VISUALES MODERNAS +// ============================================ + .utm-login-cover { position: fixed; top: 0; left: 0; bottom: 0; right: 0; + z-index: 1; + padding: 20px; } .w-350px { width: 350px !important; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); + transition: box-shadow 0.3s ease; + + &:hover { + box-shadow: 0 15px 50px rgba(0, 0, 0, 0.15); + } } .card { box-shadow: unset; } -.utm-login-cover { - position: fixed; - top: 0; bottom: 0; left: 0; right: 0; - z-index: 1; - padding: 20px; -} - -.login-pic img { - max-height: 100px; - max-width: 100%; -} - +// Form Controls mejorados .form-control { - border-radius: 6px; - border-color: #ccc; + border-radius: 8px; + border: 1px solid #ddd; box-shadow: none; + padding: 12px 40px 12px 33px; + height: 48px; + font-size: 14px; + transition: all 0.3s ease; + + &::placeholder { + color: #999; + opacity: 1; + } + + &:focus { + border-color: $primary-color; + box-shadow: 0 0 0 3px rgba($primary-color, 0.1); + outline: none; + } + + &:hover:not(:focus) { + border-color: #bbb; + } } -.form-control:focus { - border-color: #007bff; - box-shadow: 0 0 0 2px rgba(0,123,255,0.1); +// Form group feedback position +.form-group-feedback-right .form-control-feedback { + display: flex; + align-items: center; + justify-content: center; + height: 48px; + + i { + font-size: 16px; + } } +// Botones mejorados .utm-button { - border-radius: 6px; - padding: 10px 16px; + border-radius: 8px; + padding: 12px 24px; font-weight: 600; font-size: 15px; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + height: 48px; + display: inline-flex; + align-items: center; + justify-content: center; + + &::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; + } + + &:hover::before { + width: 300px; + height: 300px; + } } .utm-button-primary { -/* background-color: #007bff; - border-color: #007bff;*/ color: white; - transition: background-color 0.3s ease; + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba($primary-color, 0.4); + } + + &:active:not(:disabled) { + transform: translateY(0); + box-shadow: 0 4px 10px rgba($primary-color, 0.3); + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; + } + + .spinner { + animation: spin 1s linear infinite; + margin-right: 8px; + } } -.utm-button-primary:hover:not(:disabled) { -/* background-color: #0056b3; - border-color: #004085;*/ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } } +// Links y textos .cursor-pointer { cursor: pointer; + padding: 8px; + border-radius: 6px; + transition: all 0.2s ease; + display: inline-block; + + &:hover { + background-color: rgba(0, 0, 0, 0.03); + } } -.txt1, .txt2 { +.txt1 { font-size: 13px; - /* color: #007bff;*/ + color: #666; + margin-right: 4px; } -.txt2:hover { - text-decoration: underline; +.txt2 { + font-size: 13px; + color: $primary-color; + transition: all 0.2s ease; + font-weight: 500; + + &:hover { + color: darken($primary-color, 10%); + text-decoration: underline; + } } .border-grey { - border: 1px solid #ccc!important; + border: 1px solid #e0e0e0 !important; } +// Mensaje de demo +.text-danger.font-weight-semibold { + background: linear-gradient(135deg, #fff5f5 0%, #ffe8e8 100%); + border-top: 1px solid #ffe0e0; + border-radius: 0 0 1rem 1rem; + animation: fadeIn 0.5s ease; +} +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +// Mejora de espaciado +.my-2 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.mb-4 { + margin-bottom: 1.5rem !important; +} + +.mt-5 { + margin-top: 2rem !important; +} + +// Card body +.card-body { + padding: 1.5rem; +} + +// Smooth rendering +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +// Loading states +.spinner { + display: inline-block; + width: 16px; + height: 16px; +} + +// Focus visible para accesibilidad +button:focus, +.form-control:focus, +a:focus { + outline: none; +} + +button:focus-visible, +.form-control:focus-visible, +a:focus-visible { + outline: 2px solid $primary-color; + outline-offset: 2px; +} diff --git a/frontend/src/app/shared/components/auth/login/login.component.ts b/frontend/src/app/shared/components/auth/login/login.component.ts index 05409a56e..1979deecf 100644 --- a/frontend/src/app/shared/components/auth/login/login.component.ts +++ b/frontend/src/app/shared/components/auth/login/login.component.ts @@ -1,10 +1,11 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; import {FormBuilder, FormGroup, Validators} from '@angular/forms'; import {DomSanitizer} from '@angular/platform-browser'; import {ActivatedRoute, Router} from '@angular/router'; import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import {NgxSpinnerService} from 'ngx-spinner'; -import {Observable} from 'rxjs'; +import {Observable, Subject} from 'rxjs'; +import {filter, switchMap, takeUntil} from 'rxjs/operators'; import {AccountService} from '../../../../core/auth/account.service'; import {ApiServiceCheckerService} from '../../../../core/auth/api-checker-service'; import {StateStorageService} from '../../../../core/auth/state-storage.service'; @@ -15,14 +16,13 @@ import {ThemeChangeBehavior} from '../../../behaviors/theme-change.behavior'; import {ADMIN_DEFAULT_EMAIL, ADMIN_ROLE, DEMO_URL, USER_ROLE} from '../../../constants/global.constant'; import {extractQueryParamsForNavigation, stringParamToQueryParams} from '../../../util/query-params-to-filter.util'; import {PasswordResetInitComponent} from '../password-reset/init/password-reset-init.component'; -import {AuthServerProvider} from "../../../../core/auth/auth-jwt.service"; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.scss'] }) -export class LoginComponent implements OnInit { +export class LoginComponent implements OnInit, OnDestroy { authenticationError: boolean; password: string; rememberMe: boolean; @@ -37,6 +37,7 @@ export class LoginComponent implements OnInit { loginImage$: Observable; loadingLogin = false; isInternalNavigation = false; + destroy$ = new Subject(); constructor( private loginService: LoginService, @@ -50,6 +51,7 @@ export class LoginComponent implements OnInit { private modalService: NgbModal, private themeChangeBehavior: ThemeChangeBehavior, private spinner: NgxSpinnerService, + private stateStorageService: StateStorageService, private apiServiceCheckerService: ApiServiceCheckerService ) { this.credentials = {}; @@ -59,27 +61,37 @@ export class LoginComponent implements OnInit { ngOnInit() { this.initForm(); - this.apiServiceCheckerService.isOnlineApi$.subscribe(result => { - if (result) { - this.activatedRoute.queryParams.subscribe(params => { - if (params.token) { - this.loadingLogin = false; - this.loginService.loginWithToken(params.token, true).then(() => { + this.apiServiceCheckerService.isOnlineApi$ + .pipe( + filter(result => !!result ), + switchMap(() => this.activatedRoute.queryParams), + takeUntil(this.destroy$) + ) + .subscribe(params => { + if (params) { + this.activatedRoute.queryParams.subscribe(params => { + if (params.token) { + this.loadingLogin = false; + this.loginService.loginWithToken(params.token, true).then(() => { + this.loadingLogin = false; + this.isInternalNavigation = true; + this.startInternalNavigation(params); + }); + } else if (params.key) { this.loadingLogin = false; this.isInternalNavigation = true; - this.startInternalNavigation(params); - }); - } else if (params.key) { - this.loadingLogin = false; - this.isInternalNavigation = true; - this.loginService.loginWithKey(params.key, true).then(() => { - this.startInternalNavigation(params); - }); - } else { - this.loadingAuth = false; - } - }); - } + this.loginService.loginWithKey(params.key, true).then(() => { + this.startInternalNavigation(params); + }); + } else if (params.error) { + this.utmToast.showError('Login fail', 'Authentication error, ' + + 'check your data and try again.'); + this.loadingAuth = false; + } else { + this.loadingAuth = false; + } + }); + } }); } @@ -87,7 +99,7 @@ export class LoginComponent implements OnInit { this.accountService.identity(true).then(value => { setTimeout(() => { if (value) { - //this.spinner.show('loadingSpinner'); + // this.spinner.show('loadingSpinner'); if (url) { const urlRoute = url.split('<-PARAMS->'); const route = urlRoute[0]; @@ -97,7 +109,7 @@ export class LoginComponent implements OnInit { this.router.navigate([route], {queryParams}).then(() => { this.menuBehavior.$menu.next(false); - //this.spinner.hide('loadingSpinner'); + // this.spinner.hide('loadingSpinner'); }); }); } else { @@ -106,7 +118,7 @@ export class LoginComponent implements OnInit { } } } else { - //this.spinner.hide('loadingSpinner'); + // this.spinner.hide('loadingSpinner'); this.loadingAuth = false; } }, 1000); @@ -130,18 +142,19 @@ export class LoginComponent implements OnInit { this.loginService .login(this.formLogin.value) .then((data) => { - if (data.auth) { + if (!data.forceTfa) { this.authenticationError = false; this.logged = true; this.startLogin = false; this.spinner.show(); - } else if (data.tfaRequired && !!data.method ) { + this.startNavigation(); + } else if (data.tfaConfigured && !!data.method ) { this.spinner.show(); this.router.navigate(['/totp']) .then(() => this.spinner.hide()); } else { this.spinner.show(); - this.router.navigate(['/tfa-setup']) + this.router.navigate(['/enroll-tfa']) .then(() => this.spinner.hide()); } }) @@ -149,7 +162,7 @@ export class LoginComponent implements OnInit { this.startLogin = false; this.authenticationError = true; const utmStackError = err.headers.get('X-UtmStack-error'); - if (utmStackError.includes('UserJWTController.authorize: blocked')) { + if (utmStackError && utmStackError.includes('UserJWTController.authorize: blocked')) { this.utmToast.showError('Login blocked', 'Your ip was blocked due multiple login failures, please try again in 10 minutes'); } else { this.utmToast.showError('Login fail', 'Authentication error, ' + @@ -168,6 +181,25 @@ export class LoginComponent implements OnInit { } } + startNavigation() { + this.accountService.identity(true).then(account => { + if (account) { + const { path, queryParams } = + extractQueryParamsForNavigation(this.stateStorageService.getUrl() ? this.stateStorageService.getUrl() : '' ); + if (path) { + this.stateStorageService.resetPreviousUrl(); + } + const redirectTo = (account.authorities.includes(ADMIN_ROLE) && account.email === ADMIN_DEFAULT_EMAIL) + ? '/getting-started' : !!path ? path : '/dashboard/overview'; + this.router.navigate([redirectTo], {queryParams}) + .then(() => this.spinner.hide()); + } else { + this.logged = false; + this.utmToast.showError('Login error', 'User without privileges.'); + } + }); + } + startInternalNavigation(params) { if (params.url) { this.checkLogin(params.url); @@ -178,4 +210,8 @@ export class LoginComponent implements OnInit { } } + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/frontend/src/app/shared/components/auth/tfa-setup/tfa-setup.component.ts b/frontend/src/app/shared/components/auth/tfa-setup/tfa-setup.component.ts index b820c7873..8f65078a3 100644 --- a/frontend/src/app/shared/components/auth/tfa-setup/tfa-setup.component.ts +++ b/frontend/src/app/shared/components/auth/tfa-setup/tfa-setup.component.ts @@ -4,8 +4,9 @@ import {Router} from '@angular/router'; import { Observable, Subscription } from 'rxjs'; import {UtmToastService} from '../../../alert/utm-toast.service'; import {ThemeChangeBehavior} from '../../../behaviors/theme-change.behavior'; -import {TfaMethod, TfaService} from '../../../services/tfa/tfa.service'; +import {TfaMethod, TfaService, TfaStage} from '../../../services/tfa/tfa.service'; import {AuthServerProvider} from "../../../../core/auth/auth-jwt.service"; +import {AccountService} from "../../../../core/auth/account.service"; @Component({ selector: 'app-tfa-setup', @@ -38,6 +39,7 @@ export class TfaSetupComponent implements OnInit, OnDestroy { private router: Router, private tfaService: TfaService, private utmToastService: UtmToastService, + private accountService: AccountService, private authServerProvider: AuthServerProvider ) {} @@ -45,13 +47,6 @@ export class TfaSetupComponent implements OnInit, OnDestroy { this.loginImage$ = this.themeChangeBehavior.$themeIcon.asObservable(); } - ngOnDestroy(): void { - if (this.timerSubscription) { - this.timerSubscription.unsubscribe(); - } - } - - selectMethod(method: TfaMethod): void { this.selectedMethod = method; this.step = 'setup'; @@ -60,8 +55,9 @@ export class TfaSetupComponent implements OnInit, OnDestroy { } private initTfa(): void { - this.tfaService.initTfa({ - method: this.selectedMethod + this.tfaService.enrollTfa({ + method: this.selectedMethod, + stage: TfaStage.INIT }).subscribe((response) => { if (this.selectedMethod === TfaMethod.TOTP) { this.qrCodeUrl = response.delivery.target ? @@ -108,8 +104,9 @@ export class TfaSetupComponent implements OnInit, OnDestroy { onSubmit() { this.verifying = true; - this.tfaService.verifyTfa({ + this.tfaService.enrollTfa({ method: this.selectedMethod, + stage: TfaStage.VERIFY, code: this.code }).subscribe(response => { this.verifying = false; @@ -143,12 +140,16 @@ export class TfaSetupComponent implements OnInit, OnDestroy { completeSetup(): void { this.verifying = true; - this.tfaService.completeTfa({ + this.tfaService.enrollTfa({ + stage: TfaStage.COMPLETE, method: this.selectedMethod, enable: true }).subscribe(response => { - this.authServerProvider.tfaMethod = this.selectedMethod; - this.router.navigate(['/totp']); + + /*this.authServerProvider.tfaMethod = this.selectedMethod; + this.router.navigate(['/totp']);*/ + this.authServerProvider.storeAuthenticationToken(response.token); + this.accountService.startNavigation(); }, error => { this.verifying = false; this.verifying = false; @@ -162,4 +163,10 @@ export class TfaSetupComponent implements OnInit, OnDestroy { skipSetup(): void { this.router.navigate(['/']); } + + ngOnDestroy(): void { + if (this.timerSubscription) { + this.timerSubscription.unsubscribe(); + } + } } diff --git a/frontend/src/app/shared/components/auth/totp/totp.component.html b/frontend/src/app/shared/components/auth/totp/totp.component.html index b112d2381..2af92ea73 100644 --- a/frontend/src/app/shared/components/auth/totp/totp.component.html +++ b/frontend/src/app/shared/components/auth/totp/totp.component.html @@ -92,8 +92,8 @@

Two-Factor Authentication

- + --> + +
-
+
diff --git a/frontend/src/app/shared/components/utm/table/utm-table/dynamic-table/dynamic-table.component.ts b/frontend/src/app/shared/components/utm/table/utm-table/dynamic-table/dynamic-table.component.ts index 0c9d6c1bc..1037bc5a8 100644 --- a/frontend/src/app/shared/components/utm/table/utm-table/dynamic-table/dynamic-table.component.ts +++ b/frontend/src/app/shared/components/utm/table/utm-table/dynamic-table/dynamic-table.component.ts @@ -9,6 +9,7 @@ import { ViewChild, ViewChildren, ViewContainerRef } from '@angular/core'; +import {container} from '@angular/core/src/render3'; import {Subject} from 'rxjs'; import {IndexPatternBehavior} from '../../../../../../log-analyzer/shared/behaviors/index-pattern.behavior'; import {SortEvent} from '../../../../../directives/sortable/type/sort-event'; @@ -20,7 +21,6 @@ import { extractValueFromObjectByPath } from '../../../../../util/get-value-object-from-property-path.util'; import {SUMMARY_COLUMNS} from './summary-fields'; -import {container} from "@angular/core/src/render3"; @Component({ selector: 'app-dynamic-table', @@ -86,11 +86,14 @@ export class UtmDynamicTableComponent implements OnInit, OnChanges, OnDestroy { /** * Determine if show edit columns property */ + @Input() pageable = true; + + @Input() initialExpandedId?: number; + @Input() editableColumn = true; @ViewChild('container', {read: ViewContainerRef}) entry: ViewContainerRef; @ViewChild('container') containerRef!: ElementRef; viewDetail = -1; - @Input() pageable = true; dataTypeEnum = ElasticDataTypesEnum; utmDateFormat = UtmDateFormatEnum; destroy$: Subject = new Subject(); @@ -100,6 +103,9 @@ export class UtmDynamicTableComponent implements OnInit, OnChanges, OnDestroy { @ViewChildren('summaryWrapper') summaryWrappers: QueryList; ngOnInit(): void { + if (this.initialExpandedId !== undefined) { + this.viewDetail = this.initialExpandedId; + } } onSort($event: SortEvent) { diff --git a/frontend/src/app/shared/components/utm/util/utm-agent-connect/utm-agent-connect.component.html b/frontend/src/app/shared/components/utm/util/utm-agent-connect/utm-agent-connect.component.html index ffb9ee1c2..993c560d7 100644 --- a/frontend/src/app/shared/components/utm/util/utm-agent-connect/utm-agent-connect.component.html +++ b/frontend/src/app/shared/components/utm/util/utm-agent-connect/utm-agent-connect.component.html @@ -34,8 +34,11 @@ Info! The agent is offline. Data shown is from the last successful sync. - + [hostname]="agent.hostname"> + diff --git a/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.html b/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.html index 55e4348ad..1929e00fd 100644 --- a/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.html +++ b/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.html @@ -1,152 +1,93 @@ - -
-
-
-
-
- - - {{ agent.hostname }} ({{ agent.os }}) - +
+
- -
- - -
-
- - {{ 'Password: ' }} - - -
-
-
- - -
- - Press TAB to use automation variables in the command - - - - +
+
+ +
+ + + {{ agent.hostname }} ({{ agent.os }}) + -
- -
-
- - {{ consoleSignal }} - - -
-
- - -
- -
Agent is disconnected or missing
-
-
- +
-
-
-
- - -
-
-
-
- - - {{ agent.hostname }} ({{ agent.os }}) - + + +
+
+ {{ 'Password: ' }} + +
+
- -
-
- - {{ 'Password: ' }} - - -
-
-
- - -
+ + + +
Press TAB to use automation variables in the command - - - -
- -
-
- - {{ consoleSignal }} - - -
+ + + + + +
+ +
- - -
- -
Agent is disconnected or missing
+ +
+ + {{ consoleSignal }} + +
- +
-
+ +
+ +
Agent is disconnected or missing
+
+
+
- - +
diff --git a/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.scss b/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.scss index 54e03337e..b4bd6ae6e 100644 --- a/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.scss +++ b/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.scss @@ -40,16 +40,17 @@ } .output { + flex-grow: 1; + overflow-y: auto; + max-height: 100%; } .terminal { - max-height: 400px; - height: 400px; - overflow-y: auto; padding: 10px; font-family: monospace; font-size: 16px; color: #22da26; + overflow: hidden; .console-info { font-size: 16px; @@ -67,7 +68,13 @@ } .command-header { - display: block; - height: 28px; background: #C6C6C6; } + +.h-450px { + height: 450px; +} + +.top-zero { + top: 0 !important; +} diff --git a/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.ts b/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.ts index 337f6b54a..9a1096e0b 100644 --- a/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.ts +++ b/frontend/src/app/shared/components/utm/util/utm-agent-console/utm-agent-console.component.ts @@ -1,4 +1,6 @@ import { + AfterViewInit, + ChangeDetectorRef, Component, ElementRef, EventEmitter, @@ -28,13 +30,14 @@ import {replaceBreakLine} from '../../../../util/string-util'; templateUrl: './utm-agent-console.component.html', styleUrls: ['./utm-agent-console.component.scss'] }) -export class UtmAgentConsoleComponent implements OnInit, OnChanges, OnDestroy { +export class UtmAgentConsoleComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy { @Input() hostname: string; @Input() websocketCommand: IncidentCommandType; @Input() template: 'on-demand' | 'default' = 'default'; @Output() close = new EventEmitter(); @ViewChild('contentWrapper', {read: ElementRef}) contentWrapper!: ElementRef; - @ViewChild('commandInput') commandInput!: ElementRef; + @ViewChild('commandInput') commandInput: ElementRef; + @ViewChild('passwordInput') passwordInput: ElementRef; account: Account; commandInProgress = false; private token: string; @@ -54,18 +57,24 @@ export class UtmAgentConsoleComponent implements OnInit, OnChanges, OnDestroy { private agentManagerService: UtmAgentManagerService, private accountService: AccountService, private toast: UtmToastService, - private sessionStorage: SessionStorageService) { + private sessionStorage: SessionStorageService, + private cdr: ChangeDetectorRef) { } scrollToBottom() { - if (this.contentWrapper) { - const element = this.contentWrapper.nativeElement as HTMLElement; - element.scrollTop = element.scrollHeight; - } + console.log('scrollToBottom'); + setTimeout(() => { + if (this.contentWrapper && this.contentWrapper.nativeElement) { + const element = this.contentWrapper.nativeElement as HTMLElement; + element.scrollTop = element.scrollHeight; + console.log('Scroll height:', element.scrollHeight, 'Scroll top:', element.scrollTop); + } else { + console.warn('contentWrapper no disponible'); + } + }, 0); } - ngOnInit(): void { - } + ngOnInit(): void {} ngOnChanges(changes: SimpleChanges) { this.agent = null; @@ -77,6 +86,10 @@ export class UtmAgentConsoleComponent implements OnInit, OnChanges, OnDestroy { this.pass = ''; } + ngAfterViewInit() { + this.passwordInput.nativeElement.focus(); + } + startConnection() { this.accountService.identity().then(account => { this.account = account; @@ -118,15 +131,11 @@ export class UtmAgentConsoleComponent implements OnInit, OnChanges, OnDestroy { } focusCommandInput() { - if (this.commandInput) { - this.commandInput.nativeElement.focus(); - } - } - - ngOnDestroy(): void { - if (this.authorize && this.stompClient !== null) { - this.stompClient.disconnect(); - } + setTimeout(() => { + if (this.commandInput) { + this.commandInput.nativeElement.focus(); + } + }, 150); } initializeWebSocketConnection() { @@ -138,6 +147,8 @@ export class UtmAgentConsoleComponent implements OnInit, OnChanges, OnDestroy { this.stompClient.crossDomain = true; this.stompClient.connect({}, (frame) => { this.openSocket(); + this.authorize = true; + this.cdr.detectChanges(); this.focusCommandInput(); }, this.errorCallBack); } else { @@ -145,8 +156,9 @@ export class UtmAgentConsoleComponent implements OnInit, OnChanges, OnDestroy { } } - errorCallBack(error) { + errorCallBack = (error) => { console.log('errorCallBack -> ' + error); + this.connectionError = true; setTimeout(() => { this.initializeWebSocketConnection(); }, 5000); @@ -169,6 +181,7 @@ export class UtmAgentConsoleComponent implements OnInit, OnChanges, OnDestroy { setTimeout(() => { this.scrollToBottom(); this.focusCommandInput(); + this.cdr.detectChanges(); }, 150); }); } @@ -215,12 +228,12 @@ export class UtmAgentConsoleComponent implements OnInit, OnChanges, OnDestroy { } checkPassword() { + this.passwordInput.nativeElement.blur(); const uuid = UUID.UUID(); this.accountService.checkPassword(this.pass, uuid).subscribe(response => { if (response.body !== uuid) { this.toast.showError('Invalid check UUID', 'UUID to check your password does not match'); } else { - this.authorize = true; this.startConnection(); } }, () => { @@ -231,4 +244,10 @@ export class UtmAgentConsoleComponent implements OnInit, OnChanges, OnDestroy { insertVariablePlaceholder($event: string) { this.command += `$[${$event}]`; } + + ngOnDestroy(): void { + if (this.authorize && this.stompClient !== null) { + this.stompClient.disconnect(); + } + } } diff --git a/frontend/src/app/shared/components/utm/util/utm-agent-detail/utm-agent-detail.component.ts b/frontend/src/app/shared/components/utm/util/utm-agent-detail/utm-agent-detail.component.ts index 975e49b50..7e229177b 100644 --- a/frontend/src/app/shared/components/utm/util/utm-agent-detail/utm-agent-detail.component.ts +++ b/frontend/src/app/shared/components/utm/util/utm-agent-detail/utm-agent-detail.component.ts @@ -25,14 +25,13 @@ export class UtmAgentDetailComponent implements OnInit { } ngOnInit() { - console.log(this.agent); this.agentIp = this.agent.ip; this.ips = this.agent.addresses !== '' ? this.agent.addresses.split(',') : []; this.macs = this.agent.mac !== '' ? this.agent.mac.split(',') : []; } public getAgentIcon(): string { - if (this.agent) { + if (this.agent && this.agent.os) { if (this.agent.os.toLowerCase().includes('windows')) { return 'icon-windows8'; } else if (this.agent.os.toLowerCase().includes('linux')) { diff --git a/frontend/src/app/shared/components/utm/util/utm-code-view/utm-code-view.component.ts b/frontend/src/app/shared/components/utm/util/utm-code-view/utm-code-view.component.ts index fc92038a6..078c481ab 100644 --- a/frontend/src/app/shared/components/utm/util/utm-code-view/utm-code-view.component.ts +++ b/frontend/src/app/shared/components/utm/util/utm-code-view/utm-code-view.component.ts @@ -54,7 +54,6 @@ export class UtmCodeViewComponent implements OnInit, OnChanges { } removeSecretsTags(str) { - console.log(str); const regex = /<\/?secret>/g; return str.replace(regex, ''); } diff --git a/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.html b/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.html index 77cbb6f12..3ea1b63a2 100644 --- a/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.html +++ b/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.html @@ -1,3 +1,4 @@ + + + + + diff --git a/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.scss b/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.scss index ad5248ccd..1de86af68 100644 --- a/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.scss +++ b/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.scss @@ -2,15 +2,36 @@ @import "../../../../../../assets/styles/var"; @import "../../../../../../assets/styles/custom-elements"; -.utm-modal-header { - h6 { - font-size: 18px; +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.25rem; + border-bottom: 1px solid #e9ecef; + + h3 { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: #333; } - border-radius: 0; - background-color: $utm-header-color; -} + .btn-close { + width: 2rem; + height: 2rem; + border-radius: 0.25rem; + border: none; + background: transparent; + color: #999; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; -.utm-modal-delete { - background-color: $danger-color; + &:hover { + background: #f8f9fa; + color: #333; + } + } } diff --git a/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.ts b/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.ts index 6cae18ac1..0b7cdf550 100644 --- a/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.ts +++ b/frontend/src/app/shared/components/utm/util/utm-modal-header/utm-modal-header.component.ts @@ -8,9 +8,11 @@ import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap'; }) export class UtmModalHeaderComponent implements OnInit, AfterViewChecked, AfterViewInit { @Input() name: string; - @Input() type: 'delete' | 'normal'; - @Output() closeModal = new EventEmitter(); + @Input() type: 'default' | 'delete' | 'warning' | 'success' = 'default'; + @Input() icon?: string; @Input() showCloseButton = true; + @Output() closeModal = new EventEmitter(); + constructor(public activeModal: NgbActiveModal, private cdr: ChangeDetectorRef) { diff --git a/frontend/src/app/shared/constants/alert/alert-field.constant.ts b/frontend/src/app/shared/constants/alert/alert-field.constant.ts index 23b6011d3..ebeba9da2 100644 --- a/frontend/src/app/shared/constants/alert/alert-field.constant.ts +++ b/frontend/src/app/shared/constants/alert/alert-field.constant.ts @@ -14,6 +14,8 @@ export const ALERT_SEVERITY_FIELD = 'severity'; export const ALERT_IMPACT_FIELD = 'impact'; export const ALERT_SEVERITY_FIELD_LABEL = 'severityLabel'; export const ALERT_TAGS_FIELD = 'tags'; +export const ALERT_ASSETS_GROUP_NAME_FIELD = 'assetGroupName'; +export const ALERT_ASSETS_GROUP_ID_FIELD = 'assetGroupId'; export const ALERT_NOTE_FIELD = 'notes'; export const ALERT_OBSERVATION_FIELD = 'statusObservation'; export const ALERT_TIMESTAMP_FIELD = '@timestamp'; @@ -32,6 +34,7 @@ export const ALERT_TARGET_FIELD = 'target'; export const ALERT_ADVERSARY_FIELD = 'adversary'; export const ALERT_TECHNIQUE_FIELD = 'technique'; export const ALERT_PARENT_ID = 'parentId'; +export const ALERT_ECHOES_FIELD = 'echoes'; // SOURCE export const ALERT_SOURCE_HOSTNAME_FIELD = 'source.host'; @@ -147,6 +150,12 @@ export const ALERT_FIELDS: UtmFieldType[] = [ type: ElasticDataTypesEnum.STRING, visible: true, }, + { + label: 'Echoes', + field: ALERT_ECHOES_FIELD, + type: ElasticDataTypesEnum.NUMBER, + visible: true, + }, { label: 'Target', field: ALERT_TARGET_FIELD, @@ -211,13 +220,13 @@ export const ALERT_FIELDS: UtmFieldType[] = [ label: 'Target Host', field: ALERT_TARGET_HOST_FIELD, type: ElasticDataTypesEnum.STRING, - visible: false, + visible: true, }, { label: 'Target User', field: ALERT_TARGET_USER_FIELD, type: ElasticDataTypesEnum.STRING, - visible: false, + visible: true, } ] }, @@ -285,13 +294,13 @@ export const ALERT_FIELDS: UtmFieldType[] = [ label: 'Adversary Host', field: ALERT_ADVERSARY_HOST_FIELD, type: ElasticDataTypesEnum.STRING, - visible: false, + visible: true, }, { label: 'Adversary User', - field: ALERT_ADVERSARY_URL_FIELD, + field: ALERT_ADVERSARY_USER_FIELD, type: ElasticDataTypesEnum.STRING, - visible: false, + visible: true, } ] }, @@ -389,6 +398,39 @@ export const ALERT_FIELDS: UtmFieldType[] = [ }, ]; +export const ALERT_ECHOES_FIELDS: UtmFieldType[] = [ + { + label: 'Alert name', + field: ALERT_NAME_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Severity', + field: ALERT_SEVERITY_FIELD_LABEL, + type: ElasticDataTypesEnum.NUMBER, + visible: true, + }, + { + label: 'Status', + field: ALERT_STATUS_FIELD, + type: ElasticDataTypesEnum.NUMBER, + visible: true, + }, + { + label: 'Time', + field: ALERT_TIMESTAMP_FIELD, + type: ElasticDataTypesEnum.DATE, + visible: true, + }, + { + label: 'Sensor', + field: ALERT_SENSOR_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + } +]; + export const ALERT_FILTERS_FIELDS: UtmFieldType[] = [ { @@ -398,6 +440,12 @@ export const ALERT_FILTERS_FIELDS: UtmFieldType[] = [ customStyle: 'text-blue-800', visible: true, }, + { + label: 'Datasource Group', + field: ALERT_ASSETS_GROUP_NAME_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, { label: 'ID', field: ALERT_CASE_ID_FIELD, @@ -440,6 +488,36 @@ export const ALERT_FILTERS_FIELDS: UtmFieldType[] = [ type: ElasticDataTypesEnum.STRING, visible: false, }, + { + label: 'Category', + field: ALERT_CATEGORY_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Sensor', + field: ALERT_SENSOR_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Time', + field: ALERT_TIMESTAMP_FIELD, + type: ElasticDataTypesEnum.DATE, + visible: false, + }, + { + label: 'Incident Name', + field: ALERT_INCIDENT_NAME_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Tags', + field: ALERT_TAGS_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, { label: 'Adversary IP', field: ALERT_ADVERSARY_IP_FIELD, @@ -535,37 +613,7 @@ export const ALERT_FILTERS_FIELDS: UtmFieldType[] = [ field: ALERT_TARGET_GEOLOCATION_LONGITUDE_FIELD, type: ElasticDataTypesEnum.STRING, visible: false, - }, - { - label: 'Category', - field: ALERT_CATEGORY_FIELD, - type: ElasticDataTypesEnum.STRING, - visible: true, - }, - { - label: 'Sensor', - field: ALERT_SENSOR_FIELD, - type: ElasticDataTypesEnum.STRING, - visible: true, - }, - { - label: 'Time', - field: ALERT_TIMESTAMP_FIELD, - type: ElasticDataTypesEnum.DATE, - visible: false, - }, - { - label: 'Incident Name', - field: ALERT_INCIDENT_NAME_FIELD, - type: ElasticDataTypesEnum.STRING, - visible: true, - }, - { - label: 'Tags', - field: ALERT_TAGS_FIELD, - type: ElasticDataTypesEnum.STRING, - visible: true, - }, + } ]; export const EVENT_FIELDS: UtmFieldType[] = [ @@ -1266,3 +1314,270 @@ export const INCIDENT_AUTOMATION_ALERT_FIELDS: UtmFieldType[] = [ }, ]; + +export const ALERTS_CHILDREN_FIELDS: UtmFieldType[] = [ + { + label: 'Time', + field: ALERT_TIMESTAMP_FIELD, + type: ElasticDataTypesEnum.DATE, + visible: true, + }, + { + label: 'Alert name', + field: ALERT_NAME_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Severity', + field: ALERT_SEVERITY_FIELD_LABEL, + type: ElasticDataTypesEnum.NUMBER, + visible: true, + }, + { + label: 'Sensor', + field: ALERT_SENSOR_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Target', + field: ALERT_TARGET_FIELD, + type: ElasticDataTypesEnum.OBJECT, + visible: true, + fields: [ + { + label: 'Target IP', + field: ALERT_TARGET_IP_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Target URL', + field: ALERT_TARGET_URL_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Target Bytes Sent', + field: ALERT_TARGET_BYTES_SENT_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Target Domain', + field: ALERT_TARGET_DOMAIN_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Target ASN', + field: ALERT_TARGET_GEOLOCATION_ASN_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Target ASO', + field: ALERT_TARGET_GEOLOCATION_ASO_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Target Latitude', + field: ALERT_TARGET_GEOLOCATION_LATITUDE_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Target Longitude', + field: ALERT_TARGET_GEOLOCATION_LONGITUDE_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Target File', + field: ALERT_TARGET_FILE_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Target Host', + field: ALERT_TARGET_HOST_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Target User', + field: ALERT_TARGET_USER_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + } + ] + }, + { + label: 'Adversary', + field: ALERT_ADVERSARY_FIELD, + type: ElasticDataTypesEnum.OBJECT, + visible: true, + fields: [ + { + label: 'Adversary IP', + field: ALERT_ADVERSARY_IP_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Adversary URL', + field: ALERT_ADVERSARY_URL_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Adversary Bytes Sent', + field: ALERT_ADVERSARY_BYTES_SENT_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Adversary Domain', + field: ALERT_ADVERSARY_DOMAIN_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Adversary ASN', + field: ALERT_ADVERSARY_GEOLOCATION_ASN_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Adversary ASO', + field: ALERT_ADVERSARY_GEOLOCATION_ASO_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Adversary Latitude', + field: ALERT_ADVERSARY_GEOLOCATION_LATITUDE_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Adversary Longitude', + field: ALERT_ADVERSARY_GEOLOCATION_LONGITUDE_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Adversary File', + field: ALERT_ADVERSARY_FILE_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Adversary Host', + field: ALERT_ADVERSARY_HOST_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Adversary User', + field: ALERT_ADVERSARY_URL_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + } + ] + }, + { + label: 'Impact', + field: ALERT_IMPACT_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + fields: [ + { + label: 'Availability', + field: ALERT_IMPACT_AVAILABILITY_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: true, + }, + { + label: 'Confidentiality', + field: ALERT_IMPACT_CONFIDENTIALITY_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Integrity', + field: ALERT_IMPACT_INTEGRITY_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + ] + }, + { + label: 'ID', + field: ALERT_CASE_ID_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + /*{ + label: 'Tactic', + field: ALERT_TACTIC_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + },*/ + { + label: 'Technique', + field: ALERT_TECHNIQUE_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Note', + field: ALERT_NOTE_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Protocol', + field: ALERT_PROTOCOL_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Generated by', + field: ALERT_GENERATED_BY_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Category', + field: ALERT_CATEGORY_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Tags', + field: ALERT_TAGS_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Observation', + field: ALERT_OBSERVATION_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, + { + label: 'Incident ID', + field: ALERT_INCIDENT_ID_FIELD, + type: ElasticDataTypesEnum.NUMBER, + visible: false, + }, + { + label: 'Incident Name', + field: ALERT_INCIDENT_NAME_FIELD, + type: ElasticDataTypesEnum.STRING, + visible: false, + }, +]; diff --git a/frontend/src/app/shared/constants/date-timezone-date.const.ts b/frontend/src/app/shared/constants/date-timezone-date.const.ts index ac2068658..ebb65546d 100644 --- a/frontend/src/app/shared/constants/date-timezone-date.const.ts +++ b/frontend/src/app/shared/constants/date-timezone-date.const.ts @@ -25,6 +25,8 @@ export const TIMEZONES: Array<{ label: string; timezone: string, zone: string }> {label: 'Sydney (AEST)', timezone: 'Australia/Sydney', zone: 'Australia'}, {label: 'Melbourne (AEST)', timezone: 'Australia/Melbourne', zone: 'Australia'}, {label: 'Perth (AWST)', timezone: 'Australia/Perth', zone: 'Australia'}, + {label: 'New Zealand (NZST)', timezone: 'Pacific/Auckland', zone: 'Pacific'}, + {label: 'Fiji (FJT)', timezone: 'Pacific/Fiji', zone: 'Pacific'}, {label: 'Beijing (CST)', timezone: 'Asia/Shanghai', zone: 'Asia'}, {label: 'Tokyo (JST)', timezone: 'Asia/Tokyo', zone: 'Asia'}, {label: 'Seoul (KST)', timezone: 'Asia/Seoul', zone: 'Asia'}, @@ -37,7 +39,6 @@ export const TIMEZONES: Array<{ label: string; timezone: string, zone: string }> {label: 'Buenos Aires (ART)', timezone: 'America/Argentina/Buenos_Aires', zone: 'America'}, {label: 'Sรฃo Paulo (BRT)', timezone: 'America/Sao_Paulo', zone: 'America'}, ]; - export const DATE_FORMATS: Array<{ label: string; format: string; equivalentTo: string }> = [ {label: 'Short', format: 'short', equivalentTo: 'M/d/yy, h:mm a'}, {label: 'Medium', format: 'medium', equivalentTo: 'MMM d, y, h:mm:ss a'}, diff --git a/frontend/src/app/shared/services/elasticsearch/elastic-data.service.ts b/frontend/src/app/shared/services/elasticsearch/elastic-data.service.ts index 8e9663b67..9e3538198 100644 --- a/frontend/src/app/shared/services/elasticsearch/elastic-data.service.ts +++ b/frontend/src/app/shared/services/elasticsearch/elastic-data.service.ts @@ -15,12 +15,12 @@ export class ElasticDataService { } search(page: number, size: number, top: number, - pattern: string, filters?: any, sortBy?: string, groupByField?: string): Observable> { + pattern: string, filters?: any, sortBy?: string, includeChildren?: boolean): Observable> { const query = new QueryType(); query.add('page', page).add('size', size).add('top', top).add('indexPattern', pattern); - if (groupByField) { - query.add('groupByField', groupByField); + if (includeChildren) { + query.add('includeChildren', true); } if (sortBy) { @@ -31,6 +31,13 @@ export class ElasticDataService { , {observe: 'response'}); } + exists(pattern: string, filters?: any): Observable { + const query = new QueryType(); + query.add('indexPattern', pattern); + + return this.http.post(this.resourceUrl + 'count' + query.toString(), filters); + } + public exportToCsv(params): Observable { const headers = new HttpHeaders(); headers.append('Accept', 'application/octet-stream'); diff --git a/frontend/src/app/shared/services/login-provider.service.ts b/frontend/src/app/shared/services/login-provider.service.ts new file mode 100644 index 000000000..49315b2a0 --- /dev/null +++ b/frontend/src/app/shared/services/login-provider.service.ts @@ -0,0 +1,21 @@ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {UtmIdentityProvider} from '../../app-management/identity-provider/shared/models/utm-identity-provider.model'; +import {SERVER_API_URL} from '../../app.constants'; +import {createRequestOption} from '../util/request-util'; + +@Injectable({providedIn: 'root'}) +export class LoginProviderService { + serverApiUrl = SERVER_API_URL + 'api/utm-providers'; + + constructor(private http: HttpClient) {} + + getProviders(request: any) { + const params = createRequestOption(request); + return this.http.get(this.serverApiUrl, {params, observe: 'response'}); + } + + loginWithProvider(provider: string): void { + window.location.href = `${SERVER_API_URL}oauth2/authorization/${provider}`; + } +} diff --git a/frontend/src/app/shared/services/logstash/logstash.service.ts b/frontend/src/app/shared/services/logstash/logstash.service.ts index e37d3c80b..f864f3e51 100644 --- a/frontend/src/app/shared/services/logstash/logstash.service.ts +++ b/frontend/src/app/shared/services/logstash/logstash.service.ts @@ -11,7 +11,7 @@ import {createRequestOption} from '../../util/request-util'; providedIn: 'root' }) export class LogstashService { - public logstashFilterResourceUrl = SERVER_API_URL + 'api/logstash-filters'; + public logstashFilterResourceUrl = SERVER_API_URL + 'api/utm-filters'; constructor(private http: HttpClient) { } diff --git a/frontend/src/app/shared/services/tfa/tfa.service.ts b/frontend/src/app/shared/services/tfa/tfa.service.ts index 896c0c220..0b2d64188 100644 --- a/frontend/src/app/shared/services/tfa/tfa.service.ts +++ b/frontend/src/app/shared/services/tfa/tfa.service.ts @@ -3,6 +3,12 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import {SERVER_API_URL} from '../../../app.constants'; +export enum TfaStage { + INIT, + VERIFY, + COMPLETE +} + export enum TfaMethod { EMAIL = 'EMAIL', TOTP = 'TOTP' @@ -38,11 +44,19 @@ export interface TfaVerifyResponse { expiresInSeconds?: number; } +export interface TfaEnrollRequest { + initMethod: TfaMethod; + verifyMethod?: TfaMethod; + verifyCode?: string; + completeRequest?: TfaSaveRequest; +} + @Injectable({ providedIn: 'root' }) export class TfaService { private readonly baseUrl = `${SERVER_API_URL}api/tfa`; + private readonly enrollBaseUrl = `${SERVER_API_URL}api/enrollment/tfa`; constructor(private http: HttpClient) {} @@ -57,4 +71,11 @@ export class TfaService { completeTfa(request: TfaSaveRequest): Observable { return this.http.post(`${this.baseUrl}/complete`, request); } + + + enrollTfa(request: any): Observable { + return this.http.post(`${this.enrollBaseUrl}`, request); + } + + } diff --git a/frontend/src/app/shared/services/util/version-type.service.ts b/frontend/src/app/shared/services/util/version-type.service.ts index aece37823..9a66d204b 100644 --- a/frontend/src/app/shared/services/util/version-type.service.ts +++ b/frontend/src/app/shared/services/util/version-type.service.ts @@ -1,5 +1,12 @@ import {Injectable} from '@angular/core'; import {BehaviorSubject} from 'rxjs'; +import {ModalConfirmationComponent} from '../../components/utm/util/modal-confirmation/modal-confirmation.component'; +import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; + +export const EnterpriseFeatures = [ + 'AUTH_WITH_PROVIDERS_MODULE', +]; + export enum VersionType { COMMUNITY = 'COMMUNITY', @@ -13,6 +20,8 @@ export class VersionTypeService { private versionTypeBehavior = new BehaviorSubject(VersionType.COMMUNITY); versionType$ = this.versionTypeBehavior.asObservable(); + constructor(private modalService: NgbModal) {} + changeVersionType(versionType: VersionType) { this.versionTypeBehavior.next(versionType); } @@ -20,4 +29,24 @@ export class VersionTypeService { versionType(): VersionType { return this.versionTypeBehavior.getValue(); } + + showVersionInfo() { + const modalSource = this.modalService.open(ModalConfirmationComponent, { centered: true }); + + modalSource.componentInstance.header = 'Enterprise Feature'; + modalSource.componentInstance.message = + 'This feature is available only in the Enterprise edition of the platform. ' + + 'For more information about upgrading or accessing this functionality, please contact our support team at ' + + 'support@services.utmstack.com.'; + modalSource.componentInstance.confirmBtnText = 'OK'; + modalSource.componentInstance.confirmBtnIcon = 'icon-info'; + modalSource.componentInstance.confirmBtnType = 'default'; + modalSource.componentInstance.hideBtnCancel = true; + + modalSource.result.then(() => { + // optional callback logic + }); + } + + } diff --git a/frontend/src/app/shared/types/alert/utm-alert.type.ts b/frontend/src/app/shared/types/alert/utm-alert.type.ts index 581c3ec30..62b53babb 100644 --- a/frontend/src/app/shared/types/alert/utm-alert.type.ts +++ b/frontend/src/app/shared/types/alert/utm-alert.type.ts @@ -29,8 +29,9 @@ export class UtmAlertType { technique: string; impact?: { [key: string]: number }; parentId?: string; - children?: any[]; + hasChildren: boolean; expanded: boolean; + echoes: number; } export enum AlertStatusLabelEnum { @@ -47,6 +48,7 @@ export enum AlertStatusEnum { IN_REVIEW = 3, IGNORED = 4, COMPLETED = 5, + COMPLETED_AS_FALSE_POSITIVE = 6, } diff --git a/frontend/src/app/shared/types/utm-timeline-item.ts b/frontend/src/app/shared/types/utm-timeline-item.ts new file mode 100644 index 000000000..5614d9f96 --- /dev/null +++ b/frontend/src/app/shared/types/utm-timeline-item.ts @@ -0,0 +1,7 @@ +export interface TimelineItem { + startDate: string | Date; + name: string; + metadata: any; + iconUrl: string | undefined | null; + yOffset?: number; +} diff --git a/frontend/src/app/shared/util/replace-command-tokens.util.ts b/frontend/src/app/shared/util/replace-command-tokens.util.ts index 89399eda3..7dbddc16a 100644 --- a/frontend/src/app/shared/util/replace-command-tokens.util.ts +++ b/frontend/src/app/shared/util/replace-command-tokens.util.ts @@ -1,4 +1,33 @@ -export function replaceCommandTokens(command: string, wordsToReplace: { [key: string]: string }) { - return Object.keys(wordsToReplace) - .reduce((f, s) => f.replace(new RegExp(s, 'ig'), wordsToReplace[s]), command); +export function replaceCommandTokens(command: string, wordsToReplace: { [key: string]: string }): string { + let cmd = command; + + if (cmd.includes('-ArgumentList')) { + + const args = Object.values(wordsToReplace) + .filter(v => v && v.trim().length > 0) + .map(v => `'${v.trim()}'`) + .join(', '); + + cmd = cmd.replace( + /-ArgumentList\s+(['"].*?['"])(?=\s+-|$)/, + `-ArgumentList ${args}` + ); + } else { + const match = cmd.match(/"(.*)"/); + if (match) { + const original = match[1]; + const parts = original.split(/\s+/); + const fixedCommand = parts[0]; + const args = Object.entries(wordsToReplace) + .filter(([_, v]) => v && v.trim().length > 0) + .map(([_, v]) => v.trim()) + .join(' '); + + cmd = cmd.replace(/"(.*)"/, `"${fixedCommand} ${args}"`); + } + } + + cmd = cmd.replace(/\s+/g, ' ').trim(); + + return cmd; } diff --git a/frontend/src/app/shared/utm-shared.module.ts b/frontend/src/app/shared/utm-shared.module.ts index f4f890f75..89989cedb 100644 --- a/frontend/src/app/shared/utm-shared.module.ts +++ b/frontend/src/app/shared/utm-shared.module.ts @@ -28,6 +28,7 @@ import {PasswordStrengthBarComponent} from './components/auth/password-strength/ import { TfaSetupComponent } from './components/auth/tfa-setup/tfa-setup.component'; import {TotpComponent} from './components/auth/totp/totp.component'; import {ContactUsComponent} from './components/contact-us/contact-us.component'; +import {AlertEchoesTimelineComponent} from '../data-management/alert-management/shared/components/alert-echoes-timeline/alert-echoes-timeline.component'; import { EmailSettingNotificactionComponent } from './components/email-setting-notification/email-setting-notificaction.component'; @@ -238,27 +239,29 @@ import {HighlightPipe} from './pipes/text/highlight.pipe'; import {TimePeriodPipe} from './pipes/time-period.pipe'; import {TimezoneOffsetPipe} from './pipes/timezone-offset.pipe'; import {UtmNotifier} from './websocket/utm-notifier'; - +import { NgxEchartsModule } from 'ngx-echarts'; +import { LoginProvidersComponent } from './components/auth/login-providers/login-providers.component'; @NgModule({ - imports: [ - InlineSVGModule, - CommonModule, - ReactiveFormsModule, - TranslateModule, - NgbModule, - FormsModule, - RouterModule, - NgSelectModule, - NgxSortableModule, - NgxFlagIconCssModule, - NgxJsonViewerModule, - AssetsApplyTypeModule, - AssetsApplyNoteModule, - AssetsGroupAddModule, - InfiniteScrollModule - ], + imports: [ + InlineSVGModule, + CommonModule, + ReactiveFormsModule, + TranslateModule, + NgbModule, + FormsModule, + RouterModule, + NgSelectModule, + NgxSortableModule, + NgxFlagIconCssModule, + NgxJsonViewerModule, + AssetsApplyTypeModule, + AssetsApplyNoteModule, + AssetsGroupAddModule, + InfiniteScrollModule, + NgxEchartsModule + ], declarations: [ ElasticFilterComponent, ElasticFilterAddComponent, @@ -268,6 +271,7 @@ import {UtmNotifier} from './websocket/utm-notifier'; HasAnyAuthorityDirective, NotFoundComponent, SidebarComponent, + AlertEchoesTimelineComponent, PasswordResetInitComponent, PasswordResetFinishComponent, PasswordStrengthBarComponent, @@ -397,7 +401,8 @@ import {UtmNotifier} from './websocket/utm-notifier'; RelativeTimePipe, UtmTfaConfCheckComponent, UtmTfaVerificationComponent, - TfaSetupComponent + TfaSetupComponent, + LoginProvidersComponent ], exports: [ IndexPatternCreateComponent, @@ -407,6 +412,7 @@ import {UtmNotifier} from './websocket/utm-notifier'; SidebarComponent, PasswordResetInitComponent, PasswordResetFinishComponent, + AlertEchoesTimelineComponent, PasswordStrengthBarComponent, SortByComponent, UtmSpinnerComponent, diff --git a/frontend/src/assets/icons/echoes/echoes_default.png b/frontend/src/assets/icons/echoes/echoes_default.png new file mode 100644 index 000000000..4336b48c9 Binary files /dev/null and b/frontend/src/assets/icons/echoes/echoes_default.png differ diff --git a/frontend/src/assets/icons/echoes/echoesicon.png b/frontend/src/assets/icons/echoes/echoesicon.png new file mode 100644 index 000000000..40aa0790b Binary files /dev/null and b/frontend/src/assets/icons/echoes/echoesicon.png differ diff --git a/frontend/src/assets/img/guides/logos/utmstack.png b/frontend/src/assets/img/guides/logos/utmstack.png new file mode 100644 index 000000000..a25e627d6 Binary files /dev/null and b/frontend/src/assets/img/guides/logos/utmstack.png differ diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index d1fff92cc..586cb5a3b 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -4,8 +4,8 @@ export const environment = { production: false, - SERVER_API_URL: 'https://192.168.1.18/', - // SERVER_API_URL: 'http://localhost:8080/', + // SERVER_API_URL: 'https://192.168.1.18/', + SERVER_API_URL: 'http://localhost:8080/', SERVER_API_CONTEXT: '', SESSION_AUTH_TOKEN: window.location.host.split(':')[0].toLocaleUpperCase(), WEBSOCKET_URL: '//localhost:8080', diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index fb08a0806..05ed6f6d7 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -637,6 +637,7 @@ app-header { .add-rule-modal { .modal-content { + max-height: 900px !important; overflow-y: hidden; } } @@ -1360,6 +1361,20 @@ app-logstash-pipelines{ max-width: 350px !important; } +.popover.utm-alert-action-select { + + .arrow { + display: none !important; + } + + .popover-body { + padding: 0!important; + } + + min-width: 200px !important; + max-width: 260px !important; +} + .border-radius-1 { border-radius: 4px; } @@ -1400,6 +1415,19 @@ app-utm-items-per-page { transform: translateY(-50%); } +.view-rule-modal { + .modal-content { + overflow: hidden!important; + } +} + +.alert-rule-modal { + .modal-content { + overflow: hidden!important; + max-height: fit-content; + } +} + diff --git a/installer/build.sh b/installer/build.sh new file mode 100755 index 000000000..30123dcdf --- /dev/null +++ b/installer/build.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -e # Exit on error + +# ============================================ +# CONFIGURATION - Edit these values +# ============================================ +DEFAULT_BRANCH="prod" +INSTALLER_VERSION="v11.0.0-dev.1" +CM_ENCRYPT_SALT="your-encryption-salt-here" +CM_SIGN_PUBLIC_KEY="-----BEGIN PUBLIC KEY----- +your-public-key-here +-----END PUBLIC KEY-----" + +# ============================================ +# Build Process +# ============================================ + +# Change to installer directory +cd "$(dirname "${BASH_SOURCE[0]}")" + +# Set Go environment variables (same as GitHub Actions) +export GOOS=linux +export GOARCH=amd64 +export GOPRIVATE=github.com/utmstack +export GONOPROXY=github.com/utmstack +export GONOSUMDB=github.com/utmstack + +echo "Building V11 Installer for production release" + +# Execute build with ldflags +go build -o installer -v -ldflags "\ +-X 'github.com/utmstack/UTMStack/installer/config.DEFAULT_BRANCH=${DEFAULT_BRANCH}' \ +-X 'github.com/utmstack/UTMStack/installer/config.INSTALLER_VERSION=${INSTALLER_VERSION}' \ +-X 'github.com/utmstack/UTMStack/installer/config.REPLACE=${CM_ENCRYPT_SALT}' \ +-X 'github.com/utmstack/UTMStack/installer/config.PUBLIC_KEY=${CM_SIGN_PUBLIC_KEY}'" \ +. + +echo "โœ… Build completed: ./installer" diff --git a/installer/config/const.go b/installer/config/const.go index 842a139eb..e7ab24732 100644 --- a/installer/config/const.go +++ b/installer/config/const.go @@ -16,8 +16,6 @@ const ( ImagesPath = "/utmstack/images" - CMServer = "https://customermanager.utmstack.com" - RequiredMinCPUCores = 2 RequiredMinDiskSpace = 30 RequiredDistroUbuntu = "ubuntu" @@ -45,5 +43,8 @@ var ( func GetCMServer() string { cnf := GetConfig() - return CMServer + "/" + cnf.Branch + if cnf.Branch == "alpha" { + return "https://cm.dev.utmstack.com" + } + return "https://cm.utmstack.com" } diff --git a/installer/services/search.go b/installer/services/search.go index 429b14bef..8c884bc6e 100644 --- a/installer/services/search.go +++ b/installer/services/search.go @@ -42,7 +42,7 @@ func InitOpenSearch() error { _, err = grequests.Put(baseURL+"_index_template/utmstack_indexes", &grequests.RequestOptions{ JSON: map[string]any{ - "index_patterns": []string{"alert-*", "log-*", ".utm-*", ".utmstack-*"}, + "index_patterns": []string{"v11-alert-", "v11-log-", ".utm-", ".utmstack-"}, "template": map[string]any{ "settings": map[string]any{ "index.number_of_shards": 1, diff --git a/installer/updater/client.go b/installer/updater/client.go index af593f470..106c599e4 100644 --- a/installer/updater/client.go +++ b/installer/updater/client.go @@ -115,24 +115,20 @@ func (c *UpdaterClient) CheckUpdate() error { updates = append(updates, newUpdate) } - currentVersion, err := GetVersion() - if err != nil { - return fmt.Errorf("error getting current version: %v", err) - } - sortedUpdates := SortVersions(updates) for _, update := range sortedUpdates { - if update["version"] != currentVersion.Version { - err := c.UpdateToNewVersion(update["version"], update["edition"], update["changelog"]) + // Apply all updates from the server regardless of current version + // This allows for rollbacks, pre-release type changes (alphaโ†’dev), and ensures all updates are applied in order + // The server is responsible for only sending pending updates (marked as sent after application) + err := c.UpdateToNewVersion(update["version"], update["edition"], update["changelog"]) + if err != nil { + return fmt.Errorf("error updating to new version: %v", err) + } + if update["id"] != "offline" { + err = c.MarkUpdateSent(update["id"]) if err != nil { - return fmt.Errorf("error updating to new version: %v", err) - } - if update["id"] != "offline" { - err = c.MarkUpdateSent(update["id"]) - if err != nil { - return fmt.Errorf("error marking update as sent: %v", err) - } + return fmt.Errorf("error marking update as sent: %v", err) } } } diff --git a/installer/updater/versions.go b/installer/updater/versions.go index 4c17a5730..d7f198b9a 100644 --- a/installer/updater/versions.go +++ b/installer/updater/versions.go @@ -127,7 +127,8 @@ func ParseVersion(versionStr string) Version { versionStr = strings.TrimPrefix(versionStr, "V") // Parse version with regex: X.Y.Z or X.Y.Z-type.num - re := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:-(alpha|beta|rc)\.(\d+))?$`) + // Supports: dev, alpha, beta, rc + re := regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)(?:-(dev|alpha|beta|rc)\.(\d+))?$`) matches := re.FindStringSubmatch(versionStr) if len(matches) > 1 { @@ -161,11 +162,30 @@ func CompareVersions(v1, v2 Version) int { return v1.Patch - v2.Patch } - if v1.PrereleaseNum != v2.PrereleaseNum { - return v1.PrereleaseNum - v2.PrereleaseNum + // Handle stable vs prerelease versions + // According to semver: stable version (no prerelease) is GREATER than any prerelease + // Example: v11.0.0 > v11.0.0-rc.1 > v11.0.0-beta.1 > v11.0.0-alpha.1 > v11.0.0-dev.1 + if v1.PrereleaseName == "" && v2.PrereleaseName != "" { + return 1 // v1 is stable, v2 is prerelease โ†’ v1 > v2 + } + if v1.PrereleaseName != "" && v2.PrereleaseName == "" { + return -1 // v1 is prerelease, v2 is stable โ†’ v1 < v2 + } + + // Both are prereleases or both are stable + if v1.PrereleaseName != v2.PrereleaseName { + // Compare prerelease type names: dev < alpha < beta < rc + order := map[string]int{ + "dev": 1, + "alpha": 2, + "beta": 3, + "rc": 4, + } + return order[v1.PrereleaseName] - order[v2.PrereleaseName] } - return 0 + // Same prerelease type, compare numbers + return v1.PrereleaseNum - v2.PrereleaseNum } func SortVersions(versions []map[string]string) []map[string]string { diff --git a/plugins/alerts/main.go b/plugins/alerts/main.go index af3ac692d..81015ae84 100644 --- a/plugins/alerts/main.go +++ b/plugins/alerts/main.go @@ -255,6 +255,9 @@ func getPreviousAlertId(alert *plugins.Alert) *string { hits, err := searchQuery.SearchIn(ctx, []string{opensearch.BuildIndexPattern("v11", "alert")}) if err == nil { if hits.Hits.Total.Value != 0 { + + go updateParentAlertToOpen(hits.Hits.Hits[0]) + return utils.PointerOf(hits.Hits.Hits[0].ID) } return nil @@ -373,3 +376,67 @@ func newAlert(alert *plugins.Alert, parentId *string) error { // This should never be reached, but just in case return nil } + +func updateParentAlertToOpen(parentHit opensearch.Hit) { + defer func() { + if r := recover(); r != nil { + _ = catcher.Error("recovered from panic in updateParentAlertToOpen", nil, map[string]any{ + "panic": r, + "parentId": parentHit.ID, + }) + } + }() + + var parentAlert AlertFields + err := parentHit.Source.ParseSource(&parentAlert) + if err != nil { + _ = catcher.Error("cannot parse parent alert source", err, map[string]any{ + "parentId": parentHit.ID, + }) + return + } + + // Only update if it is Completed status + if parentAlert.Status == 5 { + parentAlert.Status = 2 + parentAlert.StatusLabel = "Open" + + err := parentHit.Source.SetSource(parentAlert) + if err != nil { + _ = catcher.Error("cannot set updated parent alert source", err, map[string]any{ + "parentId": parentHit.ID, + }) + return + } + + maxRetries := 3 + retryDelay := 2 * time.Second + + for retry := 0; retry < maxRetries; retry++ { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + + err := parentHit.Save(ctx) + cancel() + + if err != nil { + _ = catcher.Error("failed to update parent alert to Open, retrying", err, map[string]any{ + "parentId": parentHit.ID, + "retry": retry + 1, + "maxRetries": maxRetries, + }) + + if retry < maxRetries-1 { + time.Sleep(retryDelay) + retryDelay *= 2 + } + continue + } + + return + } + + _ = catcher.Error("all retries failed when updating parent alert to Open", nil, map[string]any{ + "parentId": parentHit.ID, + }) + } +} diff --git a/plugins/bitdefender/config/config.go b/plugins/bitdefender/config/config.go index 214b5cdff..001f64eb6 100644 --- a/plugins/bitdefender/config/config.go +++ b/plugins/bitdefender/config/config.go @@ -210,13 +210,13 @@ func apiPush(config BDGZModuleConfig, operation string) error { fn, ok := operationFunc[operation] if !ok { - return catcher.Error("wrong operation", nil, map[string]any{}) + return catcher.Error("wrong operation", nil, nil) } for i := 0; i < 5; i++ { response, err := fn(config) if err != nil { - _ = catcher.Error(fmt.Sprintf("%v", err), err, map[string]any{}) + _ = catcher.Error(fmt.Sprintf("%v", err), err, nil) time.Sleep(1 * time.Minute) continue } @@ -226,14 +226,14 @@ func apiPush(config BDGZModuleConfig, operation string) error { return nil } - return catcher.Error("error sending configuration after 5 retries", nil, map[string]any{}) + return catcher.Error("error sending configuration after 5 retries", nil, nil) } func sendPushEventSettings(config BDGZModuleConfig) (*http.Response, error) { byteTemplate := getTemplateSetPush(config) body, err := json.Marshal(byteTemplate) if err != nil { - return nil, catcher.Error("error when marshaling the request body to send the configuration", err, map[string]any{}) + return nil, catcher.Error("error when marshaling the request body to send the configuration", err, nil) } return sendRequest(body, config) } @@ -242,7 +242,7 @@ func getPushEventSettings(config BDGZModuleConfig) (*http.Response, error) { byteTemplate := getTemplateGet() body, err := json.Marshal(byteTemplate) if err != nil { - return nil, catcher.Error("error when marshaling the request body to get the configuration", err, map[string]any{}) + return nil, catcher.Error("error when marshaling the request body to get the configuration", err, nil) } return sendRequest(body, config) } @@ -251,7 +251,7 @@ func sendTestPushEvent(config BDGZModuleConfig) (*http.Response, error) { byteTemplate := getTemplateTest() body, err := json.Marshal(byteTemplate) if err != nil { - return nil, catcher.Error("error when marshaling the request body to send the test event", err, map[string]any{}) + return nil, catcher.Error("error when marshaling the request body to send the test event", err, nil) } return sendRequest(body, config) } diff --git a/plugins/bitdefender/config/req.go b/plugins/bitdefender/config/req.go index 74c366976..3fc613503 100644 --- a/plugins/bitdefender/config/req.go +++ b/plugins/bitdefender/config/req.go @@ -12,7 +12,7 @@ import ( func sendRequest(body []byte, config BDGZModuleConfig) (*http.Response, error) { r, err := http.NewRequest("POST", config.AccessUrl+EndpointPush, bytes.NewBuffer(body)) if err != nil { - return nil, catcher.Error("cannot create request", err, map[string]any{}) + return nil, catcher.Error("cannot create request", err, nil) } r.Header.Add("Content-Type", "application/json") @@ -21,7 +21,7 @@ func sendRequest(body []byte, config BDGZModuleConfig) (*http.Response, error) { client := &http.Client{} resp, err := client.Do(r) if err != nil { - return nil, catcher.Error("cannot send request", err, map[string]any{}) + return nil, catcher.Error("cannot send request", err, nil) } return resp, nil } diff --git a/plugins/bitdefender/server/message.go b/plugins/bitdefender/server/message.go index dc83035c4..5ebdf5da8 100644 --- a/plugins/bitdefender/server/message.go +++ b/plugins/bitdefender/server/message.go @@ -20,7 +20,7 @@ func CreateMessage(cnf *config.ConfigurationSection, events []string) { pattern := "BitdefenderGZCompanyId=" + compID match, err := regexp.MatchString(pattern, syslogMessage) if err != nil { - _ = catcher.Error("error matching pattern", err, map[string]any{}) + _ = catcher.Error("error matching pattern", err, nil) continue } diff --git a/plugins/bitdefender/server/server.go b/plugins/bitdefender/server/server.go index cfcc5ea07..02b5aa893 100644 --- a/plugins/bitdefender/server/server.go +++ b/plugins/bitdefender/server/server.go @@ -26,7 +26,7 @@ func GetLogs() http.HandlerFunc { if conf.ModuleActive { if r.Header.Get("authorization") == "" { message := "401 Missing Authorization Header" - _ = catcher.Error("missing authorization header", nil, map[string]any{}) + _ = catcher.Error("missing authorization header", nil, nil) j, _ := json.Marshal(message) w.WriteHeader(http.StatusUnauthorized) _, err := w.Write(j) @@ -45,7 +45,7 @@ func GetLogs() http.HandlerFunc { } if !isAuth { message := "401 Invalid Authentication Credentials" - _ = catcher.Error("invalid authentication credentials", nil, map[string]any{}) + _ = catcher.Error("invalid authentication credentials", nil, nil) j, _ := json.Marshal(message) w.WriteHeader(http.StatusUnauthorized) _, err := w.Write(j) @@ -58,7 +58,7 @@ func GetLogs() http.HandlerFunc { var newBody schema.BodyEvents err := json.NewDecoder(r.Body).Decode(&newBody) if err != nil { - _ = catcher.Error("error decoding body", err, map[string]any{}) + _ = catcher.Error("error decoding body", err, nil) return } @@ -72,7 +72,7 @@ func GetLogs() http.HandlerFunc { _ = catcher.Error("cannot write response", err, nil) } } else { - _ = catcher.Error("bitdefender module disabled", nil, map[string]any{}) + _ = catcher.Error("bitdefender module disabled", nil, nil) } } } diff --git a/plugins/config/main.go b/plugins/config/main.go index 7d677fa5e..f87f95f8f 100644 --- a/plugins/config/main.go +++ b/plugins/config/main.go @@ -120,7 +120,7 @@ func (a *Asset) FromVar(name any, hostnames any, ipAddresses any, confidentialit hostnamesStr := utils.CastString(hostnames) err := json.Unmarshal([]byte(hostnamesStr), &hostnamesList) if err != nil { - _ = catcher.Error("failed to unmarshal hostnames list", err, map[string]any{}) + _ = catcher.Error("failed to unmarshal hostnames list", err, nil) return } } @@ -131,7 +131,7 @@ func (a *Asset) FromVar(name any, hostnames any, ipAddresses any, confidentialit ipAddressesStr := utils.CastString(ipAddresses) err := json.Unmarshal([]byte(ipAddressesStr), &ipAddressesList) if err != nil { - _ = catcher.Error("failed to unmarshal ip addresses list", err, map[string]any{}) + _ = catcher.Error("failed to unmarshal ip addresses list", err, nil) return } } @@ -176,7 +176,7 @@ func (r *Rule) FromVar(id int64, dataTypes []string, ruleName any, confidentiali referencesStr := utils.CastString(references) err := json.Unmarshal([]byte(referencesStr), &referencesList) if err != nil { - _ = catcher.Error("failed to unmarshal references list", err, map[string]any{}) + _ = catcher.Error("failed to unmarshal references list", err, nil) return } } @@ -187,7 +187,7 @@ func (r *Rule) FromVar(id int64, dataTypes []string, ruleName any, confidentiali deduplicateStr := utils.CastString(deduplicate) err := json.Unmarshal([]byte(deduplicateStr), &deduplicateList) if err != nil { - _ = catcher.Error("failed to unmarshal deduplicate list", err, map[string]any{}) + _ = catcher.Error("failed to unmarshal deduplicate list", err, nil) return } } @@ -199,7 +199,7 @@ func (r *Rule) FromVar(id int64, dataTypes []string, ruleName any, confidentiali afterStr := utils.CastString(after) err := json.Unmarshal([]byte(afterStr), &afterBackendObj) if err != nil { - _ = catcher.Error("failed to unmarshal after list", err, map[string]any{}) + _ = catcher.Error("failed to unmarshal after list", err, nil) return } @@ -238,7 +238,7 @@ func main() { func() { db, err := connect() if err != nil { - _ = catcher.Error("failed to connect to database", err, map[string]any{}) + _ = catcher.Error("failed to connect to database", err, nil) // Don't exit, just sleep and retry time.Sleep(30 * time.Second) return @@ -247,7 +247,7 @@ func main() { filters, err := getFilters(db) if err != nil { - _ = catcher.Error("failed to get filters", err, map[string]any{}) + _ = catcher.Error("failed to get filters", err, nil) // Don't exit, just sleep and retry time.Sleep(30 * time.Second) return @@ -255,7 +255,7 @@ func main() { assets, err := getAssets(db) if err != nil { - _ = catcher.Error("failed to get assets", err, map[string]any{}) + _ = catcher.Error("failed to get assets", err, nil) // Don't exit, just sleep and retry time.Sleep(30 * time.Second) return @@ -263,7 +263,7 @@ func main() { rules, err := getRules(db) if err != nil { - _ = catcher.Error("failed to get rules", err, map[string]any{}) + _ = catcher.Error("failed to get rules", err, nil) // Don't exit, just sleep and retry time.Sleep(30 * time.Second) return @@ -271,7 +271,7 @@ func main() { patterns, err := getPatterns(db) if err != nil { - _ = catcher.Error("failed to get patterns", err, map[string]any{}) + _ = catcher.Error("failed to get patterns", err, nil) // Don't exit, just sleep and retry time.Sleep(30 * time.Second) return @@ -309,7 +309,7 @@ func main() { err = cleanUpFilters(filters) if err != nil { - _ = catcher.Error("failed to clean up filters", err, map[string]any{}) + _ = catcher.Error("failed to clean up filters", err, nil) // Don't exit, just sleep and retry time.Sleep(30 * time.Second) return @@ -317,7 +317,7 @@ func main() { err = writeFilters(filters) if err != nil { - _ = catcher.Error("failed to write filters", err, map[string]any{}) + _ = catcher.Error("failed to write filters", err, nil) // Don't exit, just sleep and retry time.Sleep(30 * time.Second) return @@ -325,7 +325,7 @@ func main() { err = cleanUpRules(rules) if err != nil { - _ = catcher.Error("failed to clean up rules", err, map[string]any{}) + _ = catcher.Error("failed to clean up rules", err, nil) // Don't exit, just sleep and retry time.Sleep(30 * time.Second) return @@ -333,7 +333,7 @@ func main() { err = writeRules(rules) if err != nil { - _ = catcher.Error("failed to write rules", err, map[string]any{}) + _ = catcher.Error("failed to write rules", err, nil) // Don't exit, just sleep and retry time.Sleep(30 * time.Second) return @@ -341,7 +341,7 @@ func main() { err = writeTenant(tenant) if err != nil { - _ = catcher.Error("failed to write tenant", err, map[string]any{}) + _ = catcher.Error("failed to write tenant", err, nil) // Don't exit, just sleep and retry time.Sleep(30 * time.Second) return @@ -349,7 +349,7 @@ func main() { err = writePatterns(patterns) if err != nil { - _ = catcher.Error("failed to write patterns", err, map[string]any{}) + _ = catcher.Error("failed to write patterns", err, nil) // Don't exit, just sleep and retry time.Sleep(30 * time.Second) return @@ -379,7 +379,7 @@ func connect() (*sql.DB, error) { err = db.Ping() if err != nil { - return nil, catcher.Error("failed to ping database", err, map[string]any{}) + return nil, catcher.Error("failed to ping database", err, nil) } return db, nil @@ -626,7 +626,7 @@ func cleanUpRules(rules []Rule) error { files, err := listFiles(rulesFolder.String()) if err != nil { - return catcher.Error("failed to list files", err, map[string]any{}) + return catcher.Error("failed to list files", err, nil) } for _, file := range files { diff --git a/plugins/crowdStrike/README.md b/plugins/crowdStrike/README.md new file mode 100644 index 000000000..b26a2b1c8 --- /dev/null +++ b/plugins/crowdStrike/README.md @@ -0,0 +1,39 @@ +# UTMStack Plugin for CrowdStrike Falcon + +## Description + +UTMStack Plugin for CrowdStrike Falcon is a connector developed in Golang that receives real-time events from `CrowdStrike Falcon Event Streams` and sends them to the `UTMStack` processing server for further processing. + +This connector uses a `GRPC` client to communicate with the UTMStack processing server. The client connects through a `Unix socket` that is created in the UTMStack working directory. + +To obtain the events, `CrowdStrike GoFalcon SDK` is used to communicate with the Falcon Event Streams API, providing real-time security event data from your CrowdStrike environment. + +Please note that the connector requires valid CrowdStrike Falcon API credentials to run. The connector will not work without proper authentication. + +## Configuration + +The plugin requires the following configuration parameters: + +- **client_id**: OAuth2 Client ID for CrowdStrike Falcon API +- **client_secret**: OAuth2 Client Secret for CrowdStrike Falcon API +- **member_cid**: (Optional) Member CID for MSSP environments +- **cloud**: Falcon cloud region (us-1, us-2, eu-1, us-gov-1) + +## Features + +- Real-time event streaming from CrowdStrike Falcon +- Automatic stream discovery and processing +- Error handling and retry mechanisms +- Event batching to optimize performance +- Timeout controls to prevent blocking +- Structured JSON event formatting + +## Authentication + +This plugin uses OAuth2 authentication with CrowdStrike Falcon API. You need to: + +1. Create API client credentials in your CrowdStrike console +2. Ensure the client has the required scopes for Event Streams API +3. Configure the credentials in UTMStack module configuration + +For more information on creating API credentials, visit: https://falcon.crowdstrike.com/support/api-clients-and-keys \ No newline at end of file diff --git a/plugins/crowdStrike/check.go b/plugins/crowdStrike/check.go new file mode 100644 index 000000000..51c0ab79b --- /dev/null +++ b/plugins/crowdStrike/check.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/threatwinds/go-sdk/catcher" +) + +func connectionChecker(url string) error { + checkConn := func() error { + if err := checkConnection(url); err != nil { + return fmt.Errorf("connection failed: %v", err) + } + return nil + } + + if err := infiniteRetryIfXError(checkConn, "connection failed"); err != nil { + return err + } + + return nil +} + +func checkConnection(url string) error { + client := &http.Client{ + Timeout: 30 * time.Second, + } + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return err + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer func() { + err := resp.Body.Close() + if err != nil { + _ = catcher.Error("error closing response body: %v", err, nil) + } + }() + + return nil +} + +func infiniteRetryIfXError(f func() error, exception string) error { + var xErrorWasLogged bool + + for { + err := f() + if err != nil && is(err, exception) { + if !xErrorWasLogged { + _ = catcher.Error("An error occurred (%s), will keep retrying indefinitely...", err, nil) + xErrorWasLogged = true + } + time.Sleep(wait) + continue + } + + return err + } +} + +func is(e error, args ...string) bool { + for _, arg := range args { + if strings.Contains(e.Error(), arg) { + return true + } + } + return false +} diff --git a/plugins/crowdStrike/config/config.go b/plugins/crowdStrike/config/config.go new file mode 100644 index 000000000..50aef2ecb --- /dev/null +++ b/plugins/crowdStrike/config/config.go @@ -0,0 +1,141 @@ +package config + +import ( + "context" + "fmt" + "log" + "strings" + sync "sync" + "time" + + "github.com/threatwinds/go-sdk/catcher" + "github.com/threatwinds/go-sdk/plugins" + "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +const ( + reconnectDelay = 5 * time.Second + maxMessageSize = 1024 * 1024 * 1024 +) + +var ( + cnf *ConfigurationSection + mu sync.Mutex + + internalKey string + modulesConfigHost string +) + +func GetConfig() *ConfigurationSection { + mu.Lock() + defer mu.Unlock() + if cnf == nil { + return &ConfigurationSection{} + } + return cnf +} + +func StartConfigurationSystem() { + for { + pluginConfig := plugins.PluginCfg("com.utmstack", false) + if !pluginConfig.Exists() { + _ = catcher.Error("plugin configuration not found", nil, nil) + time.Sleep(reconnectDelay) + continue + } + internalKey = pluginConfig.Get("internalKey").String() + modulesConfigHost = pluginConfig.Get("modulesConfig").String() + + if internalKey == "" || modulesConfigHost == "" { + fmt.Println("Internal key or Modules Config Host is not set, skipping UTMStack plugin execution") + time.Sleep(reconnectDelay) + continue + } + break + } + + for { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + ctx = metadata.AppendToOutgoingContext(ctx, "internal-key", internalKey) + conn, err := grpc.NewClient( + modulesConfigHost, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMessageSize)), + ) + + if err != nil { + catcher.Error("Failed to connect to server", err, nil) + cancel() + time.Sleep(reconnectDelay) + continue + } + + state := conn.GetState() + if state == connectivity.Shutdown || state == connectivity.TransientFailure { + catcher.Error("Connection is in shutdown or transient failure state", nil, nil) + cancel() + time.Sleep(reconnectDelay) + continue + } + + client := NewConfigServiceClient(conn) + stream, err := client.StreamConfig(ctx) + if err != nil { + catcher.Error("Failed to create stream", err, nil) + conn.Close() + cancel() + time.Sleep(reconnectDelay) + continue + } + + err = stream.Send(&BiDirectionalMessage{ + Payload: &BiDirectionalMessage_PluginInit{ + PluginInit: &PluginInit{Type: PluginType_CROWDSTRIKE}, + }, + }) + if err != nil { + catcher.Error("Failed to send PluginInit", err, nil) + conn.Close() + cancel() + time.Sleep(reconnectDelay) + continue + } + + for { + in, err := stream.Recv() + if err != nil { + if strings.Contains(err.Error(), "EOF") { + catcher.Info("Stream closed by server, reconnecting...", nil) + conn.Close() + cancel() + time.Sleep(reconnectDelay) + break + } + st, ok := status.FromError(err) + if ok && (st.Code() == codes.Unavailable || st.Code() == codes.Canceled) { + catcher.Error("Stream error: "+st.Message(), err, nil) + conn.Close() + cancel() + time.Sleep(reconnectDelay) + break + } else { + catcher.Error("Stream receive error", err, nil) + time.Sleep(reconnectDelay) + continue + } + } + + switch message := in.Payload.(type) { + case *BiDirectionalMessage_Config: + log.Printf("Received configuration update: %v", message.Config) + cnf = message.Config + } + } + } +} diff --git a/plugins/crowdStrike/config/config.pb.go b/plugins/crowdStrike/config/config.pb.go new file mode 100644 index 000000000..5c33be8d8 --- /dev/null +++ b/plugins/crowdStrike/config/config.pb.go @@ -0,0 +1,695 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.2 +// protoc v3.21.12 +// source: config.proto + +package config + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PluginType int32 + +const ( + PluginType_UNKNOWN PluginType = 0 + PluginType_AWS_IAM_USER PluginType = 1 + PluginType_AZURE PluginType = 2 + PluginType_BITDEFENDER PluginType = 3 + PluginType_GCP PluginType = 4 + PluginType_O365 PluginType = 5 + PluginType_SOC_AI PluginType = 6 + PluginType_SOPHOS PluginType = 7 + PluginType_CROWDSTRIKE PluginType = 8 +) + +// Enum value maps for PluginType. +var ( + PluginType_name = map[int32]string{ + 0: "UNKNOWN", + 1: "AWS_IAM_USER", + 2: "AZURE", + 3: "BITDEFENDER", + 4: "GCP", + 5: "O365", + 6: "SOC_AI", + 7: "SOPHOS", + 8: "CROWDSTRIKE", + } + PluginType_value = map[string]int32{ + "UNKNOWN": 0, + "AWS_IAM_USER": 1, + "AZURE": 2, + "BITDEFENDER": 3, + "GCP": 4, + "O365": 5, + "SOC_AI": 6, + "SOPHOS": 7, + "CROWDSTRIKE": 8, + } +) + +func (x PluginType) Enum() *PluginType { + p := new(PluginType) + *p = x + return p +} + +func (x PluginType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PluginType) Descriptor() protoreflect.EnumDescriptor { + return file_config_proto_enumTypes[0].Descriptor() +} + +func (PluginType) Type() protoreflect.EnumType { + return &file_config_proto_enumTypes[0] +} + +func (x PluginType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PluginType.Descriptor instead. +func (PluginType) EnumDescriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{0} +} + +type BiDirectionalMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *BiDirectionalMessage_PluginInit + // *BiDirectionalMessage_Config + Payload isBiDirectionalMessage_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BiDirectionalMessage) Reset() { + *x = BiDirectionalMessage{} + mi := &file_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BiDirectionalMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BiDirectionalMessage) ProtoMessage() {} + +func (x *BiDirectionalMessage) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BiDirectionalMessage.ProtoReflect.Descriptor instead. +func (*BiDirectionalMessage) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{0} +} + +func (x *BiDirectionalMessage) GetPayload() isBiDirectionalMessage_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *BiDirectionalMessage) GetPluginInit() *PluginInit { + if x != nil { + if x, ok := x.Payload.(*BiDirectionalMessage_PluginInit); ok { + return x.PluginInit + } + } + return nil +} + +func (x *BiDirectionalMessage) GetConfig() *ConfigurationSection { + if x != nil { + if x, ok := x.Payload.(*BiDirectionalMessage_Config); ok { + return x.Config + } + } + return nil +} + +type isBiDirectionalMessage_Payload interface { + isBiDirectionalMessage_Payload() +} + +type BiDirectionalMessage_PluginInit struct { + PluginInit *PluginInit `protobuf:"bytes,1,opt,name=plugin_init,json=pluginInit,proto3,oneof"` +} + +type BiDirectionalMessage_Config struct { + Config *ConfigurationSection `protobuf:"bytes,2,opt,name=config,proto3,oneof"` +} + +func (*BiDirectionalMessage_PluginInit) isBiDirectionalMessage_Payload() {} + +func (*BiDirectionalMessage_Config) isBiDirectionalMessage_Payload() {} + +type PluginInit struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type PluginType `protobuf:"varint,1,opt,name=type,proto3,enum=config.PluginType" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PluginInit) Reset() { + *x = PluginInit{} + mi := &file_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PluginInit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PluginInit) ProtoMessage() {} + +func (x *PluginInit) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PluginInit.ProtoReflect.Descriptor instead. +func (*PluginInit) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{1} +} + +func (x *PluginInit) GetType() PluginType { + if x != nil { + return x.Type + } + return PluginType_UNKNOWN +} + +type Configuration struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + GroupId int32 `protobuf:"varint,2,opt,name=groupId,proto3" json:"groupId,omitempty"` + ConfKey string `protobuf:"bytes,3,opt,name=confKey,proto3" json:"confKey,omitempty"` + ConfValue string `protobuf:"bytes,4,opt,name=confValue,proto3" json:"confValue,omitempty"` + ConfName string `protobuf:"bytes,5,opt,name=confName,proto3" json:"confName,omitempty"` + ConfDescription string `protobuf:"bytes,6,opt,name=confDescription,proto3" json:"confDescription,omitempty"` + ConfDataType string `protobuf:"bytes,7,opt,name=confDataType,proto3" json:"confDataType,omitempty"` + ConfRequired bool `protobuf:"varint,8,opt,name=confRequired,proto3" json:"confRequired,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Configuration) Reset() { + *x = Configuration{} + mi := &file_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Configuration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Configuration) ProtoMessage() {} + +func (x *Configuration) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Configuration.ProtoReflect.Descriptor instead. +func (*Configuration) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Configuration) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Configuration) GetGroupId() int32 { + if x != nil { + return x.GroupId + } + return 0 +} + +func (x *Configuration) GetConfKey() string { + if x != nil { + return x.ConfKey + } + return "" +} + +func (x *Configuration) GetConfValue() string { + if x != nil { + return x.ConfValue + } + return "" +} + +func (x *Configuration) GetConfName() string { + if x != nil { + return x.ConfName + } + return "" +} + +func (x *Configuration) GetConfDescription() string { + if x != nil { + return x.ConfDescription + } + return "" +} + +func (x *Configuration) GetConfDataType() string { + if x != nil { + return x.ConfDataType + } + return "" +} + +func (x *Configuration) GetConfRequired() bool { + if x != nil { + return x.ConfRequired + } + return false +} + +type ModuleGroup struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + ModuleId int32 `protobuf:"varint,2,opt,name=moduleId,proto3" json:"moduleId,omitempty"` + GroupName string `protobuf:"bytes,3,opt,name=groupName,proto3" json:"groupName,omitempty"` + GroupDescription string `protobuf:"bytes,4,opt,name=groupDescription,proto3" json:"groupDescription,omitempty"` + ModuleGroupConfigurations []*Configuration `protobuf:"bytes,5,rep,name=moduleGroupConfigurations,proto3" json:"moduleGroupConfigurations,omitempty"` + Collector string `protobuf:"bytes,6,opt,name=collector,proto3" json:"collector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModuleGroup) Reset() { + *x = ModuleGroup{} + mi := &file_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModuleGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModuleGroup) ProtoMessage() {} + +func (x *ModuleGroup) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModuleGroup.ProtoReflect.Descriptor instead. +func (*ModuleGroup) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{3} +} + +func (x *ModuleGroup) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *ModuleGroup) GetModuleId() int32 { + if x != nil { + return x.ModuleId + } + return 0 +} + +func (x *ModuleGroup) GetGroupName() string { + if x != nil { + return x.GroupName + } + return "" +} + +func (x *ModuleGroup) GetGroupDescription() string { + if x != nil { + return x.GroupDescription + } + return "" +} + +func (x *ModuleGroup) GetModuleGroupConfigurations() []*Configuration { + if x != nil { + return x.ModuleGroupConfigurations + } + return nil +} + +func (x *ModuleGroup) GetCollector() string { + if x != nil { + return x.Collector + } + return "" +} + +type ConfigurationSection struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + ServerId int32 `protobuf:"varint,2,opt,name=serverId,proto3" json:"serverId,omitempty"` + PrettyName string `protobuf:"bytes,3,opt,name=prettyName,proto3" json:"prettyName,omitempty"` + ModuleName string `protobuf:"bytes,4,opt,name=moduleName,proto3" json:"moduleName,omitempty"` + ModuleDescription string `protobuf:"bytes,5,opt,name=moduleDescription,proto3" json:"moduleDescription,omitempty"` + ModuleActive bool `protobuf:"varint,6,opt,name=moduleActive,proto3" json:"moduleActive,omitempty"` + ModuleIcon string `protobuf:"bytes,7,opt,name=moduleIcon,proto3" json:"moduleIcon,omitempty"` + ModuleCategory string `protobuf:"bytes,8,opt,name=moduleCategory,proto3" json:"moduleCategory,omitempty"` + LiteVersion bool `protobuf:"varint,9,opt,name=liteVersion,proto3" json:"liteVersion,omitempty"` + NeedsRestart bool `protobuf:"varint,10,opt,name=needsRestart,proto3" json:"needsRestart,omitempty"` + ModuleGroups []*ModuleGroup `protobuf:"bytes,11,rep,name=moduleGroups,proto3" json:"moduleGroups,omitempty"` + Activatable bool `protobuf:"varint,12,opt,name=activatable,proto3" json:"activatable,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigurationSection) Reset() { + *x = ConfigurationSection{} + mi := &file_config_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigurationSection) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigurationSection) ProtoMessage() {} + +func (x *ConfigurationSection) ProtoReflect() protoreflect.Message { + mi := &file_config_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigurationSection.ProtoReflect.Descriptor instead. +func (*ConfigurationSection) Descriptor() ([]byte, []int) { + return file_config_proto_rawDescGZIP(), []int{4} +} + +func (x *ConfigurationSection) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *ConfigurationSection) GetServerId() int32 { + if x != nil { + return x.ServerId + } + return 0 +} + +func (x *ConfigurationSection) GetPrettyName() string { + if x != nil { + return x.PrettyName + } + return "" +} + +func (x *ConfigurationSection) GetModuleName() string { + if x != nil { + return x.ModuleName + } + return "" +} + +func (x *ConfigurationSection) GetModuleDescription() string { + if x != nil { + return x.ModuleDescription + } + return "" +} + +func (x *ConfigurationSection) GetModuleActive() bool { + if x != nil { + return x.ModuleActive + } + return false +} + +func (x *ConfigurationSection) GetModuleIcon() string { + if x != nil { + return x.ModuleIcon + } + return "" +} + +func (x *ConfigurationSection) GetModuleCategory() string { + if x != nil { + return x.ModuleCategory + } + return "" +} + +func (x *ConfigurationSection) GetLiteVersion() bool { + if x != nil { + return x.LiteVersion + } + return false +} + +func (x *ConfigurationSection) GetNeedsRestart() bool { + if x != nil { + return x.NeedsRestart + } + return false +} + +func (x *ConfigurationSection) GetModuleGroups() []*ModuleGroup { + if x != nil { + return x.ModuleGroups + } + return nil +} + +func (x *ConfigurationSection) GetActivatable() bool { + if x != nil { + return x.Activatable + } + return false +} + +var File_config_proto protoreflect.FileDescriptor + +var file_config_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x90, 0x01, 0x0a, 0x14, 0x42, 0x69, 0x44, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, + 0x35, 0x0a, 0x0b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x69, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x36, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x09, + 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x34, 0x0a, 0x0a, 0x50, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x26, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, + 0xff, 0x01, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x18, 0x0a, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x07, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, + 0x6f, 0x6e, 0x66, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, + 0x6e, 0x66, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6e, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6e, 0x66, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x28, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x44, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, + 0x66, 0x44, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x44, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, + 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x66, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, + 0x64, 0x22, 0xf6, 0x01, 0x0a, 0x0b, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x47, 0x72, 0x6f, 0x75, + 0x70, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, + 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x49, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x08, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, + 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x10, 0x67, + 0x72, 0x6f, 0x75, 0x70, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x44, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x53, 0x0a, 0x19, 0x6d, 0x6f, 0x64, 0x75, 0x6c, + 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x19, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, + 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0xbd, 0x03, 0x0a, 0x14, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x12, + 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x1e, 0x0a, 0x0a, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x2c, 0x0a, 0x11, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6d, 0x6f, 0x64, 0x75, + 0x6c, 0x65, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, + 0x0c, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x41, 0x63, 0x74, 0x69, 0x76, + 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x49, 0x63, 0x6f, 0x6e, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x49, 0x63, 0x6f, + 0x6e, 0x12, 0x26, 0x0a, 0x0e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, + 0x6f, 0x72, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6d, 0x6f, 0x64, 0x75, 0x6c, + 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x6c, 0x69, 0x74, + 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, + 0x6c, 0x69, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x6e, + 0x65, 0x65, 0x64, 0x73, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0c, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, + 0x37, 0x0a, 0x0c, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, + 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4d, + 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0c, 0x6d, 0x6f, 0x64, 0x75, + 0x6c, 0x65, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69, + 0x76, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x61, + 0x63, 0x74, 0x69, 0x76, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x2a, 0x83, 0x01, 0x0a, 0x0a, 0x50, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, + 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x41, 0x57, 0x53, 0x5f, 0x49, 0x41, + 0x4d, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x5a, 0x55, 0x52, + 0x45, 0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x42, 0x49, 0x54, 0x44, 0x45, 0x46, 0x45, 0x4e, 0x44, + 0x45, 0x52, 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x47, 0x43, 0x50, 0x10, 0x04, 0x12, 0x08, 0x0a, + 0x04, 0x4f, 0x33, 0x36, 0x35, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x4f, 0x43, 0x5f, 0x41, + 0x49, 0x10, 0x06, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x4f, 0x50, 0x48, 0x4f, 0x53, 0x10, 0x07, 0x12, + 0x0f, 0x0a, 0x0b, 0x43, 0x52, 0x4f, 0x57, 0x44, 0x53, 0x54, 0x52, 0x49, 0x4b, 0x45, 0x10, 0x08, + 0x32, 0x5f, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x4e, 0x0a, 0x0c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x42, 0x69, 0x44, 0x69, 0x72, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, + 0x1c, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x42, 0x69, 0x44, 0x69, 0x72, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x28, 0x01, 0x30, + 0x01, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x75, 0x74, 0x6d, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x55, 0x54, 0x4d, 0x53, 0x74, 0x61, 0x63, + 0x6b, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, + 0x73, 0x2d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_config_proto_rawDescOnce sync.Once + file_config_proto_rawDescData = file_config_proto_rawDesc +) + +func file_config_proto_rawDescGZIP() []byte { + file_config_proto_rawDescOnce.Do(func() { + file_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_config_proto_rawDescData) + }) + return file_config_proto_rawDescData +} + +var file_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_config_proto_goTypes = []any{ + (PluginType)(0), // 0: config.PluginType + (*BiDirectionalMessage)(nil), // 1: config.BiDirectionalMessage + (*PluginInit)(nil), // 2: config.PluginInit + (*Configuration)(nil), // 3: config.Configuration + (*ModuleGroup)(nil), // 4: config.ModuleGroup + (*ConfigurationSection)(nil), // 5: config.ConfigurationSection +} +var file_config_proto_depIdxs = []int32{ + 2, // 0: config.BiDirectionalMessage.plugin_init:type_name -> config.PluginInit + 5, // 1: config.BiDirectionalMessage.config:type_name -> config.ConfigurationSection + 0, // 2: config.PluginInit.type:type_name -> config.PluginType + 3, // 3: config.ModuleGroup.moduleGroupConfigurations:type_name -> config.Configuration + 4, // 4: config.ConfigurationSection.moduleGroups:type_name -> config.ModuleGroup + 1, // 5: config.ConfigService.StreamConfig:input_type -> config.BiDirectionalMessage + 1, // 6: config.ConfigService.StreamConfig:output_type -> config.BiDirectionalMessage + 6, // [6:7] is the sub-list for method output_type + 5, // [5:6] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_config_proto_init() } +func file_config_proto_init() { + if File_config_proto != nil { + return + } + file_config_proto_msgTypes[0].OneofWrappers = []any{ + (*BiDirectionalMessage_PluginInit)(nil), + (*BiDirectionalMessage_Config)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_config_proto_rawDesc, + NumEnums: 1, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_config_proto_goTypes, + DependencyIndexes: file_config_proto_depIdxs, + EnumInfos: file_config_proto_enumTypes, + MessageInfos: file_config_proto_msgTypes, + }.Build() + File_config_proto = out.File + file_config_proto_rawDesc = nil + file_config_proto_goTypes = nil + file_config_proto_depIdxs = nil +} diff --git a/plugins/crowdStrike/config/config_grpc.pb.go b/plugins/crowdStrike/config/config_grpc.pb.go new file mode 100644 index 000000000..7f9e6c617 --- /dev/null +++ b/plugins/crowdStrike/config/config_grpc.pb.go @@ -0,0 +1,115 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v3.21.12 +// source: config.proto + +package config + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ConfigService_StreamConfig_FullMethodName = "/config.ConfigService/StreamConfig" +) + +// ConfigServiceClient is the client API for ConfigService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ConfigServiceClient interface { + StreamConfig(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[BiDirectionalMessage, BiDirectionalMessage], error) +} + +type configServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewConfigServiceClient(cc grpc.ClientConnInterface) ConfigServiceClient { + return &configServiceClient{cc} +} + +func (c *configServiceClient) StreamConfig(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[BiDirectionalMessage, BiDirectionalMessage], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &ConfigService_ServiceDesc.Streams[0], ConfigService_StreamConfig_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[BiDirectionalMessage, BiDirectionalMessage]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ConfigService_StreamConfigClient = grpc.BidiStreamingClient[BiDirectionalMessage, BiDirectionalMessage] + +// ConfigServiceServer is the server API for ConfigService service. +// All implementations must embed UnimplementedConfigServiceServer +// for forward compatibility. +type ConfigServiceServer interface { + StreamConfig(grpc.BidiStreamingServer[BiDirectionalMessage, BiDirectionalMessage]) error + mustEmbedUnimplementedConfigServiceServer() +} + +// UnimplementedConfigServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedConfigServiceServer struct{} + +func (UnimplementedConfigServiceServer) StreamConfig(grpc.BidiStreamingServer[BiDirectionalMessage, BiDirectionalMessage]) error { + return status.Errorf(codes.Unimplemented, "method StreamConfig not implemented") +} +func (UnimplementedConfigServiceServer) mustEmbedUnimplementedConfigServiceServer() {} +func (UnimplementedConfigServiceServer) testEmbeddedByValue() {} + +// UnsafeConfigServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ConfigServiceServer will +// result in compilation errors. +type UnsafeConfigServiceServer interface { + mustEmbedUnimplementedConfigServiceServer() +} + +func RegisterConfigServiceServer(s grpc.ServiceRegistrar, srv ConfigServiceServer) { + // If the following call pancis, it indicates UnimplementedConfigServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ConfigService_ServiceDesc, srv) +} + +func _ConfigService_StreamConfig_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(ConfigServiceServer).StreamConfig(&grpc.GenericServerStream[BiDirectionalMessage, BiDirectionalMessage]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type ConfigService_StreamConfigServer = grpc.BidiStreamingServer[BiDirectionalMessage, BiDirectionalMessage] + +// ConfigService_ServiceDesc is the grpc.ServiceDesc for ConfigService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ConfigService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "config.ConfigService", + HandlerType: (*ConfigServiceServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "StreamConfig", + Handler: _ConfigService_StreamConfig_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "config.proto", +} diff --git a/plugins/crowdStrike/go.mod b/plugins/crowdStrike/go.mod new file mode 100644 index 000000000..23d78a06f --- /dev/null +++ b/plugins/crowdStrike/go.mod @@ -0,0 +1,94 @@ +module github.com/utmstack/UTMStack/plugins/crowdStrike + +go 1.24.6 + +require ( + github.com/crowdstrike/gofalcon v0.16.0 + github.com/google/uuid v1.6.0 + github.com/threatwinds/go-sdk v1.0.45 + google.golang.org/grpc v1.76.0 + google.golang.org/protobuf v1.36.10 +) + +require ( + cel.dev/expr v0.24.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.1 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.11.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.24.0 // indirect + github.com/go-openapi/errors v0.22.3 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/loads v0.23.1 // indirect + github.com/go-openapi/runtime v0.29.0 // indirect + github.com/go-openapi/spec v0.22.0 // indirect + github.com/go-openapi/strfmt v0.24.0 // indirect + github.com/go-openapi/swag v0.25.1 // indirect + github.com/go-openapi/swag/cmdutils v0.25.1 // indirect + github.com/go-openapi/swag/conv v0.25.1 // indirect + github.com/go-openapi/swag/fileutils v0.25.1 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/swag/jsonutils v0.25.1 // indirect + github.com/go-openapi/swag/loading v0.25.1 // indirect + github.com/go-openapi/swag/mangling v0.25.1 // indirect + github.com/go-openapi/swag/netutils v0.25.1 // indirect + github.com/go-openapi/swag/stringutils v0.25.1 // indirect + github.com/go-openapi/swag/typeutils v0.25.1 // indirect + github.com/go-openapi/swag/yamlutils v0.25.1 // indirect + github.com/go-openapi/validate v0.25.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/google/cel-go v0.26.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.mongodb.org/mongo-driver v1.17.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/mock v0.5.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/plugins/crowdStrike/go.sum b/plugins/crowdStrike/go.sum new file mode 100644 index 000000000..78e7d37fa --- /dev/null +++ b/plugins/crowdStrike/go.sum @@ -0,0 +1,216 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= +github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/crowdstrike/gofalcon v0.16.0 h1:qwJdoYZ2OXEzArZZNrIwhOO2aFr1uL6SbxprWLjsbO4= +github.com/crowdstrike/gofalcon v0.16.0/go.mod h1:a12GB+md+hRSgVCb3Pv6CakeTIsDIUCIVWRlJelIhY0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/analysis v0.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA= +github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw= +github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs= +github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= +github.com/go-openapi/loads v0.23.1 h1:H8A0dX2KDHxDzc797h0+uiCZ5kwE2+VojaQVaTlXvS0= +github.com/go-openapi/loads v0.23.1/go.mod h1:hZSXkyACCWzWPQqizAv/Ye0yhi2zzHwMmoXQ6YQml44= +github.com/go-openapi/runtime v0.29.0 h1:Y7iDTFarS9XaFQ+fA+lBLngMwH6nYfqig1G+pHxMRO0= +github.com/go-openapi/runtime v0.29.0/go.mod h1:52HOkEmLL/fE4Pg3Kf9nxc9fYQn0UsIWyGjGIJE9dkg= +github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw= +github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0= +github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc= +github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4= +github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8= +github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo= +github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY= +github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= +github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= +github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU= +github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= +github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= +github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= +github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= +github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= +github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= +github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4= +github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE= +github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= +github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= +github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= +github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= +github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= +github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= +github.com/go-openapi/validate v0.25.0 h1:JD9eGX81hDTjoY3WOzh6WqxVBVl7xjsLnvDo1GL5WPU= +github.com/go-openapi/validate v0.25.0/go.mod h1:SUY7vKrN5FiwK6LyvSwKjDfLNirSfWwHNgxd2l29Mmw= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/threatwinds/go-sdk v1.0.45 h1:KZ3s3HviNRrOkg5EqjFnoauANFFzTqjNFyshPLY2SoI= +github.com/threatwinds/go-sdk v1.0.45/go.mod h1:tcWn6r6vqID/W/nL3UKfc5NafA3V/cSkiLvfJnwB58c= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= +go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA= +golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff h1:8Zg5TdmcbU8A7CXGjGXF1Slqu/nIFCRaR3S5gT2plIA= +google.golang.org/genproto/googleapis/api v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:dbWfpVPvW/RqafStmRWBUpMN14puDezDMHxNYiRfQu0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/plugins/crowdStrike/main.go b/plugins/crowdStrike/main.go new file mode 100644 index 000000000..c0fdd1e99 --- /dev/null +++ b/plugins/crowdStrike/main.go @@ -0,0 +1,233 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "runtime" + "strings" + "sync" + "time" + + "github.com/crowdstrike/gofalcon/falcon" + "github.com/crowdstrike/gofalcon/falcon/client" + "github.com/crowdstrike/gofalcon/falcon/client/event_streams" + "github.com/crowdstrike/gofalcon/falcon/models" + "github.com/google/uuid" + "github.com/threatwinds/go-sdk/catcher" + "github.com/threatwinds/go-sdk/plugins" + "github.com/utmstack/UTMStack/plugins/crowdStrike/config" +) + +const ( + defaultTenant = "ce66672c-e36d-4761-a8c8-90058fee1a24" + urlCheckConnection = "https://falcon.crowdstrike.com" + wait = 1 * time.Second +) + +func main() { + mode := plugins.GetCfg().Env.Mode + if mode != "manager" { + return + } + + go config.StartConfigurationSystem() + + for t := 0; t < 2*runtime.NumCPU(); t++ { + go func() { + plugins.SendLogsFromChannel() + }() + } + + delay := 5 * time.Minute + ticker := time.NewTicker(delay) + defer ticker.Stop() + + for range ticker.C { + if err := connectionChecker(urlCheckConnection); err != nil { + _ = catcher.Error("External connection failure detected: %v", err, nil) + } + + moduleConfig := config.GetConfig() + if moduleConfig != nil && moduleConfig.ModuleActive { + var wg sync.WaitGroup + wg.Add(len(moduleConfig.ModuleGroups)) + for _, grp := range moduleConfig.ModuleGroups { + go func(group *config.ModuleGroup) { + defer wg.Done() + var invalid bool + for _, c := range group.ModuleGroupConfigurations { + if strings.TrimSpace(c.ConfValue) == "" { + invalid = true + break + } + } + + if !invalid { + pullCrowdStrikeEvents(group) + } + }(grp) + } + wg.Wait() + } + } +} + +func pullCrowdStrikeEvents(group *config.ModuleGroup) { + processor := getCrowdStrikeProcessor(group) + + events, err := processor.getEvents() + if err != nil { + _ = catcher.Error("cannot get CrowdStrike events", err, map[string]any{ + "group": group.GroupName, + }) + return + } + + for _, event := range events { + _ = plugins.EnqueueLog(&plugins.Log{ + Id: uuid.NewString(), + TenantId: defaultTenant, + DataType: "crowdstrike", + DataSource: group.GroupName, + Timestamp: time.Now().UTC().Format(time.RFC3339Nano), + Raw: event, + }) + } +} + +type CrowdStrikeProcessor struct { + ClientID string + ClientSecret string + Cloud string + AppName string +} + +func getCrowdStrikeProcessor(group *config.ModuleGroup) CrowdStrikeProcessor { + processor := CrowdStrikeProcessor{} + + for _, cnf := range group.ModuleGroupConfigurations { + switch cnf.ConfKey { + case "client_id": + processor.ClientID = cnf.ConfValue + case "client_secret": + processor.ClientSecret = cnf.ConfValue + case "cloud": + processor.Cloud = cnf.ConfValue + case "app_name": + processor.AppName = cnf.ConfValue + } + } + return processor +} + +func (p *CrowdStrikeProcessor) createClient() (*client.CrowdStrikeAPISpecification, error) { + if p.ClientID == "" || p.ClientSecret == "" { + return nil, catcher.Error("cannot create CrowdStrike client", + errors.New("client ID or client secret is empty"), nil) + } + + client, err := falcon.NewClient(&falcon.ApiConfig{ + ClientId: p.ClientID, + ClientSecret: p.ClientSecret, + Cloud: falcon.Cloud(p.Cloud), + Context: context.Background(), + }) + if err != nil { + return nil, catcher.Error("cannot create CrowdStrike client", err, nil) + } + + return client, nil +} + +func (p *CrowdStrikeProcessor) getEvents() ([]string, error) { + client, err := p.createClient() + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + json := "json" + response, err := client.EventStreams.ListAvailableStreamsOAuth2( + &event_streams.ListAvailableStreamsOAuth2Params{ + AppID: p.AppName, + Format: &json, + Context: ctx, + }, + ) + if err != nil { + return nil, catcher.Error("cannot list available streams", err, nil) + } + + if err = falcon.AssertNoError(response.Payload.Errors); err != nil { + return nil, catcher.Error("CrowdStrike API error", err, nil) + } + + availableStreams := response.Payload.Resources + if len(availableStreams) == 0 { + _ = catcher.Error("no available streams found", nil, map[string]any{ + "app_id": p.AppName, + }) + return []string{}, nil + } + + var events []string + for _, availableStream := range availableStreams { + streamEvents, err := p.getStreamEvents(ctx, client, availableStream) + if err != nil { + _ = catcher.Error("cannot get stream events", err, map[string]any{ + "stream": availableStream, + }) + continue + } + events = append(events, streamEvents...) + } + + return events, nil +} + +func (p *CrowdStrikeProcessor) getStreamEvents(ctx context.Context, client *client.CrowdStrikeAPISpecification, availableStream interface{}) ([]string, error) { + stream_v2, ok := availableStream.(*models.MainAvailableStreamV2) + if !ok { + return nil, catcher.Error("invalid stream type", fmt.Errorf("cannot convert to MainAvailableStreamV2"), nil) + } + + stream, err := falcon.NewStream(ctx, client, p.AppName, stream_v2, 0) + if err != nil { + return nil, catcher.Error("cannot create stream", err, nil) + } + defer stream.Close() + + var events []string + timeout := time.NewTimer(30 * time.Second) + defer timeout.Stop() + + for { + select { + case err := <-stream.Errors: + if err.Fatal { + return events, catcher.Error("fatal stream error", err.Err, nil) + } else { + _ = catcher.Error("stream error", err.Err, nil) + } + case event := <-stream.Events: + eventJSON, err := json.Marshal(event) + if err != nil { + _ = catcher.Error("cannot marshal event", err, nil) + continue + } + events = append(events, string(eventJSON)) + + if len(events) >= 100 { + return events, nil + } + case <-timeout.C: + return events, nil + case <-ctx.Done(): + return events, nil + } + } +} diff --git a/plugins/events/main.go b/plugins/events/main.go index 69bd15f6c..005e82d9b 100644 --- a/plugins/events/main.go +++ b/plugins/events/main.go @@ -118,9 +118,6 @@ func main() { } func (p *analysisServer) Analyze(event *plugins.Event, _ grpc.ServerStreamingServer[plugins.Alert]) error { - m := utils.NewMeter("Enqueue log for OpenSearch") - defer m.Elapsed("finished") - jLog, err := utils.ToString(event) if err != nil { return catcher.Error("cannot convert event to json", err, nil) diff --git a/plugins/events/queue.go b/plugins/events/queue.go index 6e005a608..f857e2f36 100644 --- a/plugins/events/queue.go +++ b/plugins/events/queue.go @@ -3,13 +3,13 @@ package main import ( "context" "fmt" - "github.com/threatwinds/go-sdk/catcher" - "github.com/threatwinds/go-sdk/plugins" - "github.com/threatwinds/go-sdk/utils" "runtime" "sync" "time" + "github.com/threatwinds/go-sdk/catcher" + "github.com/threatwinds/go-sdk/plugins" + "github.com/threatwinds/go-sdk/opensearch" "github.com/tidwall/gjson" ) @@ -60,16 +60,6 @@ func startQueue() { numCPU := runtime.NumCPU() * 2 for i := 0; i < numCPU; i++ { go func() { - ndM := utils.NewMeter("NDJson", utils.MeterOptions{ - LogSlow: true, - SlowThreshold: 20 * time.Millisecond, - }) - - bulkM := utils.NewMeter("Bulk", utils.MeterOptions{ - LogSlow: true, - SlowThreshold: 1 * time.Second, - }) - var ndMutex = &sync.Mutex{} var nd = make([]opensearch.BulkItem, 0, 10) @@ -81,7 +71,6 @@ func startQueue() { } ndMutex.Lock() - bulkM.Reset() err := opensearch.Bulk(context.Background(), nd) if err != nil { @@ -91,23 +80,17 @@ func startQueue() { nd = make([]opensearch.BulkItem, 0, 10) ndMutex.Unlock() - bulkM.Elapsed("bulk sent") } }() for { l := <-logs - ndM.Reset() - dataType := gjson.Get(l, "dataType").String() id := gjson.Get(l, "id").String() index := opensearch.BuildCurrentIndex("v11", "log", dataType) - ndM.Elapsed("get index and id") - ndMutex.Lock() - ndM.Reset() nd = append(nd, opensearch.BulkItem{ Index: index, @@ -117,7 +100,6 @@ func startQueue() { }) ndMutex.Unlock() - ndM.Elapsed("append to NDJson list") } }() } diff --git a/plugins/gcp/main.go b/plugins/gcp/main.go index 40c0fefbc..2f8f2103c 100644 --- a/plugins/gcp/main.go +++ b/plugins/gcp/main.go @@ -102,7 +102,7 @@ func (g *GroupModule) PullLogs() { }) if err != nil { - _ = catcher.Error("failed to receive message", err, map[string]any{}) + _ = catcher.Error("failed to receive message", err, nil) time.Sleep(5 * time.Second) continue } diff --git a/plugins/geolocation/main.go b/plugins/geolocation/main.go index 51b6b6271..c0fe1cbb7 100644 --- a/plugins/geolocation/main.go +++ b/plugins/geolocation/main.go @@ -122,9 +122,6 @@ func main() { } func (p *parsingServer) ParseLog(_ context.Context, transform *plugins.Transform) (*plugins.Draft, error) { - m := utils.NewMeter("ParseLog") - defer m.Elapsed("finished") - source, ok := transform.Step.Dynamic.Params["source"] if !ok { return transform.Draft, catcher.Error("'source' parameter required", nil, nil) diff --git a/plugins/inputs/handlers.go b/plugins/inputs/handlers.go index 3ddb54539..80a6ad9de 100644 --- a/plugins/inputs/handlers.go +++ b/plugins/inputs/handlers.go @@ -3,13 +3,14 @@ package main import ( "bytes" "crypto/tls" - "github.com/threatwinds/go-sdk/catcher" - "github.com/threatwinds/go-sdk/plugins" - "github.com/threatwinds/go-sdk/utils" "net" "net/http" "time" + "github.com/threatwinds/go-sdk/catcher" + "github.com/threatwinds/go-sdk/plugins" + "github.com/threatwinds/go-sdk/utils" + "github.com/gin-gonic/gin" "github.com/google/uuid" "google.golang.org/grpc" @@ -80,7 +81,7 @@ func Log(c *gin.Context) { _, err := buf.ReadFrom(c.Request.Body) if err != nil { - e := catcher.Error("failed to read request body", err, map[string]any{}) + e := catcher.Error("failed to read request body", err, nil) e.GinError(c) return } @@ -91,7 +92,7 @@ func Log(c *gin.Context) { err = utils.ToObject(&body, l) if err != nil { - e := catcher.Error("failed to parse log", err, map[string]any{}) + e := catcher.Error("failed to parse log", err, nil) e.GinError(c) return } @@ -130,7 +131,7 @@ func GitHub(c *gin.Context) { buf := new(bytes.Buffer) _, err := buf.ReadFrom(c.Request.Body) if err != nil { - e := catcher.Error("failed to read request body", err, map[string]any{}) + e := catcher.Error("failed to read request body", err, nil) e.GinError(c) return } diff --git a/plugins/inputs/output.go b/plugins/inputs/output.go index c6d8eb6f4..c8a89e92c 100644 --- a/plugins/inputs/output.go +++ b/plugins/inputs/output.go @@ -36,7 +36,7 @@ func sendLog() { conn, err = grpc.NewClient(fmt.Sprintf("unix://%s", socketFile), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { - _ = catcher.Error("failed to connect to engine server", err, map[string]any{}) + _ = catcher.Error("failed to connect to engine server", err, nil) time.Sleep(5 * time.Second) continue } @@ -45,7 +45,7 @@ func sendLog() { inputClient, err = client.Input(context.Background()) if err != nil { - _ = catcher.Error("failed to create input client", err, map[string]any{}) + _ = catcher.Error("failed to create input client", err, nil) if conn != nil { _ = conn.Close() } @@ -67,7 +67,7 @@ func sendLog() { err := inputClient.Send(l) if err != nil { - _ = catcher.Error("failed to send log", err, map[string]any{}) + _ = catcher.Error("failed to send log", err, nil) restart <- true return } @@ -78,7 +78,7 @@ func sendLog() { for { _, err = inputClient.Recv() if err != nil { - _ = catcher.Error("failed to receive ack", err, map[string]any{}) + _ = catcher.Error("failed to receive ack", err, nil) restart <- true return } diff --git a/plugins/modules-config/config/config.go b/plugins/modules-config/config/config.go index 28780d84b..0a1576896 100644 --- a/plugins/modules-config/config/config.go +++ b/plugins/modules-config/config/config.go @@ -128,6 +128,8 @@ func (s *ConfigServer) NotifyUpdate(moduleName string, section *ConfigurationSec pluginType = PluginType_SOC_AI case "SOPHOS": pluginType = PluginType_SOPHOS + case "CROWDSTRIKE": + pluginType = PluginType_CROWDSTRIKE default: _ = catcher.Error("unknown module name", fmt.Errorf("module: %s", moduleName), nil) return @@ -165,6 +167,7 @@ func (s *ConfigServer) SyncConfigs(backend string, internalKey string) { "O365": PluginType_O365, "SOC_AI": PluginType_SOC_AI, "SOPHOS": PluginType_SOPHOS, + "CROWDSTRIKE": PluginType_CROWDSTRIKE, } for name, t := range AllModules { diff --git a/plugins/modules-config/config/config.pb.go b/plugins/modules-config/config/config.pb.go index 8f1713887..5c33be8d8 100644 --- a/plugins/modules-config/config/config.pb.go +++ b/plugins/modules-config/config/config.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 -// protoc v5.29.3 +// protoc-gen-go v1.36.2 +// protoc v3.21.12 // source: config.proto package config @@ -11,7 +11,6 @@ import ( protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" - unsafe "unsafe" ) const ( @@ -32,6 +31,7 @@ const ( PluginType_O365 PluginType = 5 PluginType_SOC_AI PluginType = 6 PluginType_SOPHOS PluginType = 7 + PluginType_CROWDSTRIKE PluginType = 8 ) // Enum value maps for PluginType. @@ -45,6 +45,7 @@ var ( 5: "O365", 6: "SOC_AI", 7: "SOPHOS", + 8: "CROWDSTRIKE", } PluginType_value = map[string]int32{ "UNKNOWN": 0, @@ -55,6 +56,7 @@ var ( "O365": 5, "SOC_AI": 6, "SOPHOS": 7, + "CROWDSTRIKE": 8, } ) @@ -529,76 +531,110 @@ func (x *ConfigurationSection) GetActivatable() bool { var File_config_proto protoreflect.FileDescriptor -const file_config_proto_rawDesc = "" + - "\n" + - "\fconfig.proto\x12\x06config\"\x90\x01\n" + - "\x14BiDirectionalMessage\x125\n" + - "\vplugin_init\x18\x01 \x01(\v2\x12.config.PluginInitH\x00R\n" + - "pluginInit\x126\n" + - "\x06config\x18\x02 \x01(\v2\x1c.config.ConfigurationSectionH\x00R\x06configB\t\n" + - "\apayload\"4\n" + - "\n" + - "PluginInit\x12&\n" + - "\x04type\x18\x01 \x01(\x0e2\x12.config.PluginTypeR\x04type\"\xff\x01\n" + - "\rConfiguration\x12\x0e\n" + - "\x02id\x18\x01 \x01(\x05R\x02id\x12\x18\n" + - "\agroupId\x18\x02 \x01(\x05R\agroupId\x12\x18\n" + - "\aconfKey\x18\x03 \x01(\tR\aconfKey\x12\x1c\n" + - "\tconfValue\x18\x04 \x01(\tR\tconfValue\x12\x1a\n" + - "\bconfName\x18\x05 \x01(\tR\bconfName\x12(\n" + - "\x0fconfDescription\x18\x06 \x01(\tR\x0fconfDescription\x12\"\n" + - "\fconfDataType\x18\a \x01(\tR\fconfDataType\x12\"\n" + - "\fconfRequired\x18\b \x01(\bR\fconfRequired\"\xf6\x01\n" + - "\vModuleGroup\x12\x0e\n" + - "\x02id\x18\x01 \x01(\x05R\x02id\x12\x1a\n" + - "\bmoduleId\x18\x02 \x01(\x05R\bmoduleId\x12\x1c\n" + - "\tgroupName\x18\x03 \x01(\tR\tgroupName\x12*\n" + - "\x10groupDescription\x18\x04 \x01(\tR\x10groupDescription\x12S\n" + - "\x19moduleGroupConfigurations\x18\x05 \x03(\v2\x15.config.ConfigurationR\x19moduleGroupConfigurations\x12\x1c\n" + - "\tcollector\x18\x06 \x01(\tR\tcollector\"\xbd\x03\n" + - "\x14ConfigurationSection\x12\x0e\n" + - "\x02id\x18\x01 \x01(\x05R\x02id\x12\x1a\n" + - "\bserverId\x18\x02 \x01(\x05R\bserverId\x12\x1e\n" + - "\n" + - "prettyName\x18\x03 \x01(\tR\n" + - "prettyName\x12\x1e\n" + - "\n" + - "moduleName\x18\x04 \x01(\tR\n" + - "moduleName\x12,\n" + - "\x11moduleDescription\x18\x05 \x01(\tR\x11moduleDescription\x12\"\n" + - "\fmoduleActive\x18\x06 \x01(\bR\fmoduleActive\x12\x1e\n" + - "\n" + - "moduleIcon\x18\a \x01(\tR\n" + - "moduleIcon\x12&\n" + - "\x0emoduleCategory\x18\b \x01(\tR\x0emoduleCategory\x12 \n" + - "\vliteVersion\x18\t \x01(\bR\vliteVersion\x12\"\n" + - "\fneedsRestart\x18\n" + - " \x01(\bR\fneedsRestart\x127\n" + - "\fmoduleGroups\x18\v \x03(\v2\x13.config.ModuleGroupR\fmoduleGroups\x12 \n" + - "\vactivatable\x18\f \x01(\bR\vactivatable*r\n" + - "\n" + - "PluginType\x12\v\n" + - "\aUNKNOWN\x10\x00\x12\x10\n" + - "\fAWS_IAM_USER\x10\x01\x12\t\n" + - "\x05AZURE\x10\x02\x12\x0f\n" + - "\vBITDEFENDER\x10\x03\x12\a\n" + - "\x03GCP\x10\x04\x12\b\n" + - "\x04O365\x10\x05\x12\n" + - "\n" + - "\x06SOC_AI\x10\x06\x12\n" + - "\n" + - "\x06SOPHOS\x10\a2_\n" + - "\rConfigService\x12N\n" + - "\fStreamConfig\x12\x1c.config.BiDirectionalMessage\x1a\x1c.config.BiDirectionalMessage(\x010\x01B 0 { for _, detail := range details { rawDetail, err := json.Marshal(detail) if err != nil { - _ = catcher.Error("error marshalling content details", err, map[string]any{}) + _ = catcher.Error("error marshalling content details", err, nil) continue } logs = append(logs, string(rawDetail)) diff --git a/plugins/sophos/main.go b/plugins/sophos/main.go index 8d3534fa6..7896dcfaf 100644 --- a/plugins/sophos/main.go +++ b/plugins/sophos/main.go @@ -91,7 +91,7 @@ func pull(startTime time.Time, group *config.ModuleGroup) { agent := getSophosCentralProcessor(group) logs, newNextKey, err := agent.getLogs(startTime.Unix(), prevKey) if err != nil { - _ = catcher.Error("error getting logs", err, map[string]any{}) + _ = catcher.Error("error getting logs", err, nil) return } @@ -384,7 +384,7 @@ func (p *SophosCentralProcessor) getLogs(fromTime int64, nextKey string) ([]stri for _, item := range response.Items { jsonItem, err := json.Marshal(item) if err != nil { - _ = catcher.Error("error marshalling content details", err, map[string]any{}) + _ = catcher.Error("error marshalling content details", err, nil) continue } logs = append(logs, string(jsonItem)) diff --git a/plugins/stats/main.go b/plugins/stats/main.go index 1d98f15dc..0e5f0742d 100644 --- a/plugins/stats/main.go +++ b/plugins/stats/main.go @@ -173,7 +173,7 @@ func (p *notificationServer) Notify(_ context.Context, msg *plugins.Message) (*e err := json.Unmarshal(messageBytes, &pMsg) if err != nil { - return &emptypb.Empty{}, catcher.Error("cannot unmarshal message", err, map[string]any{}) + return &emptypb.Empty{}, catcher.Error("cannot unmarshal message", err, nil) } statisticsQueue <- map[string]plugins.DataProcessingMessage{msg.Topic: pMsg} diff --git a/user-auditor/pom.xml b/user-auditor/pom.xml index b632a6a93..84e566434 100644 --- a/user-auditor/pom.xml +++ b/user-auditor/pom.xml @@ -1,22 +1,28 @@ - 4.0.0 + org.springframework.boot spring-boot-starter-parent 2.7.14 + com.utmstack user-auditor 1.0.0 user-auditor userAuditor + 11 + + org.springframework.boot spring-boot-starter-data-jpa @@ -27,29 +33,41 @@ + org.springframework.boot spring-boot-starter-web + + org.postgresql postgresql 42.7.2 + + org.projectlombok lombok - true + 1.18.30 + provided + + org.liquibase liquibase-core + + com.utmstack opensearch-connector 1.0.0 + + jakarta.json jakarta.json-api @@ -59,6 +77,19 @@ + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 11 + 11 + UTF-8 + + + + org.springframework.boot spring-boot-maven-plugin diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/event/Event.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/event/Event.java new file mode 100644 index 000000000..82bc16993 --- /dev/null +++ b/user-auditor/src/main/java/com/utmstack/userauditor/model/event/Event.java @@ -0,0 +1,31 @@ +package com.utmstack.userauditor.model.event; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.Getter; + +import java.util.Map; + + +@Data +public class Event { + private String id; + @JsonProperty("@timestamp") + private String timestamp; + private String deviceTime; + private String dataType; + private String dataSource; + private String tenantId; + private String tenantName; + private String raw; + private Map log; + private Side target; + private Side origin; + private String protocol; + private String connectionStatus; + private int statusCode; + private String actionResult; + private String action; + private String command; + private String severity; +} \ No newline at end of file diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/event/Geolocation.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/event/Geolocation.java new file mode 100644 index 000000000..a3fb033c0 --- /dev/null +++ b/user-auditor/src/main/java/com/utmstack/userauditor/model/event/Geolocation.java @@ -0,0 +1,13 @@ +package com.utmstack.userauditor.model.event; + +public class Geolocation { + private String country; + private String city; + private double latitude; + private double longitude; + private int asn; + private String aso; + private String countryCode; + private int accuracy; +} + diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/event/Side.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/event/Side.java new file mode 100644 index 000000000..6c2d11bf9 --- /dev/null +++ b/user-auditor/src/main/java/com/utmstack/userauditor/model/event/Side.java @@ -0,0 +1,33 @@ +package com.utmstack.userauditor.model.event; + +import lombok.Data; + +@Data +public class Side { + private long bytesSent; + private long bytesReceived; + private long packagesSent; + private long packagesReceived; + private int connections; + private double usedCpuPercent; + private double usedMemPercent; + private int totalCpuUnits; + private long totalMem; + private String ip; + private String host; + private String user; + private String group; + private int port; + private String domain; + private String fqdn; + private String mac; + private String process; + private Geolocation geolocation; + private String file; + private String path; + private String hash; + private String url; + private String email; + // getters and setters +} + diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/UserEvent.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/event/UserEvent.java similarity index 79% rename from user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/UserEvent.java rename to user-auditor/src/main/java/com/utmstack/userauditor/model/event/UserEvent.java index 5eb06b4ca..ab78b65f5 100644 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/UserEvent.java +++ b/user-auditor/src/main/java/com/utmstack/userauditor/model/event/UserEvent.java @@ -1,12 +1,10 @@ -package com.utmstack.userauditor.model.winevent; +package com.utmstack.userauditor.model.event; import lombok.Builder; import lombok.Getter; import lombok.Setter; import org.opensearch.client.opensearch._types.aggregations.TopHitsAggregate; -import java.util.List; - @Builder @Getter @Setter diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Agent.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Agent.java deleted file mode 100644 index 16595412c..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Agent.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -// import com.fasterxml.jackson.databind.ObjectMapper; // version 2.11.1 -// import com.fasterxml.jackson.annotation.JsonProperty; // version 2.11.1 -/* ObjectMapper om = new ObjectMapper(); -Root root = om.readValue(myJsonString, Root.class); */ -public class Agent { - public String ephemeral_id; - public String id; - public String type; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Beat.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Beat.java deleted file mode 100644 index 7289e327d..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Beat.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -public class Beat { - public String hostname; - public String version; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Criteria.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Criteria.java deleted file mode 100644 index 524c86fbd..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Criteria.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class Criteria { - private String sid; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Ecs.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Ecs.java deleted file mode 100644 index cdc053ea6..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Ecs.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -public class Ecs { - public String version; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Event.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Event.java deleted file mode 100644 index 9f84bcf00..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Event.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -public class Event { - public String action; - public String code; - public String created; - public String kind; - public String outcome; - public String provider; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/EventData.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/EventData.java deleted file mode 100644 index 054174b46..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/EventData.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class EventData { - @JsonProperty("AuthenticationPackageName") - public String authenticationPackageName; - - @JsonProperty("AllowedToDelegateTo") - public String allowedToDelegateTo; - - @JsonProperty("AccountExpires") - public String accountExpires; - - @JsonProperty("ElevatedToken") - public String elevatedToken; - - @JsonProperty("ImpersonationLevel") - public String impersonationLevel; - - @JsonProperty("IpAddress") - public String ipAddress; - - @JsonProperty("IpPort") - public String ipPort; - - @JsonProperty("KeyLength") - public String keyLength; - - @JsonProperty("LmPackageName") - public String lmPackageName; - - @JsonProperty("LogonGuid") - public String logonGuid; - - @JsonProperty("LogonProcessName") - public String logonProcessName; - - @JsonProperty("LogonType") - public String logonType; - - @JsonProperty("ProcessId") - public String processId; - - @JsonProperty("ProcessName") - public String processName; - - @JsonProperty("PrivilegeList") - public String privilegeList; - - @JsonProperty("RestrictedAdminMode") - public String restrictedAdminMode; - - @JsonProperty("SubjectDomainName") - public String subjectDomainName; - - @JsonProperty("SubjectLogonId") - public String subjectLogonId; - - @JsonProperty("SubjectUserName") - public String subjectUserName; - - @JsonProperty("SubjectUserSid") - public String subjectUserSid; - - @JsonProperty("TargetDomainName") - public String targetDomainName; - - @JsonProperty("TargetLinkedLogonId") - public String targetLinkedLogonId; - - @JsonProperty("TargetSid") - public String targetSid; - - @JsonProperty("TargetLogonId") - public String targetLogonId; - - @JsonProperty("TargetOutboundDomainName") - public String targetOutboundDomainName; - - @JsonProperty("TargetOutboundUserName") - public String targetOutboundUserName; - - @JsonProperty("TargetUserName") - public String targetUserName; - - @JsonProperty("TargetUserSid") - public String targetUserSid; - - @JsonProperty("TransmittedServices") - public String transmittedServices; - - @JsonProperty("VirtualAccount") - public String virtualAccount; - - @JsonProperty("WorkstationName") - public String workstationName; - - @JsonProperty("DisplayName") - public String displayName; - - @JsonProperty("HomeDirectory") - public String homeDirectory; - - @JsonProperty("HomePath") - public String homePath; - - @JsonProperty("LogonHours") - public String logonHours; - - @JsonProperty("NewUacValue") - public String newUacValue; - - @JsonProperty("OldUacValue") - public String oldUacValue; - - @JsonProperty("PasswordLastSet") - public String passwordLastSet; - - @JsonProperty("PrimaryGroupId") - public String primaryGroupId; - - @JsonProperty("ProfilePath") - public String profilePath; - - @JsonProperty("SamAccountName") - public String samAccountName; - - @JsonProperty("ScriptPath") - public String scriptPath; - - @JsonProperty("SidHistory") - public String sidHistory; - - @JsonProperty("UserAccountControl") - public String userAccountControl; - - @JsonProperty("UserParameters") - public String userParameters; - - @JsonProperty("UserPrincipalName") - public String userPrincipalName; - - @JsonProperty("UserWorkstations") - public String userWorkstations; - - @JsonProperty("RemoteCredentialGuard") - public String remoteCredentialGuard; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/EventLog.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/EventLog.java deleted file mode 100644 index 7cfa5b2a4..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/EventLog.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; - -import java.util.Date; - -@Getter -public class EventLog { - @JsonProperty("@timestamp") - public String timestamp; - @JsonProperty("@version") - public String version; - public String computer_name; - public String dataSource; - public String dataType; - public Date deviceTime; - public Global global; - public String id; - public Logx logx; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Global.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Global.java deleted file mode 100644 index e66aa5667..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Global.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -public class Global { - public String type; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Host.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Host.java deleted file mode 100644 index 54ae43d0c..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Host.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -import java.util.ArrayList; - -public class Host { - public String architecture; - public String id; - public ArrayList ip; - public ArrayList mac; - public String name; - public Os os; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Logx.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Logx.java deleted file mode 100644 index f5507d33e..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Logx.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -import lombok.Getter; - -@Getter -public class Logx { - public String type; - public WinEventLog wineventlog; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Os.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Os.java deleted file mode 100644 index 1b04ee17a..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/Os.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -public class Os { - public String build; - public String family; - public String kernel; - public String name; - public String platform; - public String type; - public String version; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/UserLog.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/UserLog.java deleted file mode 100644 index c3b9f1f2b..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/UserLog.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -import com.utmstack.userauditor.model.UserAttribute; -import lombok.*; - -import java.util.List; -import java.util.Map; - - -@NoArgsConstructor -@AllArgsConstructor -@Getter -@Setter -@Builder - -public class UserLog { - Map> users; -} diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/WinEventLog.java b/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/WinEventLog.java deleted file mode 100644 index 53d2859f9..000000000 --- a/user-auditor/src/main/java/com/utmstack/userauditor/model/winevent/WinEventLog.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.utmstack.userauditor.model.winevent; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.ArrayList; - -public class WinEventLog{ - public String activity_id; - public Agent agent; - public Beat beat; - public Ecs ecs; - public Event event; - @JsonProperty("event_data") - public EventData eventData; - @JsonProperty("event_id") - public int eventId; - public String event_name; - public Host host; - public ArrayList keywords; - public String level; - public String log_name; - - public String message; - public String opcode; - public String process_id; - public String provider_guid; - public String record_number; - public String source_name; - public ArrayList tags; - public String task; - public String thread_id; - public String version; -} - - diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/service/Impl/WindowsSource.java b/user-auditor/src/main/java/com/utmstack/userauditor/service/Impl/WindowsSource.java index ee83f06f4..31bb947af 100644 --- a/user-auditor/src/main/java/com/utmstack/userauditor/service/Impl/WindowsSource.java +++ b/user-auditor/src/main/java/com/utmstack/userauditor/service/Impl/WindowsSource.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.utmstack.userauditor.model.*; -import com.utmstack.userauditor.model.winevent.EventLog; -import com.utmstack.userauditor.model.winevent.UserEvent; +import com.utmstack.userauditor.model.event.Event; +import com.utmstack.userauditor.model.event.UserEvent; import com.utmstack.userauditor.repository.SourceScanRepository; import com.utmstack.userauditor.service.elasticsearch.ElasticsearchService; import com.utmstack.userauditor.service.elasticsearch.SearchUtil; @@ -26,10 +26,8 @@ import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; @Component @@ -44,14 +42,14 @@ public class WindowsSource implements Source { @Value("${app.elasticsearch.searchIntervalMinutes}") private int searchIntervalMinutes; - Map> userEvents; + Map> userEvents; final ElasticsearchService elasticsearchService; final SourceScanRepository sourceScanRepository; private static final String TARGET_AGG_NAME = "TARGET_USER_NAME"; - private static final String FIELD = "logx.wineventlog.event_data.TargetUserName.keyword"; + private static final String FIELD = "target.user.keyword"; private static final int ITEMS_PER_PAGE = 1000; @@ -75,7 +73,7 @@ public SourceType getType() { } @Override - public Map> findUsers(UserSource userSource) throws Exception { + public Map> findUsers(UserSource userSource) throws Exception { this.userEvents = new HashMap<>(); @@ -159,24 +157,22 @@ private static SearchRequest.Builder getBuilder(SourceFilter sourceFilter, List< return srb; } - List getLastEvents(TopHitsAggregate topEvents) { - List eventLogs = new ArrayList<>(); + List getLastEvents(TopHitsAggregate topEvents) { List> searchHits = topEvents.hits().hits(); - - searchHits.forEach(hit -> { - JsonData jsonData = hit.source(); - try { - if (jsonData != null) { - eventLogs.add(objectMapper.readValue(jsonData.toString(), EventLog.class)); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - - return eventLogs; + return searchHits.stream() + .map(Hit::source) + .filter(Objects::nonNull) + .map(jsonData -> { + try { + return objectMapper.readValue(jsonData.toString(), Event.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); } + private LocalDateTime executionDate(LocalDateTime startDate) { LocalDateTime currentDateTime = LocalDateTime.now(); diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/service/UserService.java b/user-auditor/src/main/java/com/utmstack/userauditor/service/UserService.java index 74a58040e..58e49e955 100644 --- a/user-auditor/src/main/java/com/utmstack/userauditor/service/UserService.java +++ b/user-auditor/src/main/java/com/utmstack/userauditor/service/UserService.java @@ -1,7 +1,7 @@ package com.utmstack.userauditor.service; import com.utmstack.userauditor.model.*; -import com.utmstack.userauditor.model.winevent.EventLog; +import com.utmstack.userauditor.model.event.Event; import com.utmstack.userauditor.repository.UserRepository; import com.utmstack.userauditor.repository.UserSourceRepository; import com.utmstack.userauditor.service.interfaces.Source; @@ -69,7 +69,7 @@ public void findUsers() { .findFirst() .orElseThrow(() -> new RuntimeException("Class not found")); try { - Map> users = source.findUsers(logSource); + Map> users = source.findUsers(logSource); this.synchronizeUsers(users, logSource); } catch (Exception e) { throw new RuntimeException(e); @@ -77,7 +77,7 @@ public void findUsers() { }); } - private void synchronizeUsers(Map> users, UserSource userSource) { + private void synchronizeUsers(Map> users, UserSource userSource) { users.forEach((s, eventLogs) -> { // The DWM (Desktop Windows Manager) and Usermode Font Driver Host accesses are discarded. @@ -109,10 +109,14 @@ private void synchronizeUsers(Map> users, UserSource user }); } - private List synchronizeAttributes(User user, EventLog eventLog) { - int eventId = eventLog.logx.wineventlog.eventId; - user.setSid(eventId == EventType.USER_LAST_LOGON.getEventId() ? eventLog.logx.wineventlog.eventData.targetUserSid : - eventLog.logx.wineventlog.eventData.targetSid); + private List synchronizeAttributes(User user, Event eventLog) { + int eventId = Integer.parseInt(eventLog.getLog().get("eventCode").toString()); + + user.setSid( + eventId == EventType.USER_LAST_LOGON.getEventId() + ? (String) eventLog.getLog().get("winlogEventDataTargetUserSid") + : (String) eventLog.getLog().get("winlogEventDataTargetSid")); + List attributes = new ArrayList<>(); @@ -123,7 +127,7 @@ private List synchronizeAttributes(User user, EventLog eventLog) return attributes; } - private UserAttribute createUserAttributes(User user, WindowsAttributes attribute, EventLog eventLog) { + private UserAttribute createUserAttributes(User user, WindowsAttributes attribute, Event eventLog) { return UserAttribute .builder() .attributeKey(attribute.getValue()) @@ -133,34 +137,41 @@ private UserAttribute createUserAttributes(User user, WindowsAttributes attribut .build(); } - private boolean isDeleteEvent(List eventLog) { - return eventLog.stream().anyMatch(e -> e.logx.wineventlog.eventId == EventType.USER_DELETED.getEventId()); + private boolean isDeleteEvent(List eventLog) { + return eventLog.stream().anyMatch(e -> Integer.parseInt(e.getLog().get("eventCode").toString()) == EventType.USER_DELETED.getEventId()); } - private EventLog getRecentEvent(List events) { - return events.stream().filter(s -> (s.logx.wineventlog.eventId == EventType.USER_CREATED.getEventId() - || (s.logx.wineventlog.eventId == EventType.USER_LAST_LOGON.getEventId()))) - .max(Comparator.comparing(e -> e.timestamp)) + private Event getRecentEvent(List events) { + return events.stream().filter(s -> (Integer.parseInt(s.getLog().get("eventCode").toString()) == EventType.USER_CREATED.getEventId() + || (Integer.parseInt(s.getLog().get("eventCode").toString()) == EventType.USER_LAST_LOGON.getEventId()))) + .max(Comparator.comparing(Event::getTimestamp)) .orElse(null); } - private String attributeByKey(WindowsAttributes key, EventLog eventLog) { + private String attributeByKey(WindowsAttributes key, Event eventLog) { + int eventId = Integer.parseInt(Objects.toString(eventLog.getLog().get("eventCode"), "0")); switch (key) { case SAMAccountName: - return eventLog.logx.wineventlog.eventData.targetUserName; + return Objects.toString( + eventLog.getTarget() != null ? eventLog.getTarget().getUser() : null, ""); case ObjectSID: - return eventLog.logx.wineventlog.eventData.targetUserSid; + return Objects.toString(eventLog.getLog().get("winlogEventDataTargetUserSid"), ""); case CreatedAt: - return eventLog.logx.wineventlog.event.code.equals(String.valueOf(EventType.USER_CREATED.getEventId())) ? eventLog.logx.wineventlog.event.created : ""; + return eventId == EventType.USER_CREATED.getEventId() + ? Objects.toString(eventLog.getLog().get("winlogEventDataCreatedAt"), "") + : ""; case LastLogon: - return eventLog.logx.wineventlog.event.code.equals(String.valueOf(EventType.USER_LAST_LOGON.getEventId())) ? eventLog.logx.wineventlog.event.created : ""; + return eventId == EventType.USER_LAST_LOGON.getEventId() + ? Objects.toString(eventLog.getLog().get("winlogEventDataCreatedAt"), "") + : ""; default: return ""; } } + } diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/service/elasticsearch/Constants.java b/user-auditor/src/main/java/com/utmstack/userauditor/service/elasticsearch/Constants.java index 6ed31c5a9..7f710a562 100644 --- a/user-auditor/src/main/java/com/utmstack/userauditor/service/elasticsearch/Constants.java +++ b/user-auditor/src/main/java/com/utmstack/userauditor/service/elasticsearch/Constants.java @@ -11,9 +11,7 @@ public final class Constants { // - Indices common fields // ---------------------------------------------------------------------------------- - public static final String logxWineventlogEventDataTargetSidKeyword = "logx.wineventlog.event_data.TargetSid.keyword"; - public static final String logxWineventlogEventDataTargetUserSidKeyword = "logx.wineventlog.event_data.TargetUserSid.keyword"; - public static final String logxWineventlogEventDataMemberSidKeyword = "logx.wineventlog.event_data.MemberSid.keyword"; + public static final String LOG_WINLOG_EVENT_DATA_TARGET_USER_SID_KEYWORD = "log.winlogEventDataTargetUserSid.keyword"; /** * Environment variables diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/service/elasticsearch/ElasticsearchService.java b/user-auditor/src/main/java/com/utmstack/userauditor/service/elasticsearch/ElasticsearchService.java index faa9546a8..f02181b3b 100644 --- a/user-auditor/src/main/java/com/utmstack/userauditor/service/elasticsearch/ElasticsearchService.java +++ b/user-auditor/src/main/java/com/utmstack/userauditor/service/elasticsearch/ElasticsearchService.java @@ -90,9 +90,7 @@ public SearchResponse searchBySid(String sid, String to, String from, Str private static BoolQuery.Builder getBuilder(String sid) { BoolQuery.Builder shouldList = new BoolQuery.Builder(); shouldList.minimumShouldMatch("1"); - shouldList.should(f -> f.matchPhrase(m -> m.field(Constants.logxWineventlogEventDataTargetSidKeyword).query(sid))); - shouldList.should(f -> f.matchPhrase(m -> m.field(Constants.logxWineventlogEventDataTargetUserSidKeyword).query(String.valueOf(sid)))); - shouldList.should(f -> f.matchPhrase(m -> m.field(Constants.logxWineventlogEventDataMemberSidKeyword).query(String.valueOf(sid)))); + shouldList.should(f -> f.matchPhrase(m -> m.field(Constants.LOG_WINLOG_EVENT_DATA_TARGET_USER_SID_KEYWORD).query(String.valueOf(sid)))); return shouldList; } diff --git a/user-auditor/src/main/java/com/utmstack/userauditor/service/interfaces/Source.java b/user-auditor/src/main/java/com/utmstack/userauditor/service/interfaces/Source.java index 5e671cc4c..770d54b62 100644 --- a/user-auditor/src/main/java/com/utmstack/userauditor/service/interfaces/Source.java +++ b/user-auditor/src/main/java/com/utmstack/userauditor/service/interfaces/Source.java @@ -1,13 +1,13 @@ package com.utmstack.userauditor.service.interfaces; -import com.utmstack.userauditor.model.winevent.EventLog; import com.utmstack.userauditor.model.UserSource; +import com.utmstack.userauditor.model.event.Event; import com.utmstack.userauditor.service.type.SourceType; import java.util.List; import java.util.Map; public interface Source { - Map> findUsers(UserSource userSource) throws Exception; + Map> findUsers(UserSource userSource) throws Exception; SourceType getType(); } diff --git a/user-auditor/src/main/resources/application.properties b/user-auditor/src/main/resources/application.properties index 4cd3bd4a4..a3e675df8 100644 --- a/user-auditor/src/main/resources/application.properties +++ b/user-auditor/src/main/resources/application.properties @@ -7,7 +7,7 @@ spring.jpa.show-sql=false spring.jpa.hibernate.ddl-auto=update spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER -interval=PT010S -app.elasticsearch.startYear=2023 +interval=PT005S +app.elasticsearch.startYear=2025 app.elasticsearch.searchIntervalMonths=1 app.elasticsearch.searchIntervalMinutes=5 \ No newline at end of file diff --git a/user-auditor/src/main/resources/db/changelog/changes/004-2025-11-06-update_index_pattern.sql b/user-auditor/src/main/resources/db/changelog/changes/004-2025-11-06-update_index_pattern.sql new file mode 100644 index 000000000..8f3855f61 --- /dev/null +++ b/user-auditor/src/main/resources/db/changelog/changes/004-2025-11-06-update_index_pattern.sql @@ -0,0 +1,7 @@ +-- changeset author:update-windows-agent-pattern +UPDATE utm_user_source +SET index_pattern = 'v11-log-wineventlog-*', + modified_date = now() +WHERE id = 1 AND index_name = 'WINDOWS_AGENT'; + + diff --git a/user-auditor/src/main/resources/db/changelog/changes/005-2025-11-06-update_filter_fields.sql b/user-auditor/src/main/resources/db/changelog/changes/005-2025-11-06-update_filter_fields.sql new file mode 100644 index 000000000..ff38714dc --- /dev/null +++ b/user-auditor/src/main/resources/db/changelog/changes/005-2025-11-06-update_filter_fields.sql @@ -0,0 +1,5 @@ +UPDATE utm_source_filter +SET field = 'log.eventCode' +WHERE id IN (1, 2, 3); + + diff --git a/user-auditor/src/main/resources/db/changelog/db.changelog-master.yaml b/user-auditor/src/main/resources/db/changelog/db.changelog-master.yaml index 55edb621f..6535559a7 100644 --- a/user-auditor/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/user-auditor/src/main/resources/db/changelog/db.changelog-master.yaml @@ -4,4 +4,8 @@ databaseChangeLog: - include: file: classpath:/db/changelog/changes/002-2024-01-09-change-next-execution-date.sql - include: - file: classpath:/db/changelog/changes/003-2024-05-29-clean-source-scans.sql \ No newline at end of file + file: classpath:/db/changelog/changes/003-2024-05-29-clean-source-scans.sql + - include: + file: classpath:/db/changelog/changes/004-2025-11-06-update_index_pattern.sql + - include: + file: classpath:/db/changelog/changes/005-2025-11-06-update_filter_fields.sql \ No newline at end of file diff --git a/utmstack-collector/README.md b/utmstack-collector/README.md new file mode 100644 index 000000000..fc6a008a2 --- /dev/null +++ b/utmstack-collector/README.md @@ -0,0 +1 @@ +# UTMStack Collector \ No newline at end of file diff --git a/utmstack-collector/agent/collector.pb.go b/utmstack-collector/agent/collector.pb.go new file mode 100644 index 000000000..bcf8d63b7 --- /dev/null +++ b/utmstack-collector/agent/collector.pb.go @@ -0,0 +1,858 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.9 +// protoc v3.21.12 +// source: collector.proto + +package agent + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CollectorModule int32 + +const ( + CollectorModule_AS_400 CollectorModule = 0 + CollectorModule_UTMSTACK CollectorModule = 1 +) + +// Enum value maps for CollectorModule. +var ( + CollectorModule_name = map[int32]string{ + 0: "AS_400", + 1: "UTMSTACK", + } + CollectorModule_value = map[string]int32{ + "AS_400": 0, + "UTMSTACK": 1, + } +) + +func (x CollectorModule) Enum() *CollectorModule { + p := new(CollectorModule) + *p = x + return p +} + +func (x CollectorModule) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CollectorModule) Descriptor() protoreflect.EnumDescriptor { + return file_collector_proto_enumTypes[0].Descriptor() +} + +func (CollectorModule) Type() protoreflect.EnumType { + return &file_collector_proto_enumTypes[0] +} + +func (x CollectorModule) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CollectorModule.Descriptor instead. +func (CollectorModule) EnumDescriptor() ([]byte, []int) { + return file_collector_proto_rawDescGZIP(), []int{0} +} + +type RegisterRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` + Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Collector CollectorModule `protobuf:"varint,4,opt,name=collector,proto3,enum=agent.CollectorModule" json:"collector,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RegisterRequest) Reset() { + *x = RegisterRequest{} + mi := &file_collector_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RegisterRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RegisterRequest) ProtoMessage() {} + +func (x *RegisterRequest) ProtoReflect() protoreflect.Message { + mi := &file_collector_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RegisterRequest.ProtoReflect.Descriptor instead. +func (*RegisterRequest) Descriptor() ([]byte, []int) { + return file_collector_proto_rawDescGZIP(), []int{0} +} + +func (x *RegisterRequest) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *RegisterRequest) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *RegisterRequest) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *RegisterRequest) GetCollector() CollectorModule { + if x != nil { + return x.Collector + } + return CollectorModule_AS_400 +} + +type ListCollectorResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rows []*Collector `protobuf:"bytes,1,rep,name=rows,proto3" json:"rows,omitempty"` + Total int32 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListCollectorResponse) Reset() { + *x = ListCollectorResponse{} + mi := &file_collector_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListCollectorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListCollectorResponse) ProtoMessage() {} + +func (x *ListCollectorResponse) ProtoReflect() protoreflect.Message { + mi := &file_collector_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListCollectorResponse.ProtoReflect.Descriptor instead. +func (*ListCollectorResponse) Descriptor() ([]byte, []int) { + return file_collector_proto_rawDescGZIP(), []int{1} +} + +func (x *ListCollectorResponse) GetRows() []*Collector { + if x != nil { + return x.Rows + } + return nil +} + +func (x *ListCollectorResponse) GetTotal() int32 { + if x != nil { + return x.Total + } + return 0 +} + +type Collector struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Status Status `protobuf:"varint,2,opt,name=status,proto3,enum=agent.Status" json:"status,omitempty"` + CollectorKey string `protobuf:"bytes,3,opt,name=collector_key,json=collectorKey,proto3" json:"collector_key,omitempty"` + Ip string `protobuf:"bytes,4,opt,name=ip,proto3" json:"ip,omitempty"` + Hostname string `protobuf:"bytes,5,opt,name=hostname,proto3" json:"hostname,omitempty"` + Version string `protobuf:"bytes,6,opt,name=version,proto3" json:"version,omitempty"` + Module CollectorModule `protobuf:"varint,7,opt,name=module,proto3,enum=agent.CollectorModule" json:"module,omitempty"` + LastSeen string `protobuf:"bytes,8,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Collector) Reset() { + *x = Collector{} + mi := &file_collector_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Collector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Collector) ProtoMessage() {} + +func (x *Collector) ProtoReflect() protoreflect.Message { + mi := &file_collector_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Collector.ProtoReflect.Descriptor instead. +func (*Collector) Descriptor() ([]byte, []int) { + return file_collector_proto_rawDescGZIP(), []int{2} +} + +func (x *Collector) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Collector) GetStatus() Status { + if x != nil { + return x.Status + } + return Status_ONLINE +} + +func (x *Collector) GetCollectorKey() string { + if x != nil { + return x.CollectorKey + } + return "" +} + +func (x *Collector) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *Collector) GetHostname() string { + if x != nil { + return x.Hostname + } + return "" +} + +func (x *Collector) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *Collector) GetModule() CollectorModule { + if x != nil { + return x.Module + } + return CollectorModule_AS_400 +} + +func (x *Collector) GetLastSeen() string { + if x != nil { + return x.LastSeen + } + return "" +} + +type CollectorMessages struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to StreamMessage: + // + // *CollectorMessages_Config + // *CollectorMessages_Result + StreamMessage isCollectorMessages_StreamMessage `protobuf_oneof:"stream_message"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CollectorMessages) Reset() { + *x = CollectorMessages{} + mi := &file_collector_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CollectorMessages) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CollectorMessages) ProtoMessage() {} + +func (x *CollectorMessages) ProtoReflect() protoreflect.Message { + mi := &file_collector_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CollectorMessages.ProtoReflect.Descriptor instead. +func (*CollectorMessages) Descriptor() ([]byte, []int) { + return file_collector_proto_rawDescGZIP(), []int{3} +} + +func (x *CollectorMessages) GetStreamMessage() isCollectorMessages_StreamMessage { + if x != nil { + return x.StreamMessage + } + return nil +} + +func (x *CollectorMessages) GetConfig() *CollectorConfig { + if x != nil { + if x, ok := x.StreamMessage.(*CollectorMessages_Config); ok { + return x.Config + } + } + return nil +} + +func (x *CollectorMessages) GetResult() *ConfigKnowledge { + if x != nil { + if x, ok := x.StreamMessage.(*CollectorMessages_Result); ok { + return x.Result + } + } + return nil +} + +type isCollectorMessages_StreamMessage interface { + isCollectorMessages_StreamMessage() +} + +type CollectorMessages_Config struct { + Config *CollectorConfig `protobuf:"bytes,1,opt,name=config,proto3,oneof"` +} + +type CollectorMessages_Result struct { + Result *ConfigKnowledge `protobuf:"bytes,2,opt,name=result,proto3,oneof"` +} + +func (*CollectorMessages_Config) isCollectorMessages_StreamMessage() {} + +func (*CollectorMessages_Result) isCollectorMessages_StreamMessage() {} + +type CollectorConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + CollectorId string `protobuf:"bytes,1,opt,name=collector_id,json=collectorId,proto3" json:"collector_id,omitempty"` + Groups []*CollectorConfigGroup `protobuf:"bytes,2,rep,name=groups,proto3" json:"groups,omitempty"` + RequestId string `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CollectorConfig) Reset() { + *x = CollectorConfig{} + mi := &file_collector_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CollectorConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CollectorConfig) ProtoMessage() {} + +func (x *CollectorConfig) ProtoReflect() protoreflect.Message { + mi := &file_collector_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CollectorConfig.ProtoReflect.Descriptor instead. +func (*CollectorConfig) Descriptor() ([]byte, []int) { + return file_collector_proto_rawDescGZIP(), []int{4} +} + +func (x *CollectorConfig) GetCollectorId() string { + if x != nil { + return x.CollectorId + } + return "" +} + +func (x *CollectorConfig) GetGroups() []*CollectorConfigGroup { + if x != nil { + return x.Groups + } + return nil +} + +func (x *CollectorConfig) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type CollectorConfigGroup struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + GroupName string `protobuf:"bytes,2,opt,name=group_name,json=groupName,proto3" json:"group_name,omitempty"` + GroupDescription string `protobuf:"bytes,3,opt,name=group_description,json=groupDescription,proto3" json:"group_description,omitempty"` + Configurations []*CollectorGroupConfigurations `protobuf:"bytes,4,rep,name=configurations,proto3" json:"configurations,omitempty"` + CollectorId int32 `protobuf:"varint,5,opt,name=collector_id,json=collectorId,proto3" json:"collector_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CollectorConfigGroup) Reset() { + *x = CollectorConfigGroup{} + mi := &file_collector_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CollectorConfigGroup) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CollectorConfigGroup) ProtoMessage() {} + +func (x *CollectorConfigGroup) ProtoReflect() protoreflect.Message { + mi := &file_collector_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CollectorConfigGroup.ProtoReflect.Descriptor instead. +func (*CollectorConfigGroup) Descriptor() ([]byte, []int) { + return file_collector_proto_rawDescGZIP(), []int{5} +} + +func (x *CollectorConfigGroup) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *CollectorConfigGroup) GetGroupName() string { + if x != nil { + return x.GroupName + } + return "" +} + +func (x *CollectorConfigGroup) GetGroupDescription() string { + if x != nil { + return x.GroupDescription + } + return "" +} + +func (x *CollectorConfigGroup) GetConfigurations() []*CollectorGroupConfigurations { + if x != nil { + return x.Configurations + } + return nil +} + +func (x *CollectorConfigGroup) GetCollectorId() int32 { + if x != nil { + return x.CollectorId + } + return 0 +} + +type CollectorGroupConfigurations struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + GroupId int32 `protobuf:"varint,2,opt,name=group_id,json=groupId,proto3" json:"group_id,omitempty"` + ConfKey string `protobuf:"bytes,3,opt,name=conf_key,json=confKey,proto3" json:"conf_key,omitempty"` + ConfValue string `protobuf:"bytes,4,opt,name=conf_value,json=confValue,proto3" json:"conf_value,omitempty"` + ConfName string `protobuf:"bytes,5,opt,name=conf_name,json=confName,proto3" json:"conf_name,omitempty"` + ConfDescription string `protobuf:"bytes,6,opt,name=conf_description,json=confDescription,proto3" json:"conf_description,omitempty"` + ConfDataType string `protobuf:"bytes,7,opt,name=conf_data_type,json=confDataType,proto3" json:"conf_data_type,omitempty"` + ConfRequired bool `protobuf:"varint,8,opt,name=conf_required,json=confRequired,proto3" json:"conf_required,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CollectorGroupConfigurations) Reset() { + *x = CollectorGroupConfigurations{} + mi := &file_collector_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CollectorGroupConfigurations) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CollectorGroupConfigurations) ProtoMessage() {} + +func (x *CollectorGroupConfigurations) ProtoReflect() protoreflect.Message { + mi := &file_collector_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CollectorGroupConfigurations.ProtoReflect.Descriptor instead. +func (*CollectorGroupConfigurations) Descriptor() ([]byte, []int) { + return file_collector_proto_rawDescGZIP(), []int{6} +} + +func (x *CollectorGroupConfigurations) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *CollectorGroupConfigurations) GetGroupId() int32 { + if x != nil { + return x.GroupId + } + return 0 +} + +func (x *CollectorGroupConfigurations) GetConfKey() string { + if x != nil { + return x.ConfKey + } + return "" +} + +func (x *CollectorGroupConfigurations) GetConfValue() string { + if x != nil { + return x.ConfValue + } + return "" +} + +func (x *CollectorGroupConfigurations) GetConfName() string { + if x != nil { + return x.ConfName + } + return "" +} + +func (x *CollectorGroupConfigurations) GetConfDescription() string { + if x != nil { + return x.ConfDescription + } + return "" +} + +func (x *CollectorGroupConfigurations) GetConfDataType() string { + if x != nil { + return x.ConfDataType + } + return "" +} + +func (x *CollectorGroupConfigurations) GetConfRequired() bool { + if x != nil { + return x.ConfRequired + } + return false +} + +type ConfigKnowledge struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accepted string `protobuf:"bytes,1,opt,name=accepted,proto3" json:"accepted,omitempty"` + RequestId string `protobuf:"bytes,2,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigKnowledge) Reset() { + *x = ConfigKnowledge{} + mi := &file_collector_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigKnowledge) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigKnowledge) ProtoMessage() {} + +func (x *ConfigKnowledge) ProtoReflect() protoreflect.Message { + mi := &file_collector_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigKnowledge.ProtoReflect.Descriptor instead. +func (*ConfigKnowledge) Descriptor() ([]byte, []int) { + return file_collector_proto_rawDescGZIP(), []int{7} +} + +func (x *ConfigKnowledge) GetAccepted() string { + if x != nil { + return x.Accepted + } + return "" +} + +func (x *ConfigKnowledge) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type ConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Module CollectorModule `protobuf:"varint,1,opt,name=module,proto3,enum=agent.CollectorModule" json:"module,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ConfigRequest) Reset() { + *x = ConfigRequest{} + mi := &file_collector_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConfigRequest) ProtoMessage() {} + +func (x *ConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_collector_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConfigRequest.ProtoReflect.Descriptor instead. +func (*ConfigRequest) Descriptor() ([]byte, []int) { + return file_collector_proto_rawDescGZIP(), []int{8} +} + +func (x *ConfigRequest) GetModule() CollectorModule { + if x != nil { + return x.Module + } + return CollectorModule_AS_400 +} + +var File_collector_proto protoreflect.FileDescriptor + +const file_collector_proto_rawDesc = "" + + "\n" + + "\x0fcollector.proto\x12\x05agent\x1a\fcommon.proto\"\x8d\x01\n" + + "\x0fRegisterRequest\x12\x0e\n" + + "\x02ip\x18\x01 \x01(\tR\x02ip\x12\x1a\n" + + "\bhostname\x18\x02 \x01(\tR\bhostname\x12\x18\n" + + "\aversion\x18\x03 \x01(\tR\aversion\x124\n" + + "\tcollector\x18\x04 \x01(\x0e2\x16.agent.CollectorModuleR\tcollector\"S\n" + + "\x15ListCollectorResponse\x12$\n" + + "\x04rows\x18\x01 \x03(\v2\x10.agent.CollectorR\x04rows\x12\x14\n" + + "\x05total\x18\x02 \x01(\x05R\x05total\"\xfa\x01\n" + + "\tCollector\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x05R\x02id\x12%\n" + + "\x06status\x18\x02 \x01(\x0e2\r.agent.StatusR\x06status\x12#\n" + + "\rcollector_key\x18\x03 \x01(\tR\fcollectorKey\x12\x0e\n" + + "\x02ip\x18\x04 \x01(\tR\x02ip\x12\x1a\n" + + "\bhostname\x18\x05 \x01(\tR\bhostname\x12\x18\n" + + "\aversion\x18\x06 \x01(\tR\aversion\x12.\n" + + "\x06module\x18\a \x01(\x0e2\x16.agent.CollectorModuleR\x06module\x12\x1b\n" + + "\tlast_seen\x18\b \x01(\tR\blastSeen\"\x89\x01\n" + + "\x11CollectorMessages\x120\n" + + "\x06config\x18\x01 \x01(\v2\x16.agent.CollectorConfigH\x00R\x06config\x120\n" + + "\x06result\x18\x02 \x01(\v2\x16.agent.ConfigKnowledgeH\x00R\x06resultB\x10\n" + + "\x0estream_message\"\x88\x01\n" + + "\x0fCollectorConfig\x12!\n" + + "\fcollector_id\x18\x01 \x01(\tR\vcollectorId\x123\n" + + "\x06groups\x18\x02 \x03(\v2\x1b.agent.CollectorConfigGroupR\x06groups\x12\x1d\n" + + "\n" + + "request_id\x18\x03 \x01(\tR\trequestId\"\xe2\x01\n" + + "\x14CollectorConfigGroup\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x1d\n" + + "\n" + + "group_name\x18\x02 \x01(\tR\tgroupName\x12+\n" + + "\x11group_description\x18\x03 \x01(\tR\x10groupDescription\x12K\n" + + "\x0econfigurations\x18\x04 \x03(\v2#.agent.CollectorGroupConfigurationsR\x0econfigurations\x12!\n" + + "\fcollector_id\x18\x05 \x01(\x05R\vcollectorId\"\x96\x02\n" + + "\x1cCollectorGroupConfigurations\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x19\n" + + "\bgroup_id\x18\x02 \x01(\x05R\agroupId\x12\x19\n" + + "\bconf_key\x18\x03 \x01(\tR\aconfKey\x12\x1d\n" + + "\n" + + "conf_value\x18\x04 \x01(\tR\tconfValue\x12\x1b\n" + + "\tconf_name\x18\x05 \x01(\tR\bconfName\x12)\n" + + "\x10conf_description\x18\x06 \x01(\tR\x0fconfDescription\x12$\n" + + "\x0econf_data_type\x18\a \x01(\tR\fconfDataType\x12#\n" + + "\rconf_required\x18\b \x01(\bR\fconfRequired\"L\n" + + "\x0fConfigKnowledge\x12\x1a\n" + + "\baccepted\x18\x01 \x01(\tR\baccepted\x12\x1d\n" + + "\n" + + "request_id\x18\x02 \x01(\tR\trequestId\"?\n" + + "\rConfigRequest\x12.\n" + + "\x06module\x18\x01 \x01(\x0e2\x16.agent.CollectorModuleR\x06module*+\n" + + "\x0fCollectorModule\x12\n" + + "\n" + + "\x06AS_400\x10\x00\x12\f\n" + + "\bUTMSTACK\x10\x012\xee\x02\n" + + "\x10CollectorService\x12B\n" + + "\x11RegisterCollector\x12\x16.agent.RegisterRequest\x1a\x13.agent.AuthResponse\"\x00\x12>\n" + + "\x0fDeleteCollector\x12\x14.agent.DeleteRequest\x1a\x13.agent.AuthResponse\"\x00\x12C\n" + + "\rListCollector\x12\x12.agent.ListRequest\x1a\x1c.agent.ListCollectorResponse\"\x00\x12K\n" + + "\x0fCollectorStream\x12\x18.agent.CollectorMessages\x1a\x18.agent.CollectorMessages\"\x00(\x010\x01\x12D\n" + + "\x12GetCollectorConfig\x12\x14.agent.ConfigRequest\x1a\x16.agent.CollectorConfig\"\x002d\n" + + "\x15PanelCollectorService\x12K\n" + + "\x17RegisterCollectorConfig\x12\x16.agent.CollectorConfig\x1a\x16.agent.ConfigKnowledge\"\x00B5Z3github.com/utmstack/UTMStack/docker-collector/agentb\x06proto3" + +var ( + file_collector_proto_rawDescOnce sync.Once + file_collector_proto_rawDescData []byte +) + +func file_collector_proto_rawDescGZIP() []byte { + file_collector_proto_rawDescOnce.Do(func() { + file_collector_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_collector_proto_rawDesc), len(file_collector_proto_rawDesc))) + }) + return file_collector_proto_rawDescData +} + +var file_collector_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_collector_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_collector_proto_goTypes = []any{ + (CollectorModule)(0), // 0: agent.CollectorModule + (*RegisterRequest)(nil), // 1: agent.RegisterRequest + (*ListCollectorResponse)(nil), // 2: agent.ListCollectorResponse + (*Collector)(nil), // 3: agent.Collector + (*CollectorMessages)(nil), // 4: agent.CollectorMessages + (*CollectorConfig)(nil), // 5: agent.CollectorConfig + (*CollectorConfigGroup)(nil), // 6: agent.CollectorConfigGroup + (*CollectorGroupConfigurations)(nil), // 7: agent.CollectorGroupConfigurations + (*ConfigKnowledge)(nil), // 8: agent.ConfigKnowledge + (*ConfigRequest)(nil), // 9: agent.ConfigRequest + (Status)(0), // 10: agent.Status + (*DeleteRequest)(nil), // 11: agent.DeleteRequest + (*ListRequest)(nil), // 12: agent.ListRequest + (*AuthResponse)(nil), // 13: agent.AuthResponse +} +var file_collector_proto_depIdxs = []int32{ + 0, // 0: agent.RegisterRequest.collector:type_name -> agent.CollectorModule + 3, // 1: agent.ListCollectorResponse.rows:type_name -> agent.Collector + 10, // 2: agent.Collector.status:type_name -> agent.Status + 0, // 3: agent.Collector.module:type_name -> agent.CollectorModule + 5, // 4: agent.CollectorMessages.config:type_name -> agent.CollectorConfig + 8, // 5: agent.CollectorMessages.result:type_name -> agent.ConfigKnowledge + 6, // 6: agent.CollectorConfig.groups:type_name -> agent.CollectorConfigGroup + 7, // 7: agent.CollectorConfigGroup.configurations:type_name -> agent.CollectorGroupConfigurations + 0, // 8: agent.ConfigRequest.module:type_name -> agent.CollectorModule + 1, // 9: agent.CollectorService.RegisterCollector:input_type -> agent.RegisterRequest + 11, // 10: agent.CollectorService.DeleteCollector:input_type -> agent.DeleteRequest + 12, // 11: agent.CollectorService.ListCollector:input_type -> agent.ListRequest + 4, // 12: agent.CollectorService.CollectorStream:input_type -> agent.CollectorMessages + 9, // 13: agent.CollectorService.GetCollectorConfig:input_type -> agent.ConfigRequest + 5, // 14: agent.PanelCollectorService.RegisterCollectorConfig:input_type -> agent.CollectorConfig + 13, // 15: agent.CollectorService.RegisterCollector:output_type -> agent.AuthResponse + 13, // 16: agent.CollectorService.DeleteCollector:output_type -> agent.AuthResponse + 2, // 17: agent.CollectorService.ListCollector:output_type -> agent.ListCollectorResponse + 4, // 18: agent.CollectorService.CollectorStream:output_type -> agent.CollectorMessages + 5, // 19: agent.CollectorService.GetCollectorConfig:output_type -> agent.CollectorConfig + 8, // 20: agent.PanelCollectorService.RegisterCollectorConfig:output_type -> agent.ConfigKnowledge + 15, // [15:21] is the sub-list for method output_type + 9, // [9:15] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name +} + +func init() { file_collector_proto_init() } +func file_collector_proto_init() { + if File_collector_proto != nil { + return + } + file_common_proto_init() + file_collector_proto_msgTypes[3].OneofWrappers = []any{ + (*CollectorMessages_Config)(nil), + (*CollectorMessages_Result)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_collector_proto_rawDesc), len(file_collector_proto_rawDesc)), + NumEnums: 1, + NumMessages: 9, + NumExtensions: 0, + NumServices: 2, + }, + GoTypes: file_collector_proto_goTypes, + DependencyIndexes: file_collector_proto_depIdxs, + EnumInfos: file_collector_proto_enumTypes, + MessageInfos: file_collector_proto_msgTypes, + }.Build() + File_collector_proto = out.File + file_collector_proto_goTypes = nil + file_collector_proto_depIdxs = nil +} diff --git a/utmstack-collector/agent/collector_grpc.pb.go b/utmstack-collector/agent/collector_grpc.pb.go new file mode 100644 index 000000000..a924c7361 --- /dev/null +++ b/utmstack-collector/agent/collector_grpc.pb.go @@ -0,0 +1,370 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v3.21.12 +// source: collector.proto + +package agent + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + CollectorService_RegisterCollector_FullMethodName = "/agent.CollectorService/RegisterCollector" + CollectorService_DeleteCollector_FullMethodName = "/agent.CollectorService/DeleteCollector" + CollectorService_ListCollector_FullMethodName = "/agent.CollectorService/ListCollector" + CollectorService_CollectorStream_FullMethodName = "/agent.CollectorService/CollectorStream" + CollectorService_GetCollectorConfig_FullMethodName = "/agent.CollectorService/GetCollectorConfig" +) + +// CollectorServiceClient is the client API for CollectorService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type CollectorServiceClient interface { + RegisterCollector(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*AuthResponse, error) + DeleteCollector(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*AuthResponse, error) + ListCollector(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListCollectorResponse, error) + CollectorStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[CollectorMessages, CollectorMessages], error) + GetCollectorConfig(ctx context.Context, in *ConfigRequest, opts ...grpc.CallOption) (*CollectorConfig, error) +} + +type collectorServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewCollectorServiceClient(cc grpc.ClientConnInterface) CollectorServiceClient { + return &collectorServiceClient{cc} +} + +func (c *collectorServiceClient) RegisterCollector(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*AuthResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthResponse) + err := c.cc.Invoke(ctx, CollectorService_RegisterCollector_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *collectorServiceClient) DeleteCollector(ctx context.Context, in *DeleteRequest, opts ...grpc.CallOption) (*AuthResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthResponse) + err := c.cc.Invoke(ctx, CollectorService_DeleteCollector_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *collectorServiceClient) ListCollector(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListCollectorResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListCollectorResponse) + err := c.cc.Invoke(ctx, CollectorService_ListCollector_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *collectorServiceClient) CollectorStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[CollectorMessages, CollectorMessages], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &CollectorService_ServiceDesc.Streams[0], CollectorService_CollectorStream_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[CollectorMessages, CollectorMessages]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type CollectorService_CollectorStreamClient = grpc.BidiStreamingClient[CollectorMessages, CollectorMessages] + +func (c *collectorServiceClient) GetCollectorConfig(ctx context.Context, in *ConfigRequest, opts ...grpc.CallOption) (*CollectorConfig, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CollectorConfig) + err := c.cc.Invoke(ctx, CollectorService_GetCollectorConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CollectorServiceServer is the server API for CollectorService service. +// All implementations must embed UnimplementedCollectorServiceServer +// for forward compatibility. +type CollectorServiceServer interface { + RegisterCollector(context.Context, *RegisterRequest) (*AuthResponse, error) + DeleteCollector(context.Context, *DeleteRequest) (*AuthResponse, error) + ListCollector(context.Context, *ListRequest) (*ListCollectorResponse, error) + CollectorStream(grpc.BidiStreamingServer[CollectorMessages, CollectorMessages]) error + GetCollectorConfig(context.Context, *ConfigRequest) (*CollectorConfig, error) + mustEmbedUnimplementedCollectorServiceServer() +} + +// UnimplementedCollectorServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedCollectorServiceServer struct{} + +func (UnimplementedCollectorServiceServer) RegisterCollector(context.Context, *RegisterRequest) (*AuthResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RegisterCollector not implemented") +} +func (UnimplementedCollectorServiceServer) DeleteCollector(context.Context, *DeleteRequest) (*AuthResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteCollector not implemented") +} +func (UnimplementedCollectorServiceServer) ListCollector(context.Context, *ListRequest) (*ListCollectorResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListCollector not implemented") +} +func (UnimplementedCollectorServiceServer) CollectorStream(grpc.BidiStreamingServer[CollectorMessages, CollectorMessages]) error { + return status.Errorf(codes.Unimplemented, "method CollectorStream not implemented") +} +func (UnimplementedCollectorServiceServer) GetCollectorConfig(context.Context, *ConfigRequest) (*CollectorConfig, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCollectorConfig not implemented") +} +func (UnimplementedCollectorServiceServer) mustEmbedUnimplementedCollectorServiceServer() {} +func (UnimplementedCollectorServiceServer) testEmbeddedByValue() {} + +// UnsafeCollectorServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CollectorServiceServer will +// result in compilation errors. +type UnsafeCollectorServiceServer interface { + mustEmbedUnimplementedCollectorServiceServer() +} + +func RegisterCollectorServiceServer(s grpc.ServiceRegistrar, srv CollectorServiceServer) { + // If the following call pancis, it indicates UnimplementedCollectorServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&CollectorService_ServiceDesc, srv) +} + +func _CollectorService_RegisterCollector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RegisterRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CollectorServiceServer).RegisterCollector(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CollectorService_RegisterCollector_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CollectorServiceServer).RegisterCollector(ctx, req.(*RegisterRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CollectorService_DeleteCollector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CollectorServiceServer).DeleteCollector(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CollectorService_DeleteCollector_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CollectorServiceServer).DeleteCollector(ctx, req.(*DeleteRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CollectorService_ListCollector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CollectorServiceServer).ListCollector(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CollectorService_ListCollector_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CollectorServiceServer).ListCollector(ctx, req.(*ListRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CollectorService_CollectorStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(CollectorServiceServer).CollectorStream(&grpc.GenericServerStream[CollectorMessages, CollectorMessages]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type CollectorService_CollectorStreamServer = grpc.BidiStreamingServer[CollectorMessages, CollectorMessages] + +func _CollectorService_GetCollectorConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CollectorServiceServer).GetCollectorConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CollectorService_GetCollectorConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CollectorServiceServer).GetCollectorConfig(ctx, req.(*ConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// CollectorService_ServiceDesc is the grpc.ServiceDesc for CollectorService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var CollectorService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "agent.CollectorService", + HandlerType: (*CollectorServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "RegisterCollector", + Handler: _CollectorService_RegisterCollector_Handler, + }, + { + MethodName: "DeleteCollector", + Handler: _CollectorService_DeleteCollector_Handler, + }, + { + MethodName: "ListCollector", + Handler: _CollectorService_ListCollector_Handler, + }, + { + MethodName: "GetCollectorConfig", + Handler: _CollectorService_GetCollectorConfig_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "CollectorStream", + Handler: _CollectorService_CollectorStream_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "collector.proto", +} + +const ( + PanelCollectorService_RegisterCollectorConfig_FullMethodName = "/agent.PanelCollectorService/RegisterCollectorConfig" +) + +// PanelCollectorServiceClient is the client API for PanelCollectorService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type PanelCollectorServiceClient interface { + RegisterCollectorConfig(ctx context.Context, in *CollectorConfig, opts ...grpc.CallOption) (*ConfigKnowledge, error) +} + +type panelCollectorServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewPanelCollectorServiceClient(cc grpc.ClientConnInterface) PanelCollectorServiceClient { + return &panelCollectorServiceClient{cc} +} + +func (c *panelCollectorServiceClient) RegisterCollectorConfig(ctx context.Context, in *CollectorConfig, opts ...grpc.CallOption) (*ConfigKnowledge, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ConfigKnowledge) + err := c.cc.Invoke(ctx, PanelCollectorService_RegisterCollectorConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PanelCollectorServiceServer is the server API for PanelCollectorService service. +// All implementations must embed UnimplementedPanelCollectorServiceServer +// for forward compatibility. +type PanelCollectorServiceServer interface { + RegisterCollectorConfig(context.Context, *CollectorConfig) (*ConfigKnowledge, error) + mustEmbedUnimplementedPanelCollectorServiceServer() +} + +// UnimplementedPanelCollectorServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedPanelCollectorServiceServer struct{} + +func (UnimplementedPanelCollectorServiceServer) RegisterCollectorConfig(context.Context, *CollectorConfig) (*ConfigKnowledge, error) { + return nil, status.Errorf(codes.Unimplemented, "method RegisterCollectorConfig not implemented") +} +func (UnimplementedPanelCollectorServiceServer) mustEmbedUnimplementedPanelCollectorServiceServer() {} +func (UnimplementedPanelCollectorServiceServer) testEmbeddedByValue() {} + +// UnsafePanelCollectorServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PanelCollectorServiceServer will +// result in compilation errors. +type UnsafePanelCollectorServiceServer interface { + mustEmbedUnimplementedPanelCollectorServiceServer() +} + +func RegisterPanelCollectorServiceServer(s grpc.ServiceRegistrar, srv PanelCollectorServiceServer) { + // If the following call pancis, it indicates UnimplementedPanelCollectorServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&PanelCollectorService_ServiceDesc, srv) +} + +func _PanelCollectorService_RegisterCollectorConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CollectorConfig) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PanelCollectorServiceServer).RegisterCollectorConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PanelCollectorService_RegisterCollectorConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PanelCollectorServiceServer).RegisterCollectorConfig(ctx, req.(*CollectorConfig)) + } + return interceptor(ctx, in, info, handler) +} + +// PanelCollectorService_ServiceDesc is the grpc.ServiceDesc for PanelCollectorService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var PanelCollectorService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "agent.PanelCollectorService", + HandlerType: (*PanelCollectorServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "RegisterCollectorConfig", + Handler: _PanelCollectorService_RegisterCollectorConfig_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "collector.proto", +} diff --git a/utmstack-collector/agent/common.pb.go b/utmstack-collector/agent/common.pb.go new file mode 100644 index 000000000..0d4ccf71d --- /dev/null +++ b/utmstack-collector/agent/common.pb.go @@ -0,0 +1,413 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.21.12 +// source: common.proto + +package agent + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Status int32 + +const ( + Status_ONLINE Status = 0 + Status_OFFLINE Status = 1 + Status_UNKNOWN Status = 2 +) + +// Enum value maps for Status. +var ( + Status_name = map[int32]string{ + 0: "ONLINE", + 1: "OFFLINE", + 2: "UNKNOWN", + } + Status_value = map[string]int32{ + "ONLINE": 0, + "OFFLINE": 1, + "UNKNOWN": 2, + } +) + +func (x Status) Enum() *Status { + p := new(Status) + *p = x + return p +} + +func (x Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Status) Descriptor() protoreflect.EnumDescriptor { + return file_common_proto_enumTypes[0].Descriptor() +} + +func (Status) Type() protoreflect.EnumType { + return &file_common_proto_enumTypes[0] +} + +func (x Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Status.Descriptor instead. +func (Status) EnumDescriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{0} +} + +type ConnectorType int32 + +const ( + ConnectorType_AGENT ConnectorType = 0 + ConnectorType_COLLECTOR ConnectorType = 1 +) + +// Enum value maps for ConnectorType. +var ( + ConnectorType_name = map[int32]string{ + 0: "AGENT", + 1: "COLLECTOR", + } + ConnectorType_value = map[string]int32{ + "AGENT": 0, + "COLLECTOR": 1, + } +) + +func (x ConnectorType) Enum() *ConnectorType { + p := new(ConnectorType) + *p = x + return p +} + +func (x ConnectorType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConnectorType) Descriptor() protoreflect.EnumDescriptor { + return file_common_proto_enumTypes[1].Descriptor() +} + +func (ConnectorType) Type() protoreflect.EnumType { + return &file_common_proto_enumTypes[1] +} + +func (x ConnectorType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConnectorType.Descriptor instead. +func (ConnectorType) EnumDescriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{1} +} + +type ListRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PageNumber int32 `protobuf:"varint,1,opt,name=page_number,json=pageNumber,proto3" json:"page_number,omitempty"` + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + SearchQuery string `protobuf:"bytes,3,opt,name=search_query,json=searchQuery,proto3" json:"search_query,omitempty"` + SortBy string `protobuf:"bytes,4,opt,name=sort_by,json=sortBy,proto3" json:"sort_by,omitempty"` +} + +func (x *ListRequest) Reset() { + *x = ListRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_common_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRequest) ProtoMessage() {} + +func (x *ListRequest) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRequest.ProtoReflect.Descriptor instead. +func (*ListRequest) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{0} +} + +func (x *ListRequest) GetPageNumber() int32 { + if x != nil { + return x.PageNumber + } + return 0 +} + +func (x *ListRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListRequest) GetSearchQuery() string { + if x != nil { + return x.SearchQuery + } + return "" +} + +func (x *ListRequest) GetSortBy() string { + if x != nil { + return x.SortBy + } + return "" +} + +type AuthResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` +} + +func (x *AuthResponse) Reset() { + *x = AuthResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_common_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuthResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthResponse) ProtoMessage() {} + +func (x *AuthResponse) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthResponse.ProtoReflect.Descriptor instead. +func (*AuthResponse) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{1} +} + +func (x *AuthResponse) GetId() uint32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *AuthResponse) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +type DeleteRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DeletedBy string `protobuf:"bytes,1,opt,name=deleted_by,json=deletedBy,proto3" json:"deleted_by,omitempty"` +} + +func (x *DeleteRequest) Reset() { + *x = DeleteRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_common_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRequest) ProtoMessage() {} + +func (x *DeleteRequest) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRequest.ProtoReflect.Descriptor instead. +func (*DeleteRequest) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{2} +} + +func (x *DeleteRequest) GetDeletedBy() string { + if x != nil { + return x.DeletedBy + } + return "" +} + +var File_common_proto protoreflect.FileDescriptor + +var file_common_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x22, 0x87, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x6e, 0x75, + 0x6d, 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x70, 0x61, 0x67, 0x65, + 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, + 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, + 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x22, + 0x30, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x22, 0x2e, 0x0a, 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x42, + 0x79, 0x2a, 0x2e, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0a, 0x0a, 0x06, 0x4f, + 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4f, 0x46, 0x46, 0x4c, 0x49, + 0x4e, 0x45, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x02, 0x2a, 0x29, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x47, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x0d, 0x0a, + 0x09, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x4f, 0x52, 0x10, 0x01, 0x42, 0x32, 0x5a, 0x30, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x74, 0x6d, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x2f, 0x55, 0x54, 0x4d, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_common_proto_rawDescOnce sync.Once + file_common_proto_rawDescData = file_common_proto_rawDesc +) + +func file_common_proto_rawDescGZIP() []byte { + file_common_proto_rawDescOnce.Do(func() { + file_common_proto_rawDescData = protoimpl.X.CompressGZIP(file_common_proto_rawDescData) + }) + return file_common_proto_rawDescData +} + +var file_common_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_common_proto_goTypes = []interface{}{ + (Status)(0), // 0: agent.Status + (ConnectorType)(0), // 1: agent.ConnectorType + (*ListRequest)(nil), // 2: agent.ListRequest + (*AuthResponse)(nil), // 3: agent.AuthResponse + (*DeleteRequest)(nil), // 4: agent.DeleteRequest +} +var file_common_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_common_proto_init() } +func file_common_proto_init() { + if File_common_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_common_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_common_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_common_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_common_proto_rawDesc, + NumEnums: 2, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_common_proto_goTypes, + DependencyIndexes: file_common_proto_depIdxs, + EnumInfos: file_common_proto_enumTypes, + MessageInfos: file_common_proto_msgTypes, + }.Build() + File_common_proto = out.File + file_common_proto_rawDesc = nil + file_common_proto_goTypes = nil + file_common_proto_depIdxs = nil +} diff --git a/utmstack-collector/agent/delete.go b/utmstack-collector/agent/delete.go new file mode 100644 index 000000000..051f5f7f0 --- /dev/null +++ b/utmstack-collector/agent/delete.go @@ -0,0 +1,43 @@ +package agent + +import ( + "context" + "os/user" + "strconv" + + "github.com/utmstack/UTMStack/utmstack-collector/config" + "github.com/utmstack/UTMStack/utmstack-collector/conn" + "github.com/utmstack/UTMStack/utmstack-collector/utils" + "google.golang.org/grpc/metadata" +) + +func DeleteAgent(cnf *config.Config) error { + connection, err := conn.GetAgentManagerConnection(cnf) + if err != nil { + return utils.Logger.ErrorF("error connecting to Agent Manager: %v", err) + } + + collectorClient := NewCollectorServiceClient(connection) + ctx, cancel := context.WithCancel(context.Background()) + ctx = metadata.AppendToOutgoingContext(ctx, "key", cnf.CollectorKey) + ctx = metadata.AppendToOutgoingContext(ctx, "id", strconv.Itoa(int(cnf.CollectorID))) + ctx = metadata.AppendToOutgoingContext(ctx, "type", "collector") + defer cancel() + + currentUser, err := user.Current() + if err != nil { + return utils.Logger.ErrorF("error getting user: %v", err) + } + + delReq := &DeleteRequest{ + DeletedBy: currentUser.Username, + } + + _, err = collectorClient.DeleteCollector(ctx, delReq) + if err != nil { + utils.Logger.ErrorF("error removing UTMStack Collector from Agent Manager %v", err) + } + + utils.Logger.LogF(100, "UTMStack Collector removed successfully from agent manager") + return nil +} diff --git a/utmstack-collector/agent/ping.pb.go b/utmstack-collector/agent/ping.pb.go new file mode 100644 index 000000000..21ddaf763 --- /dev/null +++ b/utmstack-collector/agent/ping.pb.go @@ -0,0 +1,218 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.21.12 +// source: ping.proto + +package agent + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PingRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type ConnectorType `protobuf:"varint,1,opt,name=type,proto3,enum=agent.ConnectorType" json:"type,omitempty"` +} + +func (x *PingRequest) Reset() { + *x = PingRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_ping_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingRequest) ProtoMessage() {} + +func (x *PingRequest) ProtoReflect() protoreflect.Message { + mi := &file_ping_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingRequest.ProtoReflect.Descriptor instead. +func (*PingRequest) Descriptor() ([]byte, []int) { + return file_ping_proto_rawDescGZIP(), []int{0} +} + +func (x *PingRequest) GetType() ConnectorType { + if x != nil { + return x.Type + } + return ConnectorType_AGENT +} + +type PingResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Received string `protobuf:"bytes,1,opt,name=received,proto3" json:"received,omitempty"` +} + +func (x *PingResponse) Reset() { + *x = PingResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_ping_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingResponse) ProtoMessage() {} + +func (x *PingResponse) ProtoReflect() protoreflect.Message { + mi := &file_ping_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingResponse.ProtoReflect.Descriptor instead. +func (*PingResponse) Descriptor() ([]byte, []int) { + return file_ping_proto_rawDescGZIP(), []int{1} +} + +func (x *PingResponse) GetReceived() string { + if x != nil { + return x.Received + } + return "" +} + +var File_ping_proto protoreflect.FileDescriptor + +var file_ping_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x1a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x37, 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x28, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0x2a, 0x0a, 0x0c, 0x50, 0x69, + 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, + 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, + 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x32, 0x42, 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x33, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x42, 0x32, 0x5a, 0x30, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x74, 0x6d, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x2f, 0x55, 0x54, 0x4d, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2d, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_ping_proto_rawDescOnce sync.Once + file_ping_proto_rawDescData = file_ping_proto_rawDesc +) + +func file_ping_proto_rawDescGZIP() []byte { + file_ping_proto_rawDescOnce.Do(func() { + file_ping_proto_rawDescData = protoimpl.X.CompressGZIP(file_ping_proto_rawDescData) + }) + return file_ping_proto_rawDescData +} + +var file_ping_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_ping_proto_goTypes = []interface{}{ + (*PingRequest)(nil), // 0: agent.PingRequest + (*PingResponse)(nil), // 1: agent.PingResponse + (ConnectorType)(0), // 2: agent.ConnectorType +} +var file_ping_proto_depIdxs = []int32{ + 2, // 0: agent.PingRequest.type:type_name -> agent.ConnectorType + 0, // 1: agent.PingService.Ping:input_type -> agent.PingRequest + 1, // 2: agent.PingService.Ping:output_type -> agent.PingResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_ping_proto_init() } +func file_ping_proto_init() { + if File_ping_proto != nil { + return + } + file_common_proto_init() + if !protoimpl.UnsafeEnabled { + file_ping_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PingRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_ping_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PingResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_ping_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ping_proto_goTypes, + DependencyIndexes: file_ping_proto_depIdxs, + MessageInfos: file_ping_proto_msgTypes, + }.Build() + File_ping_proto = out.File + file_ping_proto_rawDesc = nil + file_ping_proto_goTypes = nil + file_ping_proto_depIdxs = nil +} diff --git a/utmstack-collector/agent/ping_grpc.pb.go b/utmstack-collector/agent/ping_grpc.pb.go new file mode 100644 index 000000000..f283dc80a --- /dev/null +++ b/utmstack-collector/agent/ping_grpc.pb.go @@ -0,0 +1,114 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v3.21.12 +// source: ping.proto + +package agent + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + PingService_Ping_FullMethodName = "/agent.PingService/Ping" +) + +// PingServiceClient is the client API for PingService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type PingServiceClient interface { + Ping(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PingRequest, PingResponse], error) +} + +type pingServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewPingServiceClient(cc grpc.ClientConnInterface) PingServiceClient { + return &pingServiceClient{cc} +} + +func (c *pingServiceClient) Ping(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PingRequest, PingResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &PingService_ServiceDesc.Streams[0], PingService_Ping_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[PingRequest, PingResponse]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type PingService_PingClient = grpc.ClientStreamingClient[PingRequest, PingResponse] + +// PingServiceServer is the server API for PingService service. +// All implementations must embed UnimplementedPingServiceServer +// for forward compatibility. +type PingServiceServer interface { + Ping(grpc.ClientStreamingServer[PingRequest, PingResponse]) error + mustEmbedUnimplementedPingServiceServer() +} + +// UnimplementedPingServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedPingServiceServer struct{} + +func (UnimplementedPingServiceServer) Ping(grpc.ClientStreamingServer[PingRequest, PingResponse]) error { + return status.Errorf(codes.Unimplemented, "method Ping not implemented") +} +func (UnimplementedPingServiceServer) mustEmbedUnimplementedPingServiceServer() {} +func (UnimplementedPingServiceServer) testEmbeddedByValue() {} + +// UnsafePingServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PingServiceServer will +// result in compilation errors. +type UnsafePingServiceServer interface { + mustEmbedUnimplementedPingServiceServer() +} + +func RegisterPingServiceServer(s grpc.ServiceRegistrar, srv PingServiceServer) { + // If the following call pancis, it indicates UnimplementedPingServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&PingService_ServiceDesc, srv) +} + +func _PingService_Ping_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(PingServiceServer).Ping(&grpc.GenericServerStream[PingRequest, PingResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type PingService_PingServer = grpc.ClientStreamingServer[PingRequest, PingResponse] + +// PingService_ServiceDesc is the grpc.ServiceDesc for PingService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var PingService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "agent.PingService", + HandlerType: (*PingServiceServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Ping", + Handler: _PingService_Ping_Handler, + ClientStreams: true, + }, + }, + Metadata: "ping.proto", +} diff --git a/utmstack-collector/agent/ping_imp.go b/utmstack-collector/agent/ping_imp.go new file mode 100644 index 000000000..9cd67322d --- /dev/null +++ b/utmstack-collector/agent/ping_imp.go @@ -0,0 +1,90 @@ +package agent + +import ( + "context" + "strings" + "time" + + "github.com/utmstack/UTMStack/utmstack-collector/config" + "github.com/utmstack/UTMStack/utmstack-collector/conn" + "github.com/utmstack/UTMStack/utmstack-collector/utils" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + timeToSleep = 10 * time.Second + pingInterval = 15 * time.Second +) + +func StartPing(cnf *config.Config, ctx context.Context) { + var connErrMsgWritten, errorLogged bool + + for { + connection, err := conn.GetAgentManagerConnection(cnf) + if err != nil { + if !connErrMsgWritten { + utils.Logger.ErrorF("error connecting to Agent Manager: %v", err) + connErrMsgWritten = true + } else { + utils.Logger.LogF(100, "error connecting to Agent Manager: %v", err) + } + time.Sleep(timeToSleep) + continue + } + + client := NewPingServiceClient(connection) + stream, err := client.Ping(ctx) + if err != nil { + if !connErrMsgWritten { + utils.Logger.ErrorF("failed to start Ping Stream: %v", err) + connErrMsgWritten = true + } else { + utils.Logger.LogF(100, "failed to start Ping Stream: %v", err) + } + time.Sleep(timeToSleep) + continue + } + + utils.Logger.LogF(100, "Ping Stream started") + connErrMsgWritten = false + + ticker := time.NewTicker(pingInterval) + + for range ticker.C { + err := stream.Send(&PingRequest{Type: ConnectorType_AGENT}) + if err != nil { + if strings.Contains(err.Error(), "EOF") { + utils.Logger.LogF(100, "error sending Ping request: %v", err) + time.Sleep(timeToSleep) + break + } + st, ok := status.FromError(err) + if ok && (st.Code() == codes.Unavailable || st.Code() == codes.Canceled) { + if !errorLogged { + utils.Logger.ErrorF("error sending Ping request: %v", err) + errorLogged = true + } else { + utils.Logger.LogF(100, "error sending Ping request: %v", err) + } + time.Sleep(timeToSleep) + break + } else { + if !errorLogged { + utils.Logger.ErrorF("error sending Ping request: %v", err) + errorLogged = true + } else { + utils.Logger.LogF(100, "error sending Ping request: %v", err) + } + time.Sleep(timeToSleep) + continue + } + } + + errorLogged = false + utils.Logger.LogF(100, "Ping request sent") + } + + ticker.Stop() + } +} diff --git a/utmstack-collector/agent/register.go b/utmstack-collector/agent/register.go new file mode 100644 index 000000000..c35aab26f --- /dev/null +++ b/utmstack-collector/agent/register.go @@ -0,0 +1,63 @@ +package agent + +import ( + "context" + + "github.com/utmstack/UTMStack/utmstack-collector/config" + "github.com/utmstack/UTMStack/utmstack-collector/conn" + "github.com/utmstack/UTMStack/utmstack-collector/models" + "github.com/utmstack/UTMStack/utmstack-collector/utils" + "google.golang.org/grpc/metadata" +) + +func RegisterCollector(cnf *config.Config, UTMKey string) error { + connection, err := conn.GetAgentManagerConnection(cnf) + if err != nil { + return utils.Logger.ErrorF("error connecting to Agent Manager: %v", err) + } + + collectorClient := NewCollectorServiceClient(connection) + ctx, cancel := context.WithCancel(context.Background()) + ctx = metadata.AppendToOutgoingContext(ctx, "connection-key", UTMKey) + defer cancel() + + ip, err := utils.GetIPAddress() + if err != nil { + return utils.Logger.ErrorF("error getting ip address: %v", err) + } + + osInfo, err := utils.GetOsInfo() + if err != nil { + return utils.Logger.ErrorF("error getting os info: %v", err) + } + + version := models.Version{} + err = utils.ReadJson(config.VersionPath, &version) + if err != nil { + return utils.Logger.ErrorF("error reading version file: %v", err) + } + + request := &RegisterRequest{ + Ip: ip, + Hostname: osInfo.Hostname, + Version: version.Version, + Collector: CollectorModule_UTMSTACK, + } + + utils.Logger.Info("Registering UTMStack Collector with Agent Manager...") + utils.Logger.Info("Collector Details: IP=%s, Hostname=%s, Version=%s, Module=%s", + ip, osInfo.Hostname, version.Version, CollectorModule_UTMSTACK.String()) + + response, err := collectorClient.RegisterCollector(ctx, request) + if err != nil { + return utils.Logger.ErrorF("failed to register collector: %v", err) + } + + cnf.CollectorID = uint(response.Id) + cnf.CollectorKey = response.Key + + utils.Logger.Info("UTMStack Collector registered successfully") + utils.Logger.Info("Collector ID: %d", cnf.CollectorID) + + return nil +} diff --git a/utmstack-collector/agent/uninstall.go b/utmstack-collector/agent/uninstall.go new file mode 100644 index 000000000..26b9de0da --- /dev/null +++ b/utmstack-collector/agent/uninstall.go @@ -0,0 +1,17 @@ +package agent + +import ( + "fmt" + "path/filepath" + + "github.com/utmstack/UTMStack/utmstack-collector/config" + "github.com/utmstack/UTMStack/utmstack-collector/utils" +) + +func UninstallAll() error { + err := utils.Execute(filepath.Join(utils.GetMyPath(), fmt.Sprintf(config.ServiceLogFile, "")), utils.GetMyPath(), "uninstall") + if err != nil { + return utils.Logger.ErrorF("%v", err) + } + return nil +} diff --git a/utmstack-collector/collector/docker.go b/utmstack-collector/collector/docker.go new file mode 100644 index 000000000..2f2569d97 --- /dev/null +++ b/utmstack-collector/collector/docker.go @@ -0,0 +1,463 @@ +package collector + +import ( + "bufio" + "context" + "io" + "strings" + "sync" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" + "github.com/docker/docker/client" + "github.com/google/uuid" + "github.com/threatwinds/go-sdk/plugins" + "github.com/utmstack/UTMStack/utmstack-collector/config" + "github.com/utmstack/UTMStack/utmstack-collector/logservice" + "github.com/utmstack/UTMStack/utmstack-collector/models" + "github.com/utmstack/UTMStack/utmstack-collector/utils" +) + +type DockerCollector struct { + client *client.Client + filter *models.ContainerFilter + ctx context.Context + cancel context.CancelFunc + containerMu sync.RWMutex + containers map[string]models.Container + eventChan chan models.ContainerEvent + stopOnce sync.Once +} + +type Config struct { + DockerHost string `yaml:"docker_host"` + APIVersion string `yaml:"api_version"` + FilterRules []models.FilterRule `yaml:"filter_rules"` + MaxLogLength int `yaml:"max_log_length"` + BufferSize int `yaml:"buffer_size"` +} + +func DefaultConfig() *Config { + return &Config{ + DockerHost: config.DockerHost, + APIVersion: config.DockerAPIVersion, + MaxLogLength: config.MaxLogLength, + BufferSize: config.BufferSize, + FilterRules: config.FilterRules, + } +} + +func NewDockerCollector(config *Config) (*DockerCollector, error) { + var cli *client.Client + var err error + + if config.DockerHost != "" { + cli, err = client.NewClientWithOpts( + client.WithHost(config.DockerHost), + client.WithAPIVersionNegotiation(), + ) + } else { + cli, err = client.NewClientWithOpts(client.FromEnv) + } + + if err != nil { + return nil, utils.Logger.ErrorF("failed to create Docker client: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + _, err = cli.Ping(ctx) + if err != nil { + return nil, utils.Logger.ErrorF("failed to connect to Docker: %v", err) + } + + utils.Logger.Info("Connected to Docker daemon successfully") + + ctx, cancel = context.WithCancel(context.Background()) + + return &DockerCollector{ + client: cli, + filter: models.NewContainerFilter(config.FilterRules), + ctx: ctx, + cancel: cancel, + containers: make(map[string]models.Container), + eventChan: make(chan models.ContainerEvent, config.BufferSize), + }, nil +} + +func (d *DockerCollector) Start() error { + utils.Logger.Info("Starting Docker log collector") + + if err := d.discoverContainers(); err != nil { + return utils.Logger.ErrorF("failed to discover containers: %v", err) + } + + go d.monitorEvents() + + go d.periodicRediscovery() + + d.containerMu.RLock() + for _, container := range d.containers { + if d.filter.ShouldCollect(container) { + go d.streamContainerLogs(container) + } + } + d.containerMu.RUnlock() + + utils.Logger.Info("Docker log collector started successfully") + return nil +} + +func (d *DockerCollector) Stop() { + d.stopOnce.Do(func() { + utils.Logger.Info("Stopping Docker log collector") + d.cancel() + + close(d.eventChan) + + if d.client != nil { + d.client.Close() + } + }) +} + +func (d *DockerCollector) discoverContainers() error { + containers, err := d.client.ContainerList(d.ctx, types.ContainerListOptions{All: true}) + if err != nil { + return err + } + + d.containerMu.Lock() + defer d.containerMu.Unlock() + + for _, c := range containers { + container := d.convertContainer(c) + d.containers[container.ID] = container + } + + utils.Logger.Info("Discovered %d containers", len(containers)) + return nil +} + +func (d *DockerCollector) monitorEvents() { + utils.Logger.Info("Starting Docker events monitoring") + + eventChan, errChan := d.client.Events(d.ctx, types.EventsOptions{}) + + for { + select { + case <-d.ctx.Done(): + return + + case event := <-eventChan: + d.handleDockerEvent(event) + + case err := <-errChan: + if err != nil && err != io.EOF { + utils.Logger.ErrorF("Error monitoring Docker events: %v", err) + } + } + } +} + +func (d *DockerCollector) handleDockerEvent(event events.Message) { + if event.Type != events.ContainerEventType { + return + } + + containerEvent := models.ContainerEvent{ + ID: uuid.New().String(), + ContainerID: event.Actor.ID, + Action: event.Action, + Timestamp: time.Unix(event.Time, 0), + Attributes: event.Actor.Attributes, + } + + select { + case d.eventChan <- containerEvent: + case <-d.ctx.Done(): + return + default: + utils.Logger.Info("Event channel is full, dropping event") + } + + switch event.Action { + case "start": + d.handleContainerStart(event.Actor.ID) + case "stop", "die", "kill": + d.handleContainerStop(event.Actor.ID) + case "destroy": + d.handleContainerDestroy(event.Actor.ID) + } +} + +func (d *DockerCollector) handleContainerStart(containerID string) { + containerJSON, err := d.client.ContainerInspect(d.ctx, containerID) + if err != nil { + utils.Logger.ErrorF("Failed to inspect container %s: %v", containerID, err) + return + } + + container := d.convertContainerJSON(containerJSON) + + d.containerMu.Lock() + d.containers[containerID] = container + d.containerMu.Unlock() + + if d.filter.ShouldCollect(container) { + go d.streamContainerLogs(container) + } +} + +func (d *DockerCollector) handleContainerStop(containerID string) { + d.containerMu.Lock() + if container, exists := d.containers[containerID]; exists { + utils.Logger.Info("Container %s stopped, removing from cache", container.Name) + delete(d.containers, containerID) + } + d.containerMu.Unlock() +} + +func (d *DockerCollector) handleContainerDestroy(containerID string) { + d.containerMu.Lock() + delete(d.containers, containerID) + d.containerMu.Unlock() +} + +func (d *DockerCollector) streamContainerLogs(container models.Container) { + utils.Logger.Info("Starting log stream for container: %s", container.Name) + + options := types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + Tail: "0", + Since: time.Now().Format(time.RFC3339), + Timestamps: false, + } + + maxRetries := 3 + for retry := 0; retry < maxRetries; retry++ { + reader, err := d.client.ContainerLogs(d.ctx, container.ID, options) + if err != nil { + utils.Logger.ErrorF("Failed to get logs for container %s (attempt %d/%d): %v", + container.Name, retry+1, maxRetries, err) + if retry < maxRetries-1 { + time.Sleep(time.Duration(retry+1) * time.Second) + continue + } + return + } + + d.processLogs(reader, container) + reader.Close() + return + } +} + +func (d *DockerCollector) processLogs(reader io.ReadCloser, container models.Container) { + scanner := bufio.NewScanner(reader) + logCount := 0 + + utils.Logger.Info("Processing logs for container: %s", container.Name) + + for scanner.Scan() { + select { + case <-d.ctx.Done(): + return + default: + } + + line := scanner.Text() + if err := d.processAndSendLogLine(line, container); err == nil { + logCount++ + } + } + + utils.Logger.Info("Log stream ended for container %s. Processed %d logs", container.Name, logCount) + + if err := scanner.Err(); err != nil && err != io.EOF { + utils.Logger.ErrorF("Log stream error for container %s: %v", container.Name, err) + go d.retryLogStream(container) + } +} + +func (d *DockerCollector) retryLogStream(container models.Container) { + select { + case <-d.ctx.Done(): + return + case <-time.After(30 * time.Second): + } + + containerJSON, err := d.client.ContainerInspect(d.ctx, container.ID) + if err == nil && containerJSON.State.Running { + utils.Logger.Info("Retrying log stream for container: %s", container.Name) + go d.streamContainerLogs(container) + return + } + + utils.Logger.Info("Container %s with ID %s not found, searching by name for Swarm replacement", container.Name, container.ID[:12]) + + containers, err := d.client.ContainerList(d.ctx, types.ContainerListOptions{All: false}) + if err != nil { + utils.Logger.ErrorF("Failed to list containers while searching for %s: %v", container.Name, err) + return + } + + for _, c := range containers { + containerName := "" + if len(c.Names) > 0 { + containerName = strings.TrimPrefix(c.Names[0], "/") + } + + if containerName == container.Name && c.State == "running" { + utils.Logger.Info("Found replacement container %s with new ID %s", container.Name, c.ID[:12]) + newContainer := d.convertContainer(c) + + d.containerMu.Lock() + delete(d.containers, container.ID) // Remove old id + d.containers[newContainer.ID] = newContainer // Add new Id + d.containerMu.Unlock() + + if d.filter.ShouldCollect(newContainer) { + go d.streamContainerLogs(newContainer) + } + return + } + } + + utils.Logger.Info("Container %s no longer running and no replacement found, stopping retry", container.Name) +} + +func (d *DockerCollector) processAndSendLogLine(line string, container models.Container) error { + if len(line) > 8 { + line = line[8:] + } + + cleaned, err := models.ParseLogLine(line, config.MaxLogLength) + if err != nil { + return err + } + + enrichedLog := models.EnrichLogWithContainer(cleaned, container.Name) + + if logservice.LogQueue != nil { + osInfo, err := utils.GetOsInfo() + if err != nil { + utils.Logger.ErrorF("Failed to get OS info: %v", err) + return err + } + + utmLog := &plugins.Log{ + Raw: enrichedLog, + DataType: config.DataType, + DataSource: osInfo.Hostname, + } + + d.sendToUTMStack(utmLog) + } + + return nil +} + +func (d *DockerCollector) sendToUTMStack(utmLog *plugins.Log) { + select { + case logservice.LogQueue <- utmLog: + case <-d.ctx.Done(): + return + default: + select { + case <-time.After(100 * time.Millisecond): + select { + case logservice.LogQueue <- utmLog: + default: + // Drop log if queue is still full + } + case <-d.ctx.Done(): + return + } + } +} + +func (d *DockerCollector) convertContainer(c types.Container) models.Container { + name := "" + if len(c.Names) > 0 { + name = strings.TrimPrefix(c.Names[0], "/") + } + + return models.Container{ + ID: c.ID, + Name: name, + Image: c.Image, + Status: c.Status, + State: c.State, + Created: time.Unix(c.Created, 0), + Labels: c.Labels, + } +} + +func (d *DockerCollector) convertContainerJSON(c types.ContainerJSON) models.Container { + var created time.Time + if c.Created != "" { + if parsedTime, err := time.Parse(time.RFC3339Nano, c.Created); err == nil { + created = parsedTime + } + } + + return models.Container{ + ID: c.ID, + Name: strings.TrimPrefix(c.Name, "/"), + Image: c.Config.Image, + Status: c.State.Status, + State: c.State.Status, + Created: created, + Labels: c.Config.Labels, + } +} + +func (d *DockerCollector) periodicRediscovery() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if err := d.rediscoverContainers(); err != nil { + utils.Logger.ErrorF("Error during rediscovery: %v", err) + } + case <-d.ctx.Done(): + return + } + } +} + +func (d *DockerCollector) rediscoverContainers() error { + containers, err := d.client.ContainerList(d.ctx, types.ContainerListOptions{All: true}) + if err != nil { + return utils.Logger.ErrorF("failed to list containers: %v", err) + } + + d.containerMu.Lock() + defer d.containerMu.Unlock() + + newContainers := 0 + for _, c := range containers { + if _, exists := d.containers[c.ID]; !exists { + container := d.convertContainer(c) + d.containers[container.ID] = container + newContainers++ + + if d.filter.ShouldCollect(container) { + go d.streamContainerLogs(container) + } + } + } + + if newContainers > 0 { + utils.Logger.Info("Rediscovered %d new containers", newContainers) + } + + return nil +} diff --git a/utmstack-collector/config/config.go b/utmstack-collector/config/config.go new file mode 100644 index 000000000..0e034c559 --- /dev/null +++ b/utmstack-collector/config/config.go @@ -0,0 +1,168 @@ +package config + +import ( + "os" + "sync" + + aesCrypt "github.com/AtlasInsideCorp/AtlasInsideAES" + "github.com/google/uuid" + "github.com/utmstack/UTMStack/utmstack-collector/utils" +) + +type MSGDS struct { + DataSource string + Message string +} + +type InstallationUUID struct { + UUID string `yaml:"uuid"` +} + +type Config struct { + Server string `yaml:"server"` + CollectorID uint `yaml:"collector-id"` + CollectorKey string `yaml:"collector-key"` + SkipCertValidation bool `yaml:"insecure"` +} + +func GetInitialConfig() (*Config, string) { + cnf := Config{ + Server: os.Args[2], + } + skip := os.Args[4] + if skip == "yes" { + cnf.SkipCertValidation = true + } else { + cnf.SkipCertValidation = false + } + return &cnf, os.Args[3] +} + +var ( + cnf = Config{} + confOnce sync.Once + installationId = "" + installationIdOnce sync.Once +) + +func GetCurrentConfig() (*Config, error) { + var errR error + confOnce.Do(func() { + uuidExists := utils.CheckIfPathExist(UUIDFileName) + + var encryptConfig Config + if err := utils.ReadYAML(ConfigurationFile, &encryptConfig); err != nil { + errR = utils.Logger.ErrorF("error reading config file: %v", err) + return + } + + var key []byte + var err error + if uuidExists { + id, err := GetUUID() + if err != nil { + errR = utils.Logger.ErrorF("failed to get uuid: %v", err) + return + } + + key, err = utils.GenerateKeyByUUID(REPLACE_KEY, id) + if err != nil { + errR = utils.Logger.ErrorF("error geneating key: %v", err) + return + } + } else { + key, err = utils.GenerateKey(REPLACE_KEY) + if err != nil { + errR = utils.Logger.ErrorF("error geneating key: %v", err) + return + } + } + + collectorKey, err := aesCrypt.AESDecrypt(encryptConfig.CollectorKey, key) + if err != nil { + errR = utils.Logger.ErrorF("error encoding collector key: %v", err) + return + } + + cnf.Server = encryptConfig.Server + cnf.CollectorID = encryptConfig.CollectorID + cnf.CollectorKey = collectorKey + cnf.SkipCertValidation = encryptConfig.SkipCertValidation + + if !uuidExists { + if err := SaveConfig(&cnf); err != nil { + errR = utils.Logger.ErrorF("error writing config file: %v", err) + return + } + } + }) + if errR != nil { + return nil, errR + } + return &cnf, nil +} + +func SaveConfig(cnf *Config) error { + id, err := GenerateNewUUID() + if err != nil { + return utils.Logger.ErrorF("failed to generate uuid: %v", err) + } + + key, err := utils.GenerateKeyByUUID(REPLACE_KEY, id) + if err != nil { + return utils.Logger.ErrorF("error geneating key: %v", err) + } + + collectorKey, err := aesCrypt.AESEncrypt(cnf.CollectorKey, key) + if err != nil { + return utils.Logger.ErrorF("error encoding agent key: %v", err) + } + + encryptConf := &Config{ + Server: cnf.Server, + CollectorID: cnf.CollectorID, + CollectorKey: collectorKey, + SkipCertValidation: cnf.SkipCertValidation, + } + + if err := utils.WriteYAML(ConfigurationFile, encryptConf); err != nil { + return err + } + return nil +} + +func GenerateNewUUID() (string, error) { + id, err := uuid.NewRandom() + if err != nil { + return "", utils.Logger.ErrorF("failed to generate uuid: %v", err) + } + + InstallationUUID := InstallationUUID{ + UUID: id.String(), + } + + if err = utils.WriteYAML(UUIDFileName, InstallationUUID); err != nil { + return "", utils.Logger.ErrorF("error writing uuid file: %v", err) + } + + return InstallationUUID.UUID, nil +} + +func GetUUID() (string, error) { + var errR error + installationIdOnce.Do(func() { + var id = InstallationUUID{} + if err := utils.ReadYAML(UUIDFileName, &id); err != nil { + errR = utils.Logger.ErrorF("error reading uuid file: %v", err) + return + } + + installationId = id.UUID + }) + + if errR != nil { + return "", errR + } + + return installationId, nil +} diff --git a/utmstack-collector/config/const.go b/utmstack-collector/config/const.go new file mode 100644 index 000000000..fed31a69f --- /dev/null +++ b/utmstack-collector/config/const.go @@ -0,0 +1,75 @@ +package config + +import ( + "path/filepath" + + "github.com/utmstack/UTMStack/utmstack-collector/models" + "github.com/utmstack/UTMStack/utmstack-collector/utils" +) + +const ( + REPLACE_KEY string = "" + DockerHost string = "" + DockerAPIVersion string = "1.41" + MaxLogLength int = 32768 + BufferSize int = 50000 + + DataType string = "utmstack" +) + +var ( + DependUrl = "https://%s:%s/private/dependencies/collector/%s" + AgentManagerPort = "9000" + LogAuthProxyPort = "50051" + DependenciesPort = "9001" + + ServiceLogFile = filepath.Join(utils.GetMyPath(), "logs", "utmstack_collector.log") + UUIDFileName = filepath.Join(utils.GetMyPath(), "uuid.yml") + ConfigurationFile = filepath.Join(utils.GetMyPath(), "config.yml") + RetentionConfigFile = filepath.Join(utils.GetMyPath(), "retention.json") + LogsDBFile = filepath.Join(utils.GetMyPath(), "logs_process", "logs.db") + VersionPath = filepath.Join(utils.GetMyPath(), "version.json") +) + +var FilterRules = []models.FilterRule{ + { + Type: "name", + Pattern: ".*postgres.*", // Exclude containers with "postgres" in the name + Action: "exclude", + }, + { + Type: "name", + Pattern: ".*logstash.*", // Exclude containers with "logstash" in the name + Action: "exclude", + }, + { + Type: "name", + Pattern: ".*mutate.*", // Exclude containers with "mutate" in the name + Action: "exclude", + }, + { + Type: "name", + Pattern: ".*node1.*", // Exclude containers with "node1" in the name + Action: "exclude", + }, + { + Type: "name", + Pattern: ".*filebrowser.*", // Exclude containers with "filebrowser" in the name + Action: "exclude", + }, + { + Type: "name", + Pattern: ".*user-auditor.*", // Exclude containers with "user-auditor" in the name + Action: "exclude", + }, + { + Type: "name", + Pattern: ".*web-pdf.*", // Exclude containers with "web-pdf" in the name + Action: "exclude", + }, + { + Type: "name", + Pattern: ".*frontend.*", // Exclude containers with "frontend" in the name + Action: "exclude", + }, +} diff --git a/utmstack-collector/conn/conn.go b/utmstack-collector/conn/conn.go new file mode 100644 index 000000000..75c36fb08 --- /dev/null +++ b/utmstack-collector/conn/conn.go @@ -0,0 +1,106 @@ +package conn + +import ( + "crypto/tls" + "sync" + "time" + + "github.com/utmstack/UTMStack/utmstack-collector/config" + "github.com/utmstack/UTMStack/utmstack-collector/utils" + "google.golang.org/grpc" + "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/credentials" +) + +const ( + maxMessageSize = 1024 * 1024 * 1024 + maxConnectionAttempts = 3 + initialReconnectDelay = 10 * time.Second + maxReconnectDelay = 60 * time.Second +) + +var ( + correlationConn *grpc.ClientConn + correlationConnOnce sync.Once + agentManagerConn *grpc.ClientConn + agentManagerConnOnce sync.Once +) + +func GetAgentManagerConnection(cnf *config.Config) (*grpc.ClientConn, error) { + var err error + agentManagerConnOnce.Do(func() { + agentManagerConn, err = connectToServer(cnf.Server, config.AgentManagerPort, cnf.SkipCertValidation) + if err != nil { + err = utils.Logger.ErrorF("error connecting to Agent Manager: %v", err) + } + }) + if err != nil { + return nil, err + } + + state := agentManagerConn.GetState() + if state == connectivity.Shutdown || state == connectivity.TransientFailure { + agentManagerConn.Close() + agentManagerConn, err = connectToServer(cnf.Server, config.AgentManagerPort, cnf.SkipCertValidation) + if err != nil { + return nil, utils.Logger.ErrorF("error connecting to Agent Manager: %v", err) + } + } + + return agentManagerConn, nil +} + +func GetCorrelationConnection(cnf *config.Config) (*grpc.ClientConn, error) { + var err error + correlationConnOnce.Do(func() { + correlationConn, err = connectToServer(cnf.Server, config.LogAuthProxyPort, cnf.SkipCertValidation) + if err != nil { + err = utils.Logger.ErrorF("error connecting to Correlation: %v", err) + } + }) + if err != nil { + return nil, err + } + + state := correlationConn.GetState() + if state == connectivity.Shutdown || state == connectivity.TransientFailure { + correlationConn.Close() + correlationConn, err = connectToServer(cnf.Server, config.LogAuthProxyPort, cnf.SkipCertValidation) + if err != nil { + return nil, utils.Logger.ErrorF("error connecting to Correlation: %v", err) + } + } + + return correlationConn, nil +} + +func connectToServer(addrs, port string, skip bool) (*grpc.ClientConn, error) { + connectionAttemps := 0 + reconnectDelay := initialReconnectDelay + + serverAddress := addrs + ":" + port + var conn *grpc.ClientConn + var err error + + for { + if connectionAttemps >= maxConnectionAttempts { + return nil, utils.Logger.ErrorF("failed to connect to Server: %v", err) + } + + conn, err = grpc.NewClient( + serverAddress, + grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMessageSize)), + grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: skip}))) + if err != nil { + connectionAttemps++ + utils.Logger.ErrorF("error connecting to Server, trying again in %.0f seconds", reconnectDelay.Seconds()) + time.Sleep(reconnectDelay) + reconnectDelay = utils.IncrementReconnectDelay(reconnectDelay, maxReconnectDelay) + continue + } + + break + } + + return conn, nil +} diff --git a/utmstack-collector/database/db.go b/utmstack-collector/database/db.go new file mode 100644 index 000000000..b08d94778 --- /dev/null +++ b/utmstack-collector/database/db.go @@ -0,0 +1,129 @@ +package database + +import ( + "errors" + "fmt" + "log" + "os" + "path/filepath" + "sync" + + "github.com/glebarez/sqlite" + "github.com/utmstack/UTMStack/utmstack-collector/config" + "github.com/utmstack/UTMStack/utmstack-collector/utils" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var ( + dbInstance *Database + dbOnce sync.Once +) + +type Database struct { + db *gorm.DB + locker sync.RWMutex +} + +func (d *Database) Migrate(data interface{}) error { + return d.db.AutoMigrate(data) +} + +func (d *Database) Create(data interface{}) error { + return d.db.Create(data).Error +} + +func (d *Database) Find(data interface{}, field string, value interface{}) (bool, error) { + err := d.db.Where(fmt.Sprintf("%v = ?", field), value).Find(data).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil + } + return false, err + } + return true, nil +} + +func (d *Database) GetAll(data interface{}) error { + if err := d.db.Find(data).Error; err != nil { + return err + } + return nil +} + +func (d *Database) Update(data interface{}, searchField string, searchValue string, modifyField string, newValue interface{}) error { + return d.db.Model(data).Where(fmt.Sprintf("%v = ?", searchField), searchValue).Update(modifyField, newValue).Error +} + +func (d *Database) Delete(data interface{}, field string, value string) error { + return d.db.Where(fmt.Sprintf("%v = ?", field), value).Delete(data).Error +} + +func (d *Database) DeleteOld(data interface{}, retentionMegabytes int) (int, error) { + currentSize, err := GetDatabaseSizeInMB() + if err != nil { + return 0, utils.Logger.ErrorF("error getting database size: %v", err) + } + + var rowsAffected int + for currentSize > retentionMegabytes { + result := d.db.Where("1 = 1").Order("created_at ASC").Limit(10).Delete(data) + if result.Error != nil { + return rowsAffected, result.Error + } + rowsAffected += int(result.RowsAffected) + d.db.Exec("VACUUM;") + currentSize, err = GetDatabaseSizeInMB() + if err != nil { + return rowsAffected, utils.Logger.ErrorF("error getting database size: %v", err) + } + } + + return rowsAffected, nil +} + +func (d *Database) Lock() { + d.locker.Lock() +} + +func (d *Database) Unlock() { + d.locker.Unlock() +} + +func GetDB() *Database { + dbOnce.Do(func() { + path := filepath.Join(utils.GetMyPath(), "logs_process") + err := utils.CreatePathIfNotExist(path) + if err != nil { + log.Fatalf("error creating database path: %v", err) + } + path = config.LogsDBFile + if _, err := os.Stat(path); os.IsNotExist(err) { + file, err := os.Create(path) + if err != nil { + log.Fatalf("error creating database file: %v", err) + } + file.Close() + } + + conn, err := gorm.Open(sqlite.Open(path), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if err != nil { + log.Fatalf("error connecting with database: %v", err) + } + + dbInstance = &Database{db: conn} + + }) + + return dbInstance +} + +func GetDatabaseSizeInMB() (int, error) { + fileInfo, err := os.Stat(config.LogsDBFile) + if err != nil { + return 0, err + } + return int(fileInfo.Size() / (1024 * 1024)), nil +} diff --git a/utmstack-collector/go.mod b/utmstack-collector/go.mod new file mode 100644 index 000000000..cf0e5d24f --- /dev/null +++ b/utmstack-collector/go.mod @@ -0,0 +1,84 @@ +module github.com/utmstack/UTMStack/utmstack-collector + +go 1.25.0 + +require ( + github.com/AtlasInsideCorp/AtlasInsideAES v1.0.0 + github.com/docker/docker v24.0.7+incompatible + github.com/elastic/go-sysinfo v1.15.4 + github.com/glebarez/sqlite v1.11.0 + github.com/google/uuid v1.6.0 + github.com/kardianos/service v1.2.4 + github.com/threatwinds/go-sdk v1.0.45 + github.com/threatwinds/logger v1.2.2 + google.golang.org/grpc v1.75.1 + google.golang.org/protobuf v1.36.9 + gopkg.in/yaml.v2 v2.4.0 + gorm.io/gorm v1.31.0 +) + +require ( + cel.dev/expr v0.24.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/elastic/go-windows v1.0.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.10.1 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/arch v0.19.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.2 // indirect + howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect + sigs.k8s.io/yaml v1.5.0 // indirect +) diff --git a/utmstack-collector/go.sum b/utmstack-collector/go.sum new file mode 100644 index 000000000..34cbcc7d8 --- /dev/null +++ b/utmstack-collector/go.sum @@ -0,0 +1,251 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +github.com/AtlasInsideCorp/AtlasInsideAES v1.0.0 h1:TBiBl9KCa4i4epY0/q9WSC4ugavL6+6JUkOXWDnMM6I= +github.com/AtlasInsideCorp/AtlasInsideAES v1.0.0/go.mod h1:cRhQ3TS/VEfu/z+qaciyuDZdtxgaXgaX8+G6Wa5NzBk= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elastic/go-sysinfo v1.15.4 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q= +github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU= +github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI= +github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk= +github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/threatwinds/go-sdk v1.0.45 h1:KZ3s3HviNRrOkg5EqjFnoauANFFzTqjNFyshPLY2SoI= +github.com/threatwinds/go-sdk v1.0.45/go.mod h1:tcWn6r6vqID/W/nL3UKfc5NafA3V/cSkiLvfJnwB58c= +github.com/threatwinds/logger v1.2.2 h1:sVuT8yhbecPqP4tT8EwHfp1czNC6e1wdkE1ihNnuBdA= +github.com/threatwinds/logger v1.2.2/go.mod h1:Amq0QI1y7fkTpnBUgeGVu2Z/C4u4ys2pNLUOuj3UAAU= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU= +golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074 h1:mVXdvnmR3S3BQOqHECm9NGMjYiRtEvDYcqAqedTXY6s= +google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:vYFwMYFbmA8vl6Z/krj/h7+U/AqpHknwJX4Uqgfyc7I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074 h1:qJW29YvkiJmXOYMu5Tf8lyrTp3dOS+K4z6IixtLaCf8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M= +howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= +sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= diff --git a/utmstack-collector/logservice/processor.go b/utmstack-collector/logservice/processor.go new file mode 100644 index 000000000..c923de220 --- /dev/null +++ b/utmstack-collector/logservice/processor.go @@ -0,0 +1,262 @@ +package logservice + +import ( + "context" + "errors" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/google/uuid" + "github.com/threatwinds/go-sdk/plugins" + + "github.com/utmstack/UTMStack/utmstack-collector/agent" + "github.com/utmstack/UTMStack/utmstack-collector/config" + "github.com/utmstack/UTMStack/utmstack-collector/conn" + "github.com/utmstack/UTMStack/utmstack-collector/database" + "github.com/utmstack/UTMStack/utmstack-collector/models" + "github.com/utmstack/UTMStack/utmstack-collector/utils" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type LogProcessor struct { + db *database.Database + connErrWritten bool + ackErrWritten bool + sendErrWritten bool +} + +var ( + processor LogProcessor + processorOnce sync.Once + LogQueue = make(chan *plugins.Log) + timeToSleep = 10 * time.Second + timeCLeanLogs = 10 * time.Minute +) + +func GetLogProcessor() LogProcessor { + processorOnce.Do(func() { + processor = LogProcessor{ + db: database.GetDB(), + connErrWritten: false, + ackErrWritten: false, + sendErrWritten: false, + } + }) + return processor +} + +func (l *LogProcessor) ProcessLogs(cnf *config.Config, ctx context.Context) { + go l.CleanCountedLogs() + + for { + ctxEof, cancelEof := context.WithCancel(context.Background()) + connection, err := conn.GetCorrelationConnection(cnf) + if err != nil { + if !l.connErrWritten { + utils.Logger.ErrorF("error connecting to Correlation: %v", err) + l.connErrWritten = true + } + time.Sleep(10 * time.Second) + continue + } + + client := plugins.NewIntegrationClient(connection) + plClient := createClient(client, ctx) + l.connErrWritten = false + + go l.handleAcknowledgements(plClient, ctxEof, cancelEof) + l.processLogs(plClient, ctxEof, cancelEof) + } +} + +func (l *LogProcessor) handleAcknowledgements(plClient plugins.Integration_ProcessLogClient, ctx context.Context, cancel context.CancelFunc) { + for { + select { + case <-ctx.Done(): + return + default: + ack, err := plClient.Recv() + if err != nil { + if strings.Contains(err.Error(), "EOF") { + time.Sleep(timeToSleep) + cancel() + return + } + st, ok := status.FromError(err) + if ok && (st.Code() == codes.Unavailable || st.Code() == codes.Canceled) { + if !l.ackErrWritten { + utils.Logger.ErrorF("failed to receive ack: %v", err) + l.ackErrWritten = true + } + time.Sleep(timeToSleep) + cancel() + return + } else { + if !l.ackErrWritten { + utils.Logger.ErrorF("failed to receive ack: %v", err) + l.ackErrWritten = true + } + time.Sleep(timeToSleep) + continue + } + } + + l.ackErrWritten = false + + l.db.Lock() + err = l.db.Update(&models.Log{}, "id", ack.LastId, "processed", true) + if err != nil { + utils.Logger.ErrorF("failed to update log: %v", err) + } + l.db.Unlock() + } + } +} + +func (l *LogProcessor) processLogs(plClient plugins.Integration_ProcessLogClient, ctx context.Context, cancel context.CancelFunc) { + for { + select { + case <-ctx.Done(): + utils.Logger.Info("context done, exiting processLogs") + return + case newLog := <-LogQueue: + id, err := uuid.NewRandom() + if err != nil { + utils.Logger.ErrorF("failed to generate uuid: %v", err) + continue + } + + newLog.Id = id.String() + l.db.Lock() + err = l.db.Create(&models.Log{ID: newLog.Id, Log: newLog.Raw, Type: newLog.DataType, CreatedAt: time.Now(), DataSource: newLog.DataSource, Processed: false}) + if err != nil { + utils.Logger.ErrorF("failed to save log: %v :log: %s", err, newLog.Raw) + } + l.db.Unlock() + + err = plClient.Send(newLog) + if err != nil { + if strings.Contains(err.Error(), "EOF") { + time.Sleep(timeToSleep) + cancel() + return + } + st, ok := status.FromError(err) + if ok && (st.Code() == codes.Unavailable || st.Code() == codes.Canceled) { + if !l.sendErrWritten { + utils.Logger.ErrorF("failed to send log: %v :log: %s", err, newLog.Raw) + l.sendErrWritten = true + } + time.Sleep(timeToSleep) + cancel() + return + } else { + if !l.sendErrWritten { + utils.Logger.ErrorF("failed to send log: %v :log: %s", err, newLog.Raw) + l.sendErrWritten = true + } + time.Sleep(timeToSleep) + continue + } + } + l.sendErrWritten = false + } + } +} + +func (l *LogProcessor) CleanCountedLogs() { + ticker := time.NewTicker(timeCLeanLogs) + defer ticker.Stop() + for range ticker.C { + dataRetention, err := GetDataRetention() + if err != nil { + utils.Logger.ErrorF("error getting data retention: %s", err) + continue + } + l.db.Lock() + _, err = l.db.DeleteOld(&models.Log{}, dataRetention) + if err != nil { + utils.Logger.ErrorF("error deleting old logs: %s", err) + } + l.db.Unlock() + + unprocessed := make([]models.Log, 0, 10) + l.db.Lock() + found, err := l.db.Find(&unprocessed, "processed", false) + l.db.Unlock() + if err != nil { + utils.Logger.ErrorF("error finding unprocessed logs: %s", err) + continue + } + + if found { + for _, log := range unprocessed { + LogQueue <- &plugins.Log{ + Id: log.ID, + Raw: log.Log, + DataType: log.Type, + DataSource: log.DataSource, + Timestamp: log.CreatedAt.Format(time.RFC3339Nano), + } + } + } + } +} + +func createClient(client plugins.IntegrationClient, ctx context.Context) plugins.Integration_ProcessLogClient { + var connErrMsgWritten bool + invalidKeyCounter := 0 + for { + plClient, err := client.ProcessLog(ctx) + if err != nil { + if strings.Contains(err.Error(), "invalid agent key") { + invalidKeyCounter++ + if invalidKeyCounter >= 20 { + utils.Logger.Info("Uninstalling agent: reason: agent has been removed from the panel...") + _ = agent.UninstallAll() + os.Exit(1) + } + } else { + invalidKeyCounter = 0 + } + if !connErrMsgWritten { + utils.Logger.ErrorF("failed to create input client: %v", err) + connErrMsgWritten = true + } + time.Sleep(timeToSleep) + continue + } + return plClient + } +} + +func SetDataRetention(retention string) error { + if retention == "" { + retention = "20" + } + + retentionInt, err := strconv.Atoi(retention) + if err != nil { + return errors.New("retention must be a number (number of megabytes)") + } + + if retentionInt < 1 { + return errors.New("retention must be greater than 0") + } + + return utils.WriteJSON(config.RetentionConfigFile, models.DataRetention{Retention: retentionInt}) +} + +func GetDataRetention() (int, error) { + retention := models.DataRetention{} + err := utils.ReadJson(config.RetentionConfigFile, &retention) + if err != nil { + return 0, err + } + + return retention.Retention, nil +} diff --git a/utmstack-collector/main.go b/utmstack-collector/main.go new file mode 100644 index 000000000..20abf046b --- /dev/null +++ b/utmstack-collector/main.go @@ -0,0 +1,172 @@ +package main + +import ( + "fmt" + "os" + "time" + + pb "github.com/utmstack/UTMStack/utmstack-collector/agent" + "github.com/utmstack/UTMStack/utmstack-collector/config" + "github.com/utmstack/UTMStack/utmstack-collector/database" + "github.com/utmstack/UTMStack/utmstack-collector/logservice" + "github.com/utmstack/UTMStack/utmstack-collector/models" + "github.com/utmstack/UTMStack/utmstack-collector/serv" + "github.com/utmstack/UTMStack/utmstack-collector/updates" + "github.com/utmstack/UTMStack/utmstack-collector/utils" +) + +func main() { + utils.InitLogger(config.ServiceLogFile) + + if len(os.Args) > 1 { + arg := os.Args[1] + + isInstalled, err := utils.CheckIfServiceIsInstalled("UTMStackCollector") + if err != nil { + fmt.Println("Error checking if service is installed: ", err) + os.Exit(1) + } + if arg != "install" && !isInstalled { + fmt.Println("UTMStackCollector service is not installed") + os.Exit(1) + } else if arg == "install" && isInstalled { + fmt.Println("UTMStackCollector service is already installed") + os.Exit(1) + } + + switch arg { + case "run": + serv.RunService() + case "install": + utils.PrintBanner() + fmt.Println("Installing UTMStackCollector service ...") + + fmt.Println("[OK]") + + cnf, utmKey := config.GetInitialConfig() + + fmt.Print("Checking server connection ... ") + if err := utils.ArePortsReachable(cnf.Server, config.AgentManagerPort, config.LogAuthProxyPort, config.DependenciesPort); err != nil { + fmt.Println("\nError trying to connect to server: ", err) + os.Exit(1) + } + fmt.Println("[OK]") + + fmt.Print("Downloading Version ... ") + if err := updates.DownloadVersion(cnf.Server, cnf.SkipCertValidation); err != nil { + fmt.Println("\nError downloading version: ", err) + os.Exit(1) + } + fmt.Println("[OK]") + + fmt.Print("Configuring collector ... ") + err = pb.RegisterCollector(cnf, utmKey) + if err != nil { + fmt.Println("\nError registering collector: ", err) + os.Exit(1) + } + if err = config.SaveConfig(cnf); err != nil { + fmt.Println("\nError saving config: ", err) + os.Exit(1) + } + + if err := logservice.SetDataRetention(""); err != nil { + fmt.Println("\nError setting retention: ", err) + os.Exit(1) + } + fmt.Println("[OK]") + + fmt.Print(("Creating service ... ")) + serv.InstallService() + fmt.Println("[OK]") + fmt.Println("UTMStackCollector service installed correctly") + + case "change-retention": + fmt.Println("Changing log retention ...") + retention := os.Args[2] + + if err := logservice.SetDataRetention(retention); err != nil { + fmt.Println("Error trying to change retention: ", err) + os.Exit(1) + } + + fmt.Printf("Retention changed correctly to %s\n", retention) + time.Sleep(5 * time.Second) + + case "clean-logs": + fmt.Println("Cleaning old logs ...") + db := database.GetDB() + datR, err := logservice.GetDataRetention() + if err != nil { + fmt.Println("Error getting retention: ", err) + os.Exit(1) + } + _, err = db.DeleteOld(models.Log{}, datR) + if err != nil { + fmt.Println("Error cleaning logs: ", err) + os.Exit(1) + } + fmt.Println("Logs cleaned correctly") + time.Sleep(5 * time.Second) + + case "uninstall": + fmt.Println("Uninstalling UTMStackCollector service ...") + + cnf, err := config.GetCurrentConfig() + if err != nil { + fmt.Println("Error getting config: ", err) + os.Exit(1) + } + if err = pb.DeleteAgent(cnf); err != nil { + utils.Logger.ErrorF("error deleting collector: %v", err) + } + + os.Remove(config.ConfigurationFile) + + serv.UninstallService() + + fmt.Println("[OK]") + fmt.Println("UTMStackCollector service uninstalled correctly") + os.Exit(1) + case "help": + Help() + default: + fmt.Println("unknown option") + } + } else { + serv.RunService() + } +} + +func Help() { + fmt.Println("### UTMStack Collector ###") + fmt.Println("Usage:") + fmt.Println(" To run the service: ./utmstack_collector run") + fmt.Println(" To install the service: ./utmstack_collector install") + fmt.Println(" To change log retention: ./utmstack_collector change-retention ") + fmt.Println(" To clean old logs: ./utmstack_collector clean-logs") + fmt.Println(" To uninstall the service: ./utmstack_collector uninstall") + fmt.Println(" To debug UTMStack installation: ./utmstack_collector debug-utmstack") + fmt.Println(" For help (this message): ./utmstack_collector help") + fmt.Println() + fmt.Println("Options:") + fmt.Println(" run Run the UTMStackCollector service") + fmt.Println(" install Install the UTMStackCollector service") + fmt.Println(" change-retention Change the log retention to . Retention must be a number of megabytes. Example: 20") + fmt.Println(" clean-logs Clean old logs from the database") + fmt.Println(" uninstall Uninstall the UTMStackCollector service") + fmt.Println(" debug-utmstack Debug UTMStack installation validation") + fmt.Println(" help Display this help message") + fmt.Println() + fmt.Println("Requirements:") + fmt.Println(" - UTMStack must be installed on this system") + fmt.Println(" - File /utmstack.yaml must exist in root directory") + fmt.Println(" - Directory /utmstack/ must exist") + fmt.Println() + fmt.Println("Note:") + fmt.Println(" - Make sure to run commands with appropriate permissions.") + fmt.Println(" - All commands require administrative privileges.") + fmt.Println(" - For detailed logs, check the service log file.") + fmt.Println() + os.Exit(0) +} diff --git a/utmstack-collector/models/data.go b/utmstack-collector/models/data.go new file mode 100644 index 000000000..051820585 --- /dev/null +++ b/utmstack-collector/models/data.go @@ -0,0 +1,5 @@ +package models + +type DataRetention struct { + Retention int `json:"retention"` +} diff --git a/utmstack-collector/models/docker.go b/utmstack-collector/models/docker.go new file mode 100644 index 000000000..35aefc121 --- /dev/null +++ b/utmstack-collector/models/docker.go @@ -0,0 +1,144 @@ +package models + +import ( + "encoding/json" + "regexp" + "strings" +) + +func NewContainerFilter(rules []FilterRule) *ContainerFilter { + return &ContainerFilter{rules: rules} +} + +func (f *ContainerFilter) ShouldCollect(container Container) bool { + defaultExcludes := []string{ + "k8s.gcr.io", + "registry.k8s.io", + "quay.io/coreos", + "gcr.io/google-containers", + "docker.io/library/pause", + } + + for _, exclude := range defaultExcludes { + if strings.HasPrefix(container.Image, exclude) { + return false + } + } + + if container.Labels != nil { + if exclude, exists := container.Labels["logs.exclude"]; exists && exclude == "true" { + return false + } + } + + for _, rule := range f.rules { + if f.matchesRule(container, rule) { + return rule.Action == "include" + } + } + + return container.State == "running" || + strings.Contains(container.Status, "Up") || + (container.State == "exited" && strings.Contains(container.Status, "Exited (0)")) // Contenedores que terminaron exitosamente +} + +func (f *ContainerFilter) matchesRule(container Container, rule FilterRule) bool { + switch rule.Type { + case "image": + if rule.Pattern == "*" { + return true + } + if matched, _ := regexp.MatchString(rule.Pattern, container.Image); matched { + return true + } + case "name": + if matched, _ := regexp.MatchString(rule.Pattern, container.Name); matched { + return true + } + case "label": + parts := strings.SplitN(rule.Pattern, "=", 2) + if len(parts) == 2 { + key, value := parts[0], parts[1] + if container.Labels[key] == value { + return true + } + } + } + return false +} + +func ParseLogLine(line string, maxLength int) (string, error) { + cleaned := regexp.MustCompile(`[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]`).ReplaceAllString(line, "") + + if len(cleaned) > maxLength { + cleaned = cleaned[:maxLength] + } + + if strings.TrimSpace(cleaned) == "" { + return "", ErrEmptyLogLine + } + + return cleaned, nil +} + +func EnrichLogWithContainer(logLine, containerName string) string { + if !isValidJSON(logLine) { + return logLine + } + + var logData map[string]interface{} + if err := json.Unmarshal([]byte(logLine), &logData); err != nil { + return logLine + } + + cleanName := CleanContainerName(containerName) + logData["containerName"] = cleanName + + enrichedBytes, err := json.Marshal(logData) + if err != nil { + return logLine + } + + return string(enrichedBytes) +} + +func isValidJSON(str string) bool { + str = strings.TrimSpace(str) + return (strings.HasPrefix(str, "{") && strings.HasSuffix(str, "}")) || + (strings.HasPrefix(str, "[") && strings.HasSuffix(str, "]")) +} + +func CleanContainerName(containerName string) string { + swarmPattern := `\.\d+\.[a-zA-Z0-9]+$` + re := regexp.MustCompile(swarmPattern) + cleaned := re.ReplaceAllString(containerName, "") + + composePattern := `[_-]\d+$` + re2 := regexp.MustCompile(composePattern) + cleaned = re2.ReplaceAllString(cleaned, "") + + return cleaned +} + +var ( + ErrEmptyLogLine = NewError("empty log line after cleaning") +) + +type Error struct { + Message string +} + +func NewError(msg string) *Error { + return &Error{Message: msg} +} + +func (e *Error) Error() string { + return e.Message +} + +func SafeContainerID(id string) string { + if len(id) >= 12 { + return id[:12] + } + return id +} diff --git a/utmstack-collector/models/schema.go b/utmstack-collector/models/schema.go new file mode 100644 index 000000000..d392e1e6e --- /dev/null +++ b/utmstack-collector/models/schema.go @@ -0,0 +1,49 @@ +package models + +import ( + "time" +) + +type Container struct { + ID string `json:"id"` + Name string `json:"name"` + Image string `json:"image"` + Status string `json:"status"` + State string `json:"state"` + Created time.Time `json:"created"` + Labels map[string]string `json:"labels"` +} + +type LogEntry struct { + ID string `json:"id"` + ContainerID string `json:"container_id"` + ContainerName string `json:"container_name"` + Message string `json:"message"` +} + +type ContainerEvent struct { + ID string `json:"id"` + ContainerID string `json:"container_id"` + Action string `json:"action"` // start, stop, die, etc. + Timestamp time.Time `json:"timestamp"` + Attributes map[string]string `json:"attributes,omitempty"` +} + +type FilterRule struct { + Type string `yaml:"type"` // image, label, name + Pattern string `yaml:"pattern"` // regex pattern o string exacto + Action string `yaml:"action"` // include, exclude +} + +type ContainerFilter struct { + rules []FilterRule +} + +type Log struct { + ID string `gorm:"index"` + CreatedAt time.Time + DataSource string + Type string + Log string + Processed bool +} diff --git a/utmstack-collector/models/version.go b/utmstack-collector/models/version.go new file mode 100644 index 000000000..ed01f06e4 --- /dev/null +++ b/utmstack-collector/models/version.go @@ -0,0 +1,5 @@ +package models + +type Version struct { + Version string `json:"version"` +} diff --git a/utmstack-collector/serv/config.go b/utmstack-collector/serv/config.go new file mode 100644 index 000000000..1c4ca9cb5 --- /dev/null +++ b/utmstack-collector/serv/config.go @@ -0,0 +1,17 @@ +package serv + +import ( + "github.com/kardianos/service" +) + +// GetConfigServ creates and returns a pointer to a service configuration structure. +func GetConfigServ() *service.Config { + svcConfig := &service.Config{ + Name: "UTMStackCollector", + DisplayName: "UTMStack Collector", + Description: "UTMStack Collector Service", + Arguments: []string{"run"}, + } + + return svcConfig +} diff --git a/utmstack-collector/serv/install.go b/utmstack-collector/serv/install.go new file mode 100644 index 000000000..06ab38a06 --- /dev/null +++ b/utmstack-collector/serv/install.go @@ -0,0 +1,29 @@ +package serv + +import ( + "fmt" + "os" + + "github.com/kardianos/service" +) + +func InstallService() { + svcConfig := GetConfigServ() + prg := new(program) + newService, err := service.New(prg, svcConfig) + if err != nil { + fmt.Println("\nError creating new service: ", err) + os.Exit(1) + } + err = newService.Install() + if err != nil { + fmt.Println("\nError installing new service: ", err) + os.Exit(1) + } + + err = newService.Start() + if err != nil { + fmt.Println("\nError starting new service: ", err) + os.Exit(1) + } +} diff --git a/utmstack-collector/serv/run.go b/utmstack-collector/serv/run.go new file mode 100644 index 000000000..866278449 --- /dev/null +++ b/utmstack-collector/serv/run.go @@ -0,0 +1,21 @@ +package serv + +import ( + "github.com/kardianos/service" + "github.com/utmstack/UTMStack/utmstack-collector/utils" +) + +func RunService() { + svcConfig := GetConfigServ() + p := new(program) + + newService, err := service.New(p, svcConfig) + if err != nil { + utils.Logger.Fatal("error creating new service: %v", err) + } + + err = newService.Run() + if err != nil { + utils.Logger.Fatal("error running new service: %v", err) + } +} diff --git a/utmstack-collector/serv/service.go b/utmstack-collector/serv/service.go new file mode 100644 index 000000000..e9e5d786b --- /dev/null +++ b/utmstack-collector/serv/service.go @@ -0,0 +1,74 @@ +package serv + +import ( + "context" + "os" + "os/signal" + "strconv" + "syscall" + + "github.com/kardianos/service" + + pb "github.com/utmstack/UTMStack/utmstack-collector/agent" + "github.com/utmstack/UTMStack/utmstack-collector/collector" + "github.com/utmstack/UTMStack/utmstack-collector/config" + "github.com/utmstack/UTMStack/utmstack-collector/database" + "github.com/utmstack/UTMStack/utmstack-collector/logservice" + "github.com/utmstack/UTMStack/utmstack-collector/models" + "github.com/utmstack/UTMStack/utmstack-collector/utils" + "google.golang.org/grpc/metadata" +) + +type program struct{} + +func (p *program) Start(_ service.Service) error { + go p.run() + return nil +} + +func (p *program) Stop(_ service.Service) error { + // TODO: implement this function + return nil +} + +func (p *program) run() { + utils.InitLogger(config.ServiceLogFile) + cnf, err := config.GetCurrentConfig() + if err != nil { + utils.Logger.Fatal("error getting config: %v", err) + } + + db := database.GetDB() + err = db.Migrate(models.Log{}) + if err != nil { + utils.Logger.ErrorF("error migrating logs table: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + ctx = metadata.AppendToOutgoingContext(ctx, "key", cnf.CollectorKey) + ctx = metadata.AppendToOutgoingContext(ctx, "id", strconv.Itoa(int(cnf.CollectorID))) + ctx = metadata.AppendToOutgoingContext(ctx, "type", "collector") + + go pb.StartPing(cnf, ctx) + + logProcessor := logservice.GetLogProcessor() + go logProcessor.ProcessLogs(cnf, ctx) + + // Start UTMStack log collector + dockerConfig := collector.DefaultConfig() + dockerCollector, err := collector.NewDockerCollector(dockerConfig) + if err != nil { + utils.Logger.ErrorF("failed to create UTMStack Collector: %v", err) + } else { + go func() { + if err := dockerCollector.Start(); err != nil { + utils.Logger.ErrorF("failed to start UTMStack Collector: %v", err) + } + }() + } + + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + <-signals +} diff --git a/utmstack-collector/serv/uninstall.go b/utmstack-collector/serv/uninstall.go new file mode 100644 index 000000000..cdc39eee4 --- /dev/null +++ b/utmstack-collector/serv/uninstall.go @@ -0,0 +1,16 @@ +package serv + +import ( + "github.com/utmstack/UTMStack/utmstack-collector/utils" +) + +func UninstallService() { + err := utils.StopService("UTMStackCollector") + if err != nil { + utils.Logger.Fatal("error stopping UTMStackCollector: %v", err) + } + err = utils.UninstallService("UTMStackCollector") + if err != nil { + utils.Logger.Fatal("error uninstalling UTMStackCollector: %v", err) + } +} diff --git a/utmstack-collector/updates/dependencies.go b/utmstack-collector/updates/dependencies.go new file mode 100644 index 000000000..0c96a2b33 --- /dev/null +++ b/utmstack-collector/updates/dependencies.go @@ -0,0 +1,17 @@ +package updates + +import ( + "fmt" + + "github.com/utmstack/UTMStack/utmstack-collector/config" + "github.com/utmstack/UTMStack/utmstack-collector/utils" +) + +func DownloadVersion(address string, insecure bool) error { + if err := utils.DownloadFile(fmt.Sprintf(config.DependUrl, address, config.DependenciesPort, "version.json"), map[string]string{}, "version.json", utils.GetMyPath(), insecure); err != nil { + return fmt.Errorf("error downloading version.json : %v", err) + } + + return nil + +} diff --git a/utmstack-collector/utils/address.go b/utmstack-collector/utils/address.go new file mode 100644 index 000000000..8a6a4945d --- /dev/null +++ b/utmstack-collector/utils/address.go @@ -0,0 +1,24 @@ +package utils + +import ( + "errors" + "net" +) + +func GetIPAddress() (string, error) { + ipAddress, err := net.InterfaceAddrs() + if err != nil { + return "", err + } + + for _, addr := range ipAddress { + ipNet, ok := addr.(*net.IPNet) + if ok && !ipNet.IP.IsLoopback() { + if ipNet.IP.To4() != nil { + return ipNet.IP.String(), nil + } + } + } + + return "", errors.New("failed to get IP address") +} diff --git a/utmstack-collector/utils/banner.go b/utmstack-collector/utils/banner.go new file mode 100644 index 000000000..cb7c45f59 --- /dev/null +++ b/utmstack-collector/utils/banner.go @@ -0,0 +1,17 @@ +package utils + +import "fmt" + +func PrintBanner() { + banner := "\n" + + "..........................................................................\n" + + " _ _ _ _____ _ _ \n" + + " | | | | | | / ____| | | | | \n" + + " | | | | | |_ _ __ ___ | (___ | |_ __ _ ___ | | __ \n" + + " | | | | | __| | '_ ` _ \\ \\___ \\ | __| / _` | / __| | |/ / \n" + + " | |__| | | |_ | | | | | | ____) | | |_ | (_| | | (__ | < \n" + + " \\____/ \\__| |_| |_| |_| |_____/ \\__| \\__,_| \\___| |_|\\_\\ \n" + + ".........................................................................." + + fmt.Println(banner) +} diff --git a/utmstack-collector/utils/cmd.go b/utmstack-collector/utils/cmd.go new file mode 100644 index 000000000..334f5335f --- /dev/null +++ b/utmstack-collector/utils/cmd.go @@ -0,0 +1,12 @@ +package utils + +import ( + "os/exec" +) + +func Execute(c string, dir string, arg ...string) error { + cmd := exec.Command(c, arg...) + cmd.Dir = dir + + return cmd.Run() +} diff --git a/utmstack-collector/utils/crypt.go b/utmstack-collector/utils/crypt.go new file mode 100644 index 000000000..48fa35deb --- /dev/null +++ b/utmstack-collector/utils/crypt.go @@ -0,0 +1,22 @@ +package utils + +import ( + "encoding/base64" +) + +func GenerateKey(baseKey string) ([]byte, error) { + info, err := GetOsInfo() + if err != nil { + return nil, Logger.ErrorF("error getting os info: %v", err) + } + + data := []byte(info.Hostname + info.Mac + info.OsType) + base64Key := base64.StdEncoding.EncodeToString(data) + return []byte(baseKey + base64Key), nil +} + +func GenerateKeyByUUID(baseKey string, uuid string) ([]byte, error) { + data := []byte(baseKey + uuid) + base64Key := base64.StdEncoding.EncodeToString(data) + return []byte(base64Key), nil +} diff --git a/utmstack-collector/utils/delay.go b/utmstack-collector/utils/delay.go new file mode 100644 index 000000000..e3000ea71 --- /dev/null +++ b/utmstack-collector/utils/delay.go @@ -0,0 +1,11 @@ +package utils + +import "time" + +func IncrementReconnectDelay(delay time.Duration, maxReconnectDelay time.Duration) time.Duration { + delay *= 2 + if delay > maxReconnectDelay { + delay = maxReconnectDelay + } + return delay +} diff --git a/utmstack-collector/utils/download.go b/utmstack-collector/utils/download.go new file mode 100644 index 000000000..055c44881 --- /dev/null +++ b/utmstack-collector/utils/download.go @@ -0,0 +1,48 @@ +package utils + +import ( + "crypto/tls" + "fmt" + "io" + "net/http" + "os" + "path/filepath" +) + +func DownloadFile(url string, headers map[string]string, fileName string, path string, skipTlsVerification bool) error { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("error creating new request: %v", err) + } + for key, value := range headers { + req.Header.Add(key, value) + } + + client := &http.Client{} + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTlsVerification}, + } + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("error sending request: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected status %d; got %d", http.StatusOK, resp.StatusCode) + } + + out, err := os.Create(filepath.Join(path, fileName)) + if err != nil { + return fmt.Errorf("error creating file: %v", err) + } + defer func() { _ = out.Close() }() + + _, err = io.Copy(out, resp.Body) + if err != nil { + return fmt.Errorf("error copying file: %v", err) + } + + return nil +} diff --git a/utmstack-collector/utils/files.go b/utmstack-collector/utils/files.go new file mode 100644 index 000000000..5d7c7355f --- /dev/null +++ b/utmstack-collector/utils/files.go @@ -0,0 +1,104 @@ +package utils + +import ( + "encoding/json" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +func GetMyPath() string { + ex, err := os.Executable() + if err != nil { + return "" + } + exPath := filepath.Dir(ex) + return exPath +} + +func ReadYAML(path string, result interface{}) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer func() { _ = file.Close() }() + + d := yaml.NewDecoder(file) + if err := d.Decode(result); err != nil { + return err + } + + return nil +} + +func WriteStringToFile(fileName string, body string) error { + file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.ModePerm) + if err != nil { + return err + } + defer func() { _ = file.Close() }() + + _, err = file.WriteString(body) + return err +} + +func WriteYAML(url string, data interface{}) error { + config, err := yaml.Marshal(data) + if err != nil { + return err + } + + err = WriteStringToFile(url, string(config)) + if err != nil { + return err + } + + return nil +} + +func WriteJSON(path string, data interface{}) error { + jsonData, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err + } + + err = WriteStringToFile(path, string(jsonData)) + if err != nil { + return err + } + + return nil +} + +func ReadJson(fileName string, data interface{}) error { + content, err := os.ReadFile(fileName) + if err != nil { + return err + } + + err = json.Unmarshal(content, data) + if err != nil { + return err + } + + return nil +} + +func CreatePathIfNotExist(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + if err := os.MkdirAll(path, 0755); err != nil { + return Logger.ErrorF("error creating path: %v", err) + } + } else if err != nil { + return Logger.ErrorF("error checking path: %v", err) + } + return nil +} + +func CheckIfPathExist(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + return true +} diff --git a/utmstack-collector/utils/host.go b/utmstack-collector/utils/host.go new file mode 100644 index 000000000..e79c972c3 --- /dev/null +++ b/utmstack-collector/utils/host.go @@ -0,0 +1,21 @@ +package utils + +import "net" + +func GetHostAliases(hostname string) ([]string, error) { + var aliases []string + addresses, err := net.LookupHost(hostname) + if err != nil { + return nil, err + } + + for _, address := range addresses { + newAliases, err := net.LookupAddr(address) + if err != nil { + return nil, err + } + aliases = append(aliases, newAliases...) + } + + return aliases, nil +} diff --git a/utmstack-collector/utils/logger.go b/utmstack-collector/utils/logger.go new file mode 100644 index 000000000..a93ac7455 --- /dev/null +++ b/utmstack-collector/utils/logger.go @@ -0,0 +1,44 @@ +package utils + +import ( + "path/filepath" + "sync" + + "github.com/threatwinds/logger" +) + +var ( + Logger *logger.Logger + loggerOnceInstance sync.Once + logLevelConfigFile = filepath.Join(GetMyPath(), "log_level.yml") + LogLevelMap = map[string]int{ + "debug": 100, + "info": 200, + "notice": 300, + "warning": 400, + "error": 500, + "critical": 502, + "alert": 509, + } +) + +type LogLevels struct { + Level string `yaml:"level"` +} + +func InitLogger(filename string) { + logLevel := LogLevels{} + err := ReadYAML(logLevelConfigFile, &logLevel) + if err != nil { + logLevel.Level = "info" + } + logLevelInt := 200 + if val, ok := LogLevelMap[logLevel.Level]; ok { + logLevelInt = val + } + loggerOnceInstance.Do(func() { + Logger = logger.NewLogger( + &logger.Config{Format: "text", Level: logLevelInt, Output: filename, Retries: 3, Wait: 5}, + ) + }) +} diff --git a/utmstack-collector/utils/os.go b/utmstack-collector/utils/os.go new file mode 100644 index 000000000..1b095729f --- /dev/null +++ b/utmstack-collector/utils/os.go @@ -0,0 +1,62 @@ +package utils + +import ( + "os" + "os/user" + "strconv" + "strings" + + "github.com/elastic/go-sysinfo" +) + +type OSInfo struct { + Hostname string + OsType string + Platform string + CurrentUser string + Mac string + OsMajorVersion string + OsMinorVersion string + Aliases string + Addresses string +} + +func GetOsInfo() (OSInfo, error) { + var info OSInfo + + hostInfo, err := sysinfo.Host() + if err != nil { + return info, Logger.ErrorF("error getting host info: %v", err) + } + info.OsType = hostInfo.Info().OS.Type + info.Platform = hostInfo.Info().OS.Platform + info.Mac = strings.Join(hostInfo.Info().MACs, ",") + info.OsMajorVersion = strconv.Itoa(hostInfo.Info().OS.Major) + info.OsMinorVersion = strconv.Itoa(hostInfo.Info().OS.Minor) + info.Addresses = strings.Join(hostInfo.Info().IPs, ",") + + hostName, err := os.Hostname() + if err != nil { + return info, Logger.ErrorF("error getting hostname: %v", err) + } + info.Hostname = hostName + + currentUser, err := user.Current() + if err != nil { + return info, Logger.ErrorF("error getting user: %v", err) + } + info.CurrentUser = currentUser.Username + + aliases, err := GetHostAliases(hostInfo.Info().Hostname) + if err != nil { + aliases = aliases[:0] + aliases = append(aliases, "") + } + if len(aliases) == 1 && strings.Contains(aliases[0], "any") { + aliases = aliases[:0] + aliases = append(aliases, "") + } + info.Aliases = strings.Join(aliases, ",") + + return info, nil +} diff --git a/utmstack-collector/utils/port.go b/utmstack-collector/utils/port.go new file mode 100644 index 000000000..af1e06596 --- /dev/null +++ b/utmstack-collector/utils/port.go @@ -0,0 +1,29 @@ +package utils + +import ( + "fmt" + "net" + "time" +) + +func ArePortsReachable(ip string, ports ...string) error { + var conn net.Conn + var err error + +external: + for _, port := range ports { + for i := 0; i < 3; i++ { + conn, err = net.DialTimeout("tcp", fmt.Sprintf("%s:%s", ip, port), 5*time.Second) + if err == nil { + conn.Close() + continue external + } + time.Sleep(5 * time.Second) + } + if err != nil { + return Logger.ErrorF("cannot connect to %s on port %s: %v", ip, port, err) + } + } + + return nil +} diff --git a/utmstack-collector/utils/services.go b/utmstack-collector/utils/services.go new file mode 100644 index 000000000..5d4625d52 --- /dev/null +++ b/utmstack-collector/utils/services.go @@ -0,0 +1,109 @@ +package utils + +import ( + "fmt" + "os" + "runtime" +) + +func StopService(name string) error { + path := GetMyPath() + switch runtime.GOOS { + case "windows": + err := Execute("sc", path, "stop", name) + if err != nil { + return Logger.ErrorF("error stoping service: %v", err) + } + case "linux": + err := Execute("systemctl", path, "stop", name) + if err != nil { + return Logger.ErrorF("error stoping service: %v", err) + } + case "darwin": + err := Execute("launchctl", path, "remove", name) + if err != nil { + return Logger.ErrorF("error stopping macOS service: %v", err) + } + } + return nil +} + +func UninstallService(name string) error { + path := GetMyPath() + switch runtime.GOOS { + case "windows": + err := Execute("sc", path, "delete", name) + if err != nil { + return Logger.ErrorF("error uninstalling service: %v", err) + } + case "linux": + err := Execute("systemctl", path, "disable", name) + if err != nil { + return Logger.ErrorF("error uninstalling service: %v", err) + } + err = Execute("rm", "/etc/systemd/system/", "/etc/systemd/system/"+name+".service") + if err != nil { + return Logger.ErrorF("error uninstalling service: %v", err) + } + case "darwin": + Execute("launchctl", path, "remove", name) + Execute("rm", "/Library/LaunchDaemons/"+name+".plist") + Execute("rm", "/Users/"+os.Getenv("USER")+"/Library/LaunchAgents/"+name+".plist") + + } + return nil +} + +func CheckIfServiceIsInstalled(serv string) (bool, error) { + path := GetMyPath() + var err error + switch runtime.GOOS { + case "windows": + err = Execute("sc", path, "query", serv) + case "linux": + err = Execute("systemctl", path, "status", serv) + case "darwin": + err = Execute("launchctl", path, "list", serv) + default: + return false, Logger.ErrorF("operative system unknown") + } + + return err == nil, nil +} + +func CreateLinuxService(serviceName string, execStart string) error { + servicePath := "/etc/systemd/system/" + serviceName + ".service" + if !CheckIfPathExist(servicePath) { + file, err := os.Create(servicePath) + if err != nil { + return Logger.ErrorF("error creating %s file: %v", servicePath, err) + } + defer func() { _ = file.Close() }() + + serviceContent := fmt.Sprintf(`[Unit] +Description=%s +After=network.target + +[Service] +ExecStart=%s +Restart=always + +[Install] +WantedBy=multi-user.target +`, serviceName, execStart) + + _, err = file.WriteString(serviceContent) + if err != nil { + return err + } + + err = file.Sync() + if err != nil { + return err + } + } else { + return Logger.ErrorF("service %s already exists", serviceName) + } + + return nil +} diff --git a/utmstack-collector/version.json b/utmstack-collector/version.json new file mode 100644 index 000000000..b8bfb0ca9 --- /dev/null +++ b/utmstack-collector/version.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +}