├── roles └── agent │ ├── molecule │ ├── default │ │ ├── Dockerfile.amazonlinux2023 │ │ ├── Dockerfile.amazonlinux2 │ │ ├── Dockerfile.ubuntu2004 │ │ ├── Dockerfile.ubuntu2204 │ │ ├── Dockerfile.ubuntu2404 │ │ ├── molecule.yml │ │ ├── converge.yml │ │ └── verify.yml │ ├── apple-silicon │ │ ├── Dockerfile.rocky9-arm64 │ │ ├── Dockerfile.ubuntu2204-arm64 │ │ ├── Dockerfile.ubuntu2404-arm64 │ │ ├── converge.yml │ │ ├── verify.yml │ │ └── molecule.yml │ └── vagrant │ │ ├── converge.yml │ │ ├── molecule.yml │ │ ├── prepare.yml │ │ ├── side_effect.yml │ │ └── verify.yml │ ├── handlers │ └── main.yml │ ├── meta │ └── main.yml │ ├── tasks │ ├── main.yml │ ├── configure.yml │ ├── install.yml │ └── activate.yml │ ├── templates │ ├── contrast.conf.j2 │ └── contrast_security.yaml.j2 │ ├── defaults │ └── main.yml │ └── README.md ├── examples ├── requirements.yml ├── group_vars │ └── production.yml ├── inventory.ini ├── deactivate-contrast.yml └── deploy-contrast.yml ├── .gitignore ├── tests ├── vagrant │ ├── inventory │ └── setup.yml └── vagrant.conf ├── container_test.yml ├── galaxy.yml ├── LICENSE ├── CHANGELOG.md ├── TESTING_STRATEGY.md ├── QUICKSTART.md ├── test-docker.sh ├── CONTRIBUTING.md ├── README.md └── Vagrantfile /roles/agent/molecule/default/Dockerfile.amazonlinux2023: -------------------------------------------------------------------------------- 1 | FROM amazonlinux:2023 2 | RUN yum update -y && yum install -y \ 3 | python3 \ 4 | python3-pip \ 5 | sudo \ 6 | systemd \ 7 | procps \ 8 | ca-certificates \ 9 | && yum clean all \ 10 | && python3 --version 11 | CMD ["/bin/bash", "-c", "while true; do sleep 30; done"] 12 | -------------------------------------------------------------------------------- /roles/agent/molecule/default/Dockerfile.amazonlinux2: -------------------------------------------------------------------------------- 1 | FROM amazonlinux:2 2 | RUN yum update -y && yum install -y \ 3 | python3 \ 4 | python3-pip \ 5 | sudo \ 6 | systemd \ 7 | procps \ 8 | curl \ 9 | ca-certificates \ 10 | && yum clean all \ 11 | && python3 --version 12 | CMD ["/bin/bash", "-c", "while true; do sleep 30; done"] 13 | -------------------------------------------------------------------------------- /roles/agent/molecule/default/Dockerfile.ubuntu2004: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | RUN apt-get update && apt-get install -y \ 4 | python3 \ 5 | python3-apt \ 6 | sudo \ 7 | systemd \ 8 | procps \ 9 | curl \ 10 | ca-certificates \ 11 | && rm -rf /var/lib/apt/lists/* \ 12 | && ln -sf /usr/bin/python3 /usr/bin/python 13 | CMD ["/bin/bash", "-c", "while true; do sleep 30; done"] 14 | -------------------------------------------------------------------------------- /roles/agent/molecule/default/Dockerfile.ubuntu2204: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | RUN apt-get update && apt-get install -y \ 4 | python3 \ 5 | python3-apt \ 6 | sudo \ 7 | systemd \ 8 | procps \ 9 | curl \ 10 | ca-certificates \ 11 | && rm -rf /var/lib/apt/lists/* \ 12 | && ln -sf /usr/bin/python3 /usr/bin/python 13 | CMD ["/bin/bash", "-c", "while true; do sleep 30; done"] 14 | -------------------------------------------------------------------------------- /roles/agent/molecule/default/Dockerfile.ubuntu2404: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | RUN apt-get update && apt-get install -y \ 4 | python3 \ 5 | python3-apt \ 6 | sudo \ 7 | systemd \ 8 | procps \ 9 | curl \ 10 | ca-certificates \ 11 | && rm -rf /var/lib/apt/lists/* \ 12 | && ln -sf /usr/bin/python3 /usr/bin/python 13 | CMD ["/bin/bash", "-c", "while true; do sleep 30; done"] 14 | -------------------------------------------------------------------------------- /examples/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Ansible requirements file 3 | # Install with: ansible-galaxy install -r requirements.yml 4 | 5 | collections: 6 | # Contrast Security Collection 7 | - name: contrast.security 8 | version: ">=1.0.0" 9 | 10 | # Optional: Community collections that might be useful 11 | - name: community.general 12 | version: ">=3.0.0" 13 | 14 | - name: ansible.posix 15 | version: ">=1.0.0" 16 | 17 | # If you need specific roles (example) 18 | roles: [] 19 | # Example of how to include roles if needed 20 | # - name: geerlingguy.java 21 | # version: "2.1.0" 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | *.retry 3 | *.log 4 | *.pyc 5 | __pycache__/ 6 | .pytest_cache/ 7 | 8 | # Ansible 9 | *.tar.gz 10 | .galaxy/ 11 | .ansible 12 | 13 | # IDE 14 | .vscode/ 15 | .idea/ 16 | *.swp 17 | *.swo 18 | *~ 19 | 20 | # OS 21 | .DS_Store 22 | Thumbs.db 23 | 24 | # Testing 25 | .molecule/ 26 | .cache/ 27 | .tox/ 28 | tests/output/ 29 | 30 | # Build 31 | build/ 32 | dist/ 33 | *.egg-info/ 34 | 35 | # Credentials (never commit these!) 36 | **/vault.yml 37 | **/credentials.yml 38 | **/secrets.yml 39 | **/*_credentials.yml 40 | **/*_secrets.yml 41 | 42 | # Local overrides 43 | local.yml 44 | 45 | # Vagrant 46 | .vagrant/ -------------------------------------------------------------------------------- /tests/vagrant/inventory: -------------------------------------------------------------------------------- 1 | [all:vars] 2 | ansible_python_interpreter=/usr/bin/python3 3 | ansible_ssh_private_key_file=~/.vagrant.d/insecure_private_key 4 | 5 | [test_vms] 6 | ubuntu2004-contrast-test ansible_host=192.168.56.10 7 | ubuntu2204-contrast-test ansible_host=192.168.56.11 8 | centos8-contrast-test ansible_host=192.168.56.12 9 | rocky9-contrast-test ansible_host=192.168.56.13 10 | amazonlinux2-contrast-test ansible_host=192.168.56.14 11 | 12 | [ubuntu] 13 | ubuntu2004-contrast-test 14 | ubuntu2204-contrast-test 15 | 16 | [centos] 17 | centos8-contrast-test 18 | 19 | [rocky] 20 | rocky9-contrast-test 21 | 22 | [amazonlinux] 23 | amazonlinux2-contrast-test 24 | 25 | [systemd_hosts:children] 26 | ubuntu 27 | centos 28 | rocky 29 | amazonlinux 30 | 31 | [test_vms:vars] 32 | ansible_user=vagrant 33 | -------------------------------------------------------------------------------- /roles/agent/molecule/apple-silicon/Dockerfile.rocky9-arm64: -------------------------------------------------------------------------------- 1 | FROM rockylinux:9 2 | 3 | # Install base packages (handle curl conflicts) 4 | RUN dnf update -y && dnf install -y --allowerasing \ 5 | python3 \ 6 | python3-pip \ 7 | sudo \ 8 | systemd \ 9 | procps-ng \ 10 | curl \ 11 | wget \ 12 | ca-certificates \ 13 | java-11-openjdk-devel \ 14 | && dnf clean all 15 | 16 | # Configure systemd 17 | RUN systemctl set-default multi-user.target 18 | 19 | # Remove systemd 'wants' to avoid unnecessary services 20 | RUN find /etc/systemd/system \ 21 | /lib/systemd/system \ 22 | -path '*.wants/*' \ 23 | -not -name '*journal*' \ 24 | -not -name '*systemd-tmpfiles*' \ 25 | -not -name '*systemd-user-sessions*' \ 26 | -delete 27 | 28 | # Start systemd 29 | ENTRYPOINT ["/sbin/init"] 30 | -------------------------------------------------------------------------------- /roles/agent/molecule/apple-silicon/Dockerfile.ubuntu2204-arm64: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | # Install base packages 6 | RUN apt-get update && apt-get install -y \ 7 | python3 \ 8 | python3-pip \ 9 | python3-apt \ 10 | sudo \ 11 | systemd \ 12 | systemd-sysv \ 13 | procps \ 14 | curl \ 15 | wget \ 16 | ca-certificates \ 17 | openjdk-11-jdk \ 18 | && rm -rf /var/lib/apt/lists/* \ 19 | && ln -sf /usr/bin/python3 /usr/bin/python 20 | 21 | # Configure systemd 22 | RUN systemctl set-default multi-user.target 23 | 24 | # Remove systemd 'wants' to avoid unnecessary services 25 | RUN find /etc/systemd/system \ 26 | /lib/systemd/system \ 27 | -path '*.wants/*' \ 28 | -not -name '*journal*' \ 29 | -not -name '*systemd-tmpfiles*' \ 30 | -not -name '*systemd-user-sessions*' \ 31 | -delete 32 | 33 | # Start systemd 34 | ENTRYPOINT ["/sbin/init"] 35 | -------------------------------------------------------------------------------- /roles/agent/molecule/apple-silicon/Dockerfile.ubuntu2404-arm64: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | # Install base packages (handle PEP 668 for Ubuntu 24.04+) 6 | RUN apt-get update && apt-get install -y \ 7 | python3 \ 8 | python3-pip \ 9 | python3-venv \ 10 | python3-full \ 11 | python3-apt \ 12 | sudo \ 13 | systemd \ 14 | systemd-sysv \ 15 | procps \ 16 | curl \ 17 | wget \ 18 | ca-certificates \ 19 | openjdk-11-jdk \ 20 | && rm -rf /var/lib/apt/lists/* \ 21 | && ln -sf /usr/bin/python3 /usr/bin/python 22 | 23 | # Configure systemd 24 | RUN systemctl set-default multi-user.target 25 | 26 | # Remove systemd 'wants' to avoid unnecessary services 27 | RUN find /etc/systemd/system \ 28 | /lib/systemd/system \ 29 | -path '*.wants/*' \ 30 | -not -name '*journal*' \ 31 | -not -name '*systemd-tmpfiles*' \ 32 | -not -name '*systemd-user-sessions*' \ 33 | -delete 34 | 35 | # Start systemd 36 | ENTRYPOINT ["/sbin/init"] 37 | -------------------------------------------------------------------------------- /container_test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test Contrast Agent in Container 3 | hosts: localhost 4 | connection: local 5 | become: true 6 | vars: 7 | contrast_agent_enabled: true 8 | contrast_agent_version: "6.19.0" 9 | contrast_config_method: "environment" # Use environment instead of systemd 10 | contrast_test_mode: true 11 | contrast_security_config: 12 | api: 13 | url: "https://app.contrastsecurity.com/Contrast" 14 | api_key: "container-test-key" 15 | service_key: "container-test-service" 16 | user_name: "container-test-user" 17 | application: 18 | name: "ContainerTest" 19 | 20 | tasks: 21 | - name: Include contrast agent role 22 | include_role: 23 | name: agent 24 | 25 | - name: Verify agent installation 26 | stat: 27 | path: /opt/contrast/contrast.jar 28 | register: agent_check 29 | 30 | - name: Show test result 31 | debug: 32 | msg: "✅ Container test {{ 'PASSED' if agent_check.stat.exists else 'FAILED' }}" 33 | -------------------------------------------------------------------------------- /roles/agent/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # handlers file for contrast.security.agent role 3 | 4 | - name: Reload systemd 5 | ansible.builtin.systemd: 6 | daemon_reload: true 7 | become: true 8 | when: contrast_config_method == 'systemd' 9 | 10 | - name: Restart {{ contrast_service_name }} 11 | ansible.builtin.systemd: 12 | name: "{{ contrast_service_name }}" 13 | state: restarted 14 | become: true 15 | when: 16 | - contrast_config_method == 'systemd' 17 | - not (contrast_test_mode | default(false)) 18 | 19 | - name: Restart {{ contrast_service_name }} for setenv 20 | ansible.builtin.service: 21 | name: "{{ contrast_service_name }}" 22 | state: restarted 23 | become: true 24 | when: 25 | - contrast_config_method == 'setenv' 26 | - not (contrast_test_mode | default(false)) 27 | 28 | - name: Ensure service is running 29 | ansible.builtin.systemd: 30 | name: "{{ contrast_service_name }}" 31 | state: started 32 | enabled: true 33 | become: true 34 | when: 35 | - contrast_config_method == 'systemd' 36 | - not (contrast_test_mode | default(false)) 37 | -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Ansible Collection metadata file 3 | namespace: contrast 4 | name: security 5 | version: 1.0.0 6 | readme: README.md 7 | authors: 8 | - Contrast Security 9 | 10 | description: > 11 | Official Ansible Collection for deploying and managing Contrast Security agents. 12 | This collection provides roles for automated deployment of the Contrast Java agent 13 | on EC2 instances and other platforms. 14 | 15 | license: 16 | - MIT 17 | 18 | tags: 19 | - security 20 | - appsec 21 | - contrast 22 | - java 23 | - agent 24 | - apm 25 | - monitoring 26 | - vulnerability 27 | 28 | repository: https://github.com/Contrast-Security-OSS/ansible-collection-contrast 29 | documentation: https://github.com/Contrast-Security-OSS/ansible-collection-contrast/blob/main/README.md 30 | homepage: https://www.contrastsecurity.com 31 | issues: https://github.com/Contrast-Security-OSS/ansible-collection-contrast/issues 32 | 33 | dependencies: {} 34 | 35 | build_ignore: 36 | - .git 37 | - .github 38 | - tests/output 39 | - '*.tar.gz' 40 | - .gitignore 41 | - .pytest_cache 42 | -------------------------------------------------------------------------------- /roles/agent/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # meta file for contrast.security.agent role 3 | 4 | galaxy_info: 5 | role_name: agent 6 | namespace: contrast.security 7 | author: Contrast Security 8 | description: > 9 | Official Ansible role for deploying and managing the Contrast Security Java agent. 10 | Supports automated deployment on EC2 instances with Tomcat and other Java application servers. 11 | company: Contrast Security 12 | license: MIT 13 | min_ansible_version: "2.9" 14 | 15 | platforms: 16 | - name: EL 17 | versions: 18 | - "7" 19 | - "8" 20 | - "9" 21 | - name: Amazon 22 | versions: 23 | - "2" 24 | - "2023" 25 | - name: Ubuntu 26 | versions: 27 | - bionic 28 | - focal 29 | - jammy 30 | - name: Debian 31 | versions: 32 | - buster 33 | - bullseye 34 | - bookworm 35 | 36 | galaxy_tags: 37 | - security 38 | - contrast 39 | - java 40 | - agent 41 | - appsec 42 | - vulnerability 43 | - monitoring 44 | - tomcat 45 | - ec2 46 | - aws 47 | 48 | dependencies: [] 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Contrast Security, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /roles/agent/molecule/apple-silicon/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | become: true 5 | vars: 6 | contrast_agent_enabled: true 7 | contrast_agent_version: "6.19.0" 8 | contrast_config_method: "environment" # Use environment for container testing 9 | contrast_test_mode: true 10 | contrast_security_config: 11 | api: 12 | url: "https://app.contrastsecurity.com/Contrast" 13 | api_key: "molecule-test-key" 14 | service_key: "molecule-test-service" 15 | user_name: "molecule-test-user" 16 | application: 17 | name: "MoleculeTest" 18 | 19 | tasks: 20 | - name: Include contrast agent role 21 | include_role: 22 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 23 | vars: 24 | contrast_agent_enabled: true 25 | contrast_service_name: "tomcat" 26 | contrast_config_method: "setenv" 27 | contrast_agent_version: "6.19.0" 28 | contrast_test_mode: true 29 | contrast_security_config: 30 | api: 31 | url: "https://app.contrastsecurity.com/Contrast" 32 | api_key: "test-api-key" 33 | service_key: "test-service-key" 34 | user_name: "test-user" 35 | application: 36 | name: "molecule-test-app" 37 | tags: "env:test,molecule:true" 38 | server: 39 | name: "molecule-test-server" 40 | environment: "test" 41 | -------------------------------------------------------------------------------- /roles/agent/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # main tasks file for contrast.security.agent role 3 | 4 | - name: Validate required variables 5 | ansible.builtin.assert: 6 | that: 7 | - contrast_service_name is defined 8 | - contrast_service_name | length > 0 9 | fail_msg: "contrast_service_name must be defined and not empty" 10 | success_msg: "All required variables are properly defined" 11 | 12 | - name: Display agent configuration 13 | ansible.builtin.debug: 14 | msg: 15 | - "Contrast agent enabled: {{ contrast_agent_enabled }}" 16 | - "Target service: {{ contrast_service_name }}" 17 | - "Configuration method: {{ contrast_config_method }}" 18 | - "Configuration source: {{ contrast_config_source }}" 19 | - "Agent version: {{ contrast_agent_version }}" 20 | 21 | - name: Include agent installation tasks 22 | ansible.builtin.include_tasks: install.yml 23 | when: contrast_agent_enabled | bool 24 | 25 | - name: Include agent configuration tasks 26 | ansible.builtin.include_tasks: configure.yml 27 | when: contrast_agent_enabled | bool 28 | 29 | - name: Include agent activation tasks 30 | ansible.builtin.include_tasks: activate.yml 31 | 32 | - name: Verify agent status 33 | ansible.builtin.debug: 34 | msg: > 35 | {% if contrast_agent_enabled %} 36 | Contrast agent has been activated for {{ contrast_service_name }}. 37 | {% else %} 38 | Contrast agent has been deactivated for {{ contrast_service_name }}. 39 | {% endif %} 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.0.0] - 2025-07-04 11 | 12 | ### Added 13 | - Initial release of the Contrast Security Ansible Collection 14 | - `contrast.security.agent` role for Java agent deployment 15 | - Support for systemd and setenv.sh activation methods 16 | - Support for environment variable and YAML configuration 17 | - Automatic agent version detection from Maven Central 18 | - Idempotent installation and configuration 19 | - Clean deactivation mechanism 20 | - DataDog compatibility configuration 21 | - Comprehensive documentation and examples 22 | 23 | ### Features 24 | - Master toggle for agent enablement (`contrast_agent_enabled`) 25 | - Flexible configuration through `contrast_security_config` dictionary 26 | - Support for custom JVM arguments 27 | - Automatic directory creation 28 | - Download verification and timeout handling 29 | - Support for air-gapped environments 30 | 31 | ### Supported Platforms 32 | - Red Hat Enterprise Linux 7, 8, 9 33 | - CentOS 7, 8 34 | - Amazon Linux 2, 2023 35 | - Ubuntu 20.04, 22.04, 24.04 36 | - Debian 11, 12, 13 37 | 38 | [Unreleased]: https://github.com/Contrast-Security-OSS/ansible-collection-contrast/compare/v1.0.0...HEAD 39 | [1.0.0]: https://github.com/Contrast-Security-OSS/ansible-collection-contrast/releases/tag/v1.0.0 40 | -------------------------------------------------------------------------------- /roles/agent/molecule/apple-silicon/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | gather_facts: false 5 | tasks: 6 | - name: Verify Contrast agent installation 7 | stat: 8 | path: /opt/contrast/contrast.jar 9 | register: agent_jar 10 | 11 | - name: Assert agent JAR exists 12 | assert: 13 | that: 14 | - agent_jar.stat.exists 15 | - agent_jar.stat.isreg 16 | fail_msg: "Contrast agent JAR not found at /opt/contrast/contrast.jar" 17 | success_msg: "✅ Contrast agent JAR found and is a regular file" 18 | 19 | - name: Verify agent directory permissions 20 | stat: 21 | path: /opt/contrast 22 | register: agent_dir 23 | 24 | - name: Assert agent directory is properly configured 25 | assert: 26 | that: 27 | - agent_dir.stat.exists 28 | - agent_dir.stat.isdir 29 | fail_msg: "Contrast agent directory not properly configured" 30 | success_msg: "✅ Contrast agent directory properly configured" 31 | 32 | - name: Check if systemd override exists (if using systemd) 33 | stat: 34 | path: "/etc/systemd/system/{{ service_name | default('tomcat') }}.service.d/contrast.conf" 35 | register: systemd_override 36 | when: contrast_config_method == "systemd" 37 | 38 | - name: Verify systemd configuration (if using systemd) 39 | assert: 40 | that: 41 | - systemd_override.stat.exists 42 | fail_msg: "Systemd override configuration not found" 43 | success_msg: "✅ Systemd override configuration found" 44 | when: contrast_config_method == "systemd" 45 | 46 | - name: Display test completion 47 | debug: 48 | msg: "🎉 All Apple Silicon ARM64 container tests passed successfully!" 49 | -------------------------------------------------------------------------------- /roles/agent/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: docker 6 | platforms: 7 | - name: ubuntu2004 8 | image: ubuntu:20.04 9 | pre_build_image: false 10 | dockerfile: Dockerfile.ubuntu2004 11 | privileged: true 12 | - name: ubuntu2204 13 | image: ubuntu:22.04 14 | pre_build_image: false 15 | dockerfile: Dockerfile.ubuntu2204 16 | privileged: true 17 | - name: ubuntu2404 18 | image: ubuntu:24.04 19 | pre_build_image: false 20 | dockerfile: Dockerfile.ubuntu2404 21 | privileged: true 22 | - name: amazonlinux2 23 | image: amazonlinux:2 24 | pre_build_image: false 25 | dockerfile: Dockerfile.amazonlinux2 26 | privileged: true 27 | - name: amazonlinux2023 28 | image: amazonlinux:2023 29 | pre_build_image: false 30 | dockerfile: Dockerfile.amazonlinux2023 31 | privileged: true 32 | 33 | provisioner: 34 | name: ansible 35 | inventory: 36 | host_vars: 37 | ubuntu2004: 38 | ansible_python_interpreter: /usr/bin/python3 39 | ubuntu2204: 40 | ansible_python_interpreter: /usr/bin/python3 41 | ubuntu2404: 42 | ansible_python_interpreter: /usr/bin/python3 43 | amazonlinux2: 44 | ansible_python_interpreter: /usr/bin/python3 45 | amazonlinux2023: 46 | ansible_python_interpreter: /usr/bin/python3 47 | 48 | config_options: 49 | defaults: 50 | callbacks_enabled: profile_tasks 51 | stdout_callback: yaml 52 | roles_path: /Users/jonathanharper/MCP/contrast.security/roles 53 | 54 | verifier: 55 | name: ansible 56 | 57 | scenario: 58 | name: default 59 | test_sequence: 60 | - dependency 61 | - destroy 62 | - syntax 63 | - create 64 | - prepare 65 | - converge 66 | - idempotence 67 | - verify 68 | - destroy 69 | -------------------------------------------------------------------------------- /roles/agent/molecule/apple-silicon/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | 5 | driver: 6 | name: docker 7 | 8 | platforms: 9 | - name: ubuntu2204-arm64 10 | image: ubuntu:22.04 11 | platform: linux/arm64 12 | pre_build_image: false 13 | dockerfile: Dockerfile.ubuntu2204-arm64 14 | privileged: true 15 | volumes: 16 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 17 | cgroupns_mode: host 18 | 19 | - name: ubuntu2404-arm64 20 | image: ubuntu:24.04 21 | platform: linux/arm64 22 | pre_build_image: false 23 | dockerfile: Dockerfile.ubuntu2404-arm64 24 | privileged: true 25 | volumes: 26 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 27 | cgroupns_mode: host 28 | 29 | - name: rocky9-arm64 30 | image: rockylinux:9 31 | platform: linux/arm64 32 | pre_build_image: false 33 | dockerfile: Dockerfile.rocky9-arm64 34 | privileged: true 35 | volumes: 36 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 37 | cgroupns_mode: host 38 | 39 | provisioner: 40 | name: ansible 41 | inventory: 42 | host_vars: 43 | ubuntu2204-arm64: 44 | ansible_python_interpreter: /usr/bin/python3 45 | contrast_config_method: environment # Use environment for container testing 46 | ubuntu2404-arm64: 47 | ansible_python_interpreter: /usr/bin/python3 48 | contrast_config_method: environment 49 | rocky9-arm64: 50 | ansible_python_interpreter: /usr/bin/python3 51 | contrast_config_method: environment 52 | config_options: 53 | defaults: 54 | callbacks_enabled: profile_tasks 55 | stdout_callback: yaml 56 | 57 | verifier: 58 | name: ansible 59 | 60 | scenario: 61 | name: apple-silicon 62 | test_sequence: 63 | - dependency 64 | - destroy 65 | - syntax 66 | - create 67 | - prepare 68 | - converge 69 | - idempotence 70 | - verify 71 | - destroy 72 | -------------------------------------------------------------------------------- /examples/group_vars/production.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # group_vars/production.yml 3 | # Production-specific configuration for Contrast Security 4 | 5 | # Enable agent in production 6 | contrast_agent_enabled: true 7 | 8 | # Production configuration 9 | contrast_security_config: 10 | api: 11 | url: "https://app.contrastsecurity.com/Contrast" 12 | # Use vault for sensitive data 13 | api_key: "{{ vault_contrast_api_key }}" 14 | service_key: "{{ vault_contrast_service_key }}" 15 | user_name: "{{ vault_contrast_user_name }}" 16 | 17 | application: 18 | name: "{{ app_name | default(inventory_hostname_short) }}" 19 | tags: "env:production,deployment:{{ deployment_id | default('manual') }}" 20 | version: "{{ app_version | default('unknown') }}" 21 | 22 | server: 23 | name: "{{ inventory_hostname }}" 24 | environment: "production" 25 | tags: "dc:{{ datacenter }},region:{{ aws_region | default('us-east-1') }}" 26 | 27 | agent: 28 | logger: 29 | path: "/var/log/contrast/contrast.log" 30 | level: "WARN" 31 | roll_daily: true 32 | roll_size: "100MB" 33 | backups: 7 34 | 35 | security_logger: 36 | path: "/var/log/contrast/security.log" 37 | level: "INFO" 38 | 39 | # Production: Protect enabled, Assess disabled 40 | assess: 41 | enable: false 42 | 43 | protect: 44 | enable: true 45 | rules: 46 | # Example: disable specific rules if needed 47 | disabled_rules: [] 48 | 49 | # Protection settings 50 | mode: "block" # or "monitor" 51 | 52 | # JVM settings for production 53 | contrast_jvm_args: 54 | - "-Xmx2048m" 55 | - "-Xms2048m" 56 | # If using with DataDog 57 | - "-Ddd.trace.classes.exclude=com.contrast*" 58 | # Additional production JVM flags 59 | - "-XX:+UseG1GC" 60 | - "-XX:MaxGCPauseMillis=200" 61 | 62 | # Production-specific settings 63 | contrast_download_timeout: 120 64 | contrast_download_validate_certs: true 65 | 66 | # Use specific version in production (pin for stability) 67 | contrast_agent_version: "6.19.0" 68 | -------------------------------------------------------------------------------- /tests/vagrant/setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Setup test environment on Vagrant VMs 3 | hosts: all 4 | become: true 5 | gather_facts: true 6 | 7 | vars: 8 | # Service name mapping 9 | service_mapping: 10 | ubuntu2004: tomcat9 11 | ubuntu2204: tomcat9 12 | centos8: tomcat 13 | rocky9: tomcat 14 | amazonlinux2: tomcat 15 | 16 | # Java package mapping 17 | java_mapping: 18 | ubuntu2004: openjdk-11-jdk 19 | ubuntu2204: openjdk-11-jdk 20 | centos8: java-11-openjdk-devel 21 | rocky9: java-11-openjdk-devel 22 | amazonlinux2: java-11-amazon-corretto-devel 23 | 24 | tasks: 25 | - name: Set distribution-specific variables 26 | set_fact: 27 | target_service_name: "{{ service_mapping[test_scenario] | default(service_name) }}" 28 | target_java_package: "{{ java_mapping[test_scenario] | default(java_package) }}" 29 | 30 | - name: Update package cache 31 | package: 32 | update_cache: yes 33 | retries: 3 34 | delay: 10 35 | 36 | - name: Install base packages 37 | package: 38 | name: 39 | - curl 40 | - wget 41 | - unzip 42 | - python3 43 | - "{{ target_java_package }}" 44 | state: present 45 | 46 | - name: Install Tomcat 47 | package: 48 | name: "{{ target_service_name if ansible_os_family == 'Debian' else 'tomcat' }}" 49 | state: present 50 | 51 | - name: Create necessary directories 52 | file: 53 | path: "{{ item }}" 54 | state: directory 55 | owner: tomcat 56 | group: tomcat 57 | mode: '0755' 58 | loop: 59 | - /opt/tomcat 60 | - /opt/tomcat/bin 61 | - /var/log/contrast 62 | ignore_errors: true 63 | 64 | - name: Create setenv.sh 65 | copy: 66 | dest: /opt/tomcat/bin/setenv.sh 67 | content: | 68 | #!/bin/bash 69 | export JAVA_OPTS="" 70 | mode: '0755' 71 | force: no 72 | 73 | - name: Enable and start Tomcat service 74 | systemd: 75 | name: "{{ target_service_name }}" 76 | enabled: yes 77 | state: started 78 | ignore_errors: true # Service might not start in all test environments 79 | -------------------------------------------------------------------------------- /roles/agent/templates/contrast.conf.j2: -------------------------------------------------------------------------------- 1 | # {{ ansible_managed }} 2 | # Contrast Security Java Agent Configuration for {{ contrast_service_name }} 3 | 4 | [Service] 5 | {% set java_tool_options = [] %} 6 | 7 | # Add Contrast agent 8 | {% set _ = java_tool_options.append('-javaagent:' + contrast_agent_path) %} 9 | 10 | # Add configuration path if using YAML 11 | {% if contrast_config_source == 'yaml' %} 12 | {% set _ = java_tool_options.append('-Dcontrast.config.path=' + contrast_config_path) %} 13 | {% endif %} 14 | 15 | # Add configuration via system properties if using environment 16 | {% if contrast_config_source == 'environment' and contrast_security_config %} 17 | {% for key, value in contrast_security_config.items() recursive %} 18 | {% if value is mapping %} 19 | {% for subkey, subvalue in value.items() %} 20 | {% if subvalue is mapping %} 21 | {% for subsubkey, subsubvalue in subvalue.items() %} 22 | {% set _ = java_tool_options.append('-Dcontrast.' + key + '.' + subkey + '.' + subsubkey + '=' + subsubvalue | string) %} 23 | {% endfor %} 24 | {% else %} 25 | {% set _ = java_tool_options.append('-Dcontrast.' + key + '.' + subkey + '=' + subvalue | string) %} 26 | {% endif %} 27 | {% endfor %} 28 | {% else %} 29 | {% set _ = java_tool_options.append('-Dcontrast.' + key + '=' + value | string) %} 30 | {% endif %} 31 | {% endfor %} 32 | {% endif %} 33 | 34 | # Add custom JVM arguments 35 | {% for arg in contrast_jvm_args %} 36 | {% set _ = java_tool_options.append(arg) %} 37 | {% endfor %} 38 | 39 | Environment="JAVA_TOOL_OPTIONS={{ java_tool_options | join(' ') }}" 40 | 41 | # Set environment variables if using environment configuration 42 | {% if contrast_config_source == 'environment' and contrast_security_config %} 43 | {% for key, value in contrast_security_config.items() recursive %} 44 | {% if value is mapping %} 45 | {% for subkey, subvalue in value.items() %} 46 | {% if subvalue is mapping %} 47 | {% for subsubkey, subsubvalue in subvalue.items() %} 48 | Environment="CONTRAST__{{ key | upper }}__{{ subkey | upper }}__{{ subsubkey | upper }}={{ subsubvalue }}" 49 | {% endfor %} 50 | {% else %} 51 | Environment="CONTRAST__{{ key | upper }}__{{ subkey | upper }}={{ subvalue }}" 52 | {% endif %} 53 | {% endfor %} 54 | {% else %} 55 | Environment="CONTRAST__{{ key | upper }}={{ value }}" 56 | {% endif %} 57 | {% endfor %} 58 | {% endif %} 59 | -------------------------------------------------------------------------------- /roles/agent/templates/contrast_security.yaml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | # {{ ansible_managed }} 3 | # Contrast Security Agent Configuration 4 | # Generated for: {{ contrast_service_name }} 5 | 6 | {% if contrast_security_config %} 7 | # API Configuration 8 | {% if contrast_security_config.api is defined %} 9 | api: 10 | {% for key, value in contrast_security_config.api.items() %} 11 | {{ key }}: {{ value | to_json }} 12 | {% endfor %} 13 | {% endif %} 14 | 15 | # Application Configuration 16 | {% if contrast_security_config.application is defined %} 17 | application: 18 | {% for key, value in contrast_security_config.application.items() %} 19 | {{ key }}: {{ value | to_json }} 20 | {% endfor %} 21 | {% endif %} 22 | 23 | # Server Configuration 24 | {% if contrast_security_config.server is defined %} 25 | server: 26 | {% for key, value in contrast_security_config.server.items() %} 27 | {{ key }}: {{ value | to_json }} 28 | {% endfor %} 29 | {% endif %} 30 | 31 | # Agent Configuration 32 | {% if contrast_security_config.agent is defined %} 33 | agent: 34 | {% for key, value in contrast_security_config.agent.items() recursive %} 35 | {% if value is mapping %} 36 | {{ key }}: 37 | {% for subkey, subvalue in value.items() %} 38 | {{ subkey }}: {{ subvalue | to_json }} 39 | {% endfor %} 40 | {% else %} 41 | {{ key }}: {{ value | to_json }} 42 | {% endif %} 43 | {% endfor %} 44 | {% endif %} 45 | 46 | # Assess Configuration 47 | {% if contrast_security_config.assess is defined %} 48 | assess: 49 | {% for key, value in contrast_security_config.assess.items() %} 50 | {{ key }}: {{ value | to_json }} 51 | {% endfor %} 52 | {% endif %} 53 | 54 | # Protect Configuration 55 | {% if contrast_security_config.protect is defined %} 56 | protect: 57 | {% for key, value in contrast_security_config.protect.items() %} 58 | {{ key }}: {{ value | to_json }} 59 | {% endfor %} 60 | {% endif %} 61 | 62 | # Additional configurations 63 | {% for section, config in contrast_security_config.items() if section not in ['api', 'application', 'server', 'agent', 'assess', 'protect'] %} 64 | {{ section }}: 65 | {% for key, value in config.items() recursive %} 66 | {% if value is mapping %} 67 | {{ key }}: 68 | {% for subkey, subvalue in value.items() %} 69 | {{ subkey }}: {{ subvalue | to_json }} 70 | {% endfor %} 71 | {% else %} 72 | {{ key }}: {{ value | to_json }} 73 | {% endif %} 74 | {% endfor %} 75 | {% endfor %} 76 | 77 | {% else %} 78 | # No configuration provided 79 | # Please set contrast_security_config in your playbook 80 | {% endif %} 81 | -------------------------------------------------------------------------------- /roles/agent/molecule/vagrant/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | become: true 5 | gather_facts: true 6 | 7 | pre_tasks: 8 | - name: Set service name for testing 9 | set_fact: 10 | contrast_service_name: "{{ tomcat_package | default('tomcat9' if ansible_os_family == 'Debian' else 'tomcat') }}" 11 | when: contrast_service_name is not defined 12 | 13 | tasks: 14 | - name: "Include contrast.security.agent role - Basic activation" 15 | include_role: 16 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 17 | vars: 18 | contrast_agent_enabled: true 19 | contrast_agent_version: "6.19.0" 20 | contrast_config_method: "systemd" 21 | contrast_config_source: "environment" 22 | contrast_test_mode: true 23 | contrast_security_config: 24 | api: 25 | url: "https://app.contrastsecurity.com/Contrast" 26 | api_key: "test-api-key" 27 | service_key: "test-service-key" 28 | user_name: "test-user" 29 | application: 30 | name: "TestApp" 31 | tags: "env:test,scenario:vagrant" 32 | agent: 33 | logger: 34 | path: "/var/log/contrast/contrast.log" 35 | level: "INFO" 36 | 37 | - name: Wait a moment for changes to settle 38 | pause: 39 | seconds: 2 40 | 41 | - name: "Test role idempotency" 42 | include_role: 43 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 44 | vars: 45 | contrast_agent_enabled: true 46 | contrast_agent_version: "6.19.0" 47 | contrast_config_method: "systemd" 48 | contrast_config_source: "environment" 49 | contrast_test_mode: true 50 | contrast_security_config: 51 | api: 52 | url: "https://app.contrastsecurity.com/Contrast" 53 | api_key: "test-api-key" 54 | service_key: "test-service-key" 55 | user_name: "test-user" 56 | application: 57 | name: "TestApp" 58 | tags: "env:test,scenario:vagrant" 59 | agent: 60 | logger: 61 | path: "/var/log/contrast/contrast.log" 62 | level: "INFO" 63 | register: second_run 64 | 65 | - name: Assert idempotency 66 | assert: 67 | that: 68 | - not second_run.changed 69 | fail_msg: "Role is not idempotent - second run should not make changes" 70 | success_msg: "Role is idempotent" 71 | -------------------------------------------------------------------------------- /roles/agent/tasks/configure.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # configure.yml - Agent configuration tasks 3 | 4 | - name: Create Contrast configuration directory 5 | ansible.builtin.file: 6 | path: "{{ contrast_config_path | dirname }}" 7 | state: directory 8 | owner: "{{ contrast_file_owner }}" 9 | group: "{{ contrast_file_group }}" 10 | mode: "{{ contrast_directory_mode }}" 11 | become: true 12 | when: 13 | - contrast_config_source == 'yaml' 14 | - contrast_create_directories | bool 15 | 16 | - name: Deploy contrast_security.yaml from template 17 | ansible.builtin.template: 18 | src: "{{ contrast_yaml_template_src }}" 19 | dest: "{{ contrast_config_path }}" 20 | owner: "{{ contrast_file_owner }}" 21 | group: "{{ contrast_file_group }}" 22 | mode: "{{ contrast_file_mode }}" 23 | backup: yes 24 | become: true 25 | when: contrast_config_source == 'yaml' 26 | notify: 27 | - Reload systemd 28 | - Restart {{ contrast_service_name }} 29 | 30 | - name: Build environment variables from config dictionary 31 | ansible.builtin.set_fact: 32 | contrast_env_vars: | 33 | {% set env_vars = [] %} 34 | {% for key, value in contrast_security_config.items() recursive %} 35 | {% if value is mapping %} 36 | {% for subkey, subvalue in value.items() %} 37 | {% if subvalue is mapping %} 38 | {% for subsubkey, subsubvalue in subvalue.items() %} 39 | {% set var_name = 'CONTRAST__' + key.upper() + '__' + subkey.upper() + '__' + subsubkey.upper() %} 40 | {% set _ = env_vars.append(var_name + '=' + subsubvalue | string) %} 41 | {% endfor %} 42 | {% else %} 43 | {% set var_name = 'CONTRAST__' + key.upper() + '__' + subkey.upper() %} 44 | {% set _ = env_vars.append(var_name + '=' + subvalue | string) %} 45 | {% endif %} 46 | {% endfor %} 47 | {% else %} 48 | {% set var_name = 'CONTRAST__' + key.upper() %} 49 | {% set _ = env_vars.append(var_name + '=' + value | string) %} 50 | {% endif %} 51 | {% endfor %} 52 | {{ env_vars }} 53 | when: 54 | - contrast_config_source == 'environment' 55 | - contrast_security_config | length > 0 56 | 57 | - name: Display configuration method 58 | ansible.builtin.debug: 59 | msg: > 60 | Configuration method: {{ contrast_config_source }} 61 | {% if contrast_config_source == 'yaml' %} 62 | Configuration file: {{ contrast_config_path }} 63 | {% else %} 64 | Environment variables will be set during activation 65 | {% endif %} 66 | -------------------------------------------------------------------------------- /roles/agent/tasks/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # install.yml - Agent download and placement tasks 3 | 4 | - name: Create Contrast agent directory 5 | ansible.builtin.file: 6 | path: "{{ contrast_agent_path | dirname }}" 7 | state: directory 8 | owner: "{{ contrast_file_owner }}" 9 | group: "{{ contrast_file_group }}" 10 | mode: "{{ contrast_directory_mode }}" 11 | become: true 12 | when: contrast_create_directories | bool 13 | 14 | - name: Get latest version from Maven metadata 15 | block: 16 | - name: Download Maven metadata 17 | ansible.builtin.uri: 18 | url: https://repo1.maven.org/maven2/com/contrastsecurity/contrast-agent/maven-metadata.xml 19 | return_content: yes 20 | register: maven_metadata 21 | when: contrast_agent_version == 'LATEST' 22 | 23 | - name: Extract latest version 24 | ansible.builtin.set_fact: 25 | contrast_agent_actual_version: "{{ maven_metadata.content | regex_search('([^<]+)', '\\1') | first }}" 26 | when: 27 | - contrast_agent_version == 'LATEST' 28 | - maven_metadata is defined 29 | - maven_metadata.content is defined 30 | 31 | - name: Set actual version for specific version 32 | ansible.builtin.set_fact: 33 | contrast_agent_actual_version: "{{ contrast_agent_version }}" 34 | when: contrast_agent_version != 'LATEST' 35 | 36 | - name: Set download URL for agent JAR 37 | ansible.builtin.set_fact: 38 | contrast_agent_jar_url: "https://repo1.maven.org/maven2/com/contrastsecurity/contrast-agent/{{ contrast_agent_actual_version }}/contrast-agent-{{ contrast_agent_actual_version }}.jar" 39 | when: 40 | - contrast_agent_download_url is search('maven-metadata.xml') 41 | - contrast_agent_actual_version is defined 42 | 43 | - name: Use provided download URL 44 | ansible.builtin.set_fact: 45 | contrast_agent_jar_url: "{{ contrast_agent_download_url }}" 46 | when: contrast_agent_download_url is not search('maven-metadata.xml') 47 | 48 | - name: Download Contrast agent JAR 49 | ansible.builtin.get_url: 50 | url: "{{ contrast_agent_jar_url }}" 51 | dest: "{{ contrast_agent_path }}" 52 | owner: "{{ contrast_file_owner }}" 53 | group: "{{ contrast_file_group }}" 54 | mode: "{{ contrast_file_mode }}" 55 | timeout: "{{ contrast_download_timeout }}" 56 | validate_certs: "{{ contrast_download_validate_certs }}" 57 | become: true 58 | register: agent_download 59 | 60 | - name: Display download status 61 | ansible.builtin.debug: 62 | msg: "Contrast agent {{ contrast_agent_actual_version | default(contrast_agent_version) }} downloaded to {{ contrast_agent_path }}" 63 | when: agent_download.changed 64 | -------------------------------------------------------------------------------- /roles/agent/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for contrast.security.agent role 3 | 4 | # Master toggle to enable or disable agent activation 5 | # If true, the role activates the agent. If false, it deactivates the agent. 6 | contrast_agent_enabled: false 7 | 8 | # The name of the systemd service to target for agent injection 9 | contrast_service_name: tomcat 10 | 11 | # Version of the agent to download 12 | # Can be a specific version number (e.g., '6.19.0') or 'LATEST' 13 | contrast_agent_version: LATEST 14 | 15 | # Full URL to download the agent JAR 16 | # Defaults to Maven Central URL for the specified version 17 | # Can be overridden for EOP or air-gapped environments 18 | contrast_agent_download_url: "{% if contrast_agent_version == 'LATEST' %}https://repo1.maven.org/maven2/com/contrastsecurity/contrast-agent/maven-metadata.xml{% else %}https://repo1.maven.org/maven2/com/contrastsecurity/contrast-agent/{{ contrast_agent_version }}/contrast-agent-{{ contrast_agent_version }}.jar{% endif %}" 19 | 20 | # Filesystem path where the contrast.jar will be placed 21 | contrast_agent_path: /opt/contrast/contrast.jar 22 | 23 | # Method for agent activation and configuration 24 | # Choices: systemd, setenv 25 | contrast_config_method: systemd 26 | 27 | # Source of configuration settings 28 | # Choices: environment, yaml 29 | contrast_config_source: environment 30 | 31 | # A dictionary of Contrast agent settings 32 | # Maps directly to contrast_security.yaml keys 33 | contrast_security_config: {} 34 | # Example: 35 | # api: 36 | # url: "https://app.contrastsecurity.com/Contrast" 37 | # api_key: "your-api-key" 38 | # service_key: "your-service-key" 39 | # user_name: "your-user-name" 40 | # application: 41 | # name: "MyApp" 42 | # tags: "env:prod,owner:team-alpha" 43 | # agent: 44 | # logger: 45 | # path: "/var/log/contrast/contrast.log" 46 | 47 | # Path to the Jinja2 template for generating contrast_security.yaml 48 | contrast_yaml_template_src: contrast_security.yaml.j2 49 | 50 | # Destination for the generated contrast_security.yaml file 51 | contrast_config_path: /etc/contrast/contrast_security.yaml 52 | 53 | # A list of additional, custom JVM arguments to inject 54 | contrast_jvm_args: [] 55 | # Example for DataDog compatibility: 56 | # - "-Ddd.trace.classes.exclude=com.contrast*" 57 | # - "-Xmx1280m" 58 | 59 | # Create necessary directories 60 | contrast_create_directories: true 61 | 62 | # Owner and group for Contrast files 63 | contrast_file_owner: root 64 | contrast_file_group: root 65 | 66 | # Permissions for directories and files 67 | contrast_directory_mode: '0755' 68 | contrast_file_mode: '0644' 69 | 70 | # Download timeout in seconds 71 | contrast_download_timeout: 60 72 | 73 | # Whether to validate SSL certificates when downloading 74 | contrast_download_validate_certs: true 75 | -------------------------------------------------------------------------------- /roles/agent/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | become: true 5 | 6 | pre_tasks: 7 | - name: Update apt cache 8 | apt: 9 | update_cache: yes 10 | when: ansible_os_family == "Debian" 11 | changed_when: false 12 | 13 | # Skip yum cache update as it's not necessary for testing and can cause issues 14 | - name: Skip yum cache update for testing 15 | debug: 16 | msg: "Skipping yum cache update for container testing" 17 | when: ansible_os_family == "RedHat" 18 | 19 | - name: Install required packages (Debian) 20 | package: 21 | name: 22 | - procps 23 | state: present 24 | when: ansible_os_family == "Debian" 25 | 26 | - name: Install required packages (RedHat) 27 | shell: yum install -y procps-ng 28 | when: ansible_os_family == "RedHat" 29 | changed_when: false 30 | 31 | - name: Create mock tomcat directories 32 | file: 33 | path: "{{ item }}" 34 | state: directory 35 | mode: '0755' 36 | loop: 37 | - /opt/tomcat 38 | - /opt/tomcat/bin 39 | 40 | - name: Create mock tomcat setenv.sh script 41 | copy: 42 | dest: /opt/tomcat/bin/setenv.sh 43 | content: | 44 | #!/bin/bash 45 | # Mock setenv.sh for testing 46 | export JAVA_OPTS="" 47 | mode: '0755' 48 | force: no 49 | 50 | - name: Create mock systemd directory 51 | file: 52 | path: /etc/systemd/system 53 | state: directory 54 | mode: '0755' 55 | 56 | - name: Create mock tomcat service file 57 | copy: 58 | dest: /etc/systemd/system/tomcat.service 59 | content: | 60 | [Unit] 61 | Description=Mock Tomcat Service for Testing 62 | 63 | [Service] 64 | Type=simple 65 | ExecStart=/bin/sleep infinity 66 | ExecReload=/bin/true 67 | 68 | [Install] 69 | WantedBy=multi-user.target 70 | mode: '0644' 71 | 72 | tasks: 73 | - name: "Include contrast.security.agent role" 74 | include_role: 75 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 76 | vars: 77 | contrast_agent_enabled: true 78 | contrast_service_name: "tomcat" 79 | contrast_config_method: "setenv" 80 | contrast_agent_version: "6.19.0" 81 | contrast_test_mode: true 82 | contrast_security_config: 83 | api: 84 | url: "https://app.contrastsecurity.com/Contrast" 85 | api_key: "test-api-key" 86 | service_key: "test-service-key" 87 | user_name: "test-user" 88 | application: 89 | name: "molecule-test-app" 90 | tags: "env:test,molecule:true" 91 | server: 92 | name: "molecule-test-server" 93 | environment: "test" 94 | -------------------------------------------------------------------------------- /TESTING_STRATEGY.md: -------------------------------------------------------------------------------- 1 | # Testing Strategy Guide 2 | 3 | This document explains when to use each testing approach in the Contrast Security Ansible Collection. 4 | 5 | ## Testing Approaches Overview 6 | 7 | | Approach | Purpose | Duration | Use Case | 8 | |----------|---------|----------|-----------| 9 | | `test-docker.sh` | Quick validation | ~2 minutes | Development, quick checks | 10 | | `molecule test` (default) | Full x86_64 testing | ~10 minutes | CI/CD, comprehensive validation | 11 | | `molecule test -s apple-silicon` | Full ARM64 testing | ~10 minutes | Apple Silicon comprehensive testing | 12 | 13 | ## When to Use Each Approach 14 | 15 | ### Use `test-docker.sh` when: 16 | - ✅ **Developing on Apple Silicon Mac** 17 | - ✅ **Quick "does it work?" validation** 18 | - ✅ **Testing specific OS targets** 19 | - ✅ **Local development workflow** 20 | - ✅ **Fast feedback needed** 21 | 22 | ### Use `molecule test` (default) when: 23 | - ✅ **Running on Intel/AMD systems** 24 | - ✅ **CI/CD pipeline testing** 25 | - ✅ **Comprehensive validation needed** 26 | - ✅ **Testing multiple scenarios** 27 | - ✅ **Before releasing changes** 28 | 29 | ### Use `molecule test -s apple-silicon` when: 30 | - ✅ **Apple Silicon comprehensive testing** 31 | - ✅ **Validating ARM64 compatibility** 32 | - ✅ **Testing systemd integration on ARM64** 33 | - ✅ **Full test suite on Apple Silicon** 34 | 35 | ## Examples 36 | 37 | ### Developer Workflow (Apple Silicon) 38 | ```bash 39 | # Quick check during development 40 | ./test-docker.sh ubuntu2204 41 | 42 | # Full validation before committing 43 | molecule test -s apple-silicon 44 | ``` 45 | 46 | ### CI/CD Pipeline 47 | ```bash 48 | # x86_64 systems 49 | molecule test 50 | 51 | # ARM64 systems (if needed) 52 | molecule test -s apple-silicon 53 | ``` 54 | 55 | ### Manual Testing Different Targets 56 | ```bash 57 | # Test Ubuntu 24.04 quickly 58 | ./test-docker.sh ubuntu2404 59 | 60 | # Test Rocky Linux 9 quickly 61 | ./test-docker.sh rocky9 62 | 63 | # Full test with all scenarios 64 | molecule test -s apple-silicon 65 | ``` 66 | 67 | ## Performance Comparison 68 | 69 | | Test Type | Container Startup | Test Execution | Total Time | 70 | |-----------|------------------|----------------|------------| 71 | | `test-docker.sh` | ~30s | ~90s | ~2 minutes | 72 | | `molecule` | ~60s | ~8 minutes | ~10 minutes | 73 | 74 | ## Architecture Support 75 | 76 | | Platform | test-docker.sh | molecule (default) | molecule (apple-silicon) | 77 | |----------|----------------|-------------------|-------------------------| 78 | | Intel/AMD Mac | ✅ | ✅ | ❌ | 79 | | Apple Silicon | ✅ | ❌ | ✅ | 80 | | Linux x86_64 | ✅ | ✅ | ❌ | 81 | | Linux ARM64 | ✅ | ❌ | ✅ | 82 | 83 | ## Recommendation 84 | 85 | For **most development work** on Apple Silicon: 86 | 1. Use `./test-docker.sh` for quick validation 87 | 2. Use `molecule test -s apple-silicon` before major commits 88 | 3. Let CI/CD handle `molecule test` on x86_64 89 | 90 | This gives you the best of both worlds: fast feedback and comprehensive testing. 91 | -------------------------------------------------------------------------------- /examples/inventory.ini: -------------------------------------------------------------------------------- 1 | # Example inventory for Contrast Security deployment 2 | # This demonstrates different ways to organize your hosts and variables 3 | 4 | [all:vars] 5 | # Global settings 6 | ansible_user=ec2-user 7 | ansible_ssh_private_key_file=~/.ssh/id_rsa 8 | 9 | # Contrast defaults (can be overridden per group/host) 10 | contrast_agent_version=LATEST 11 | contrast_config_method=systemd 12 | contrast_config_source=environment 13 | 14 | [app_servers] 15 | # Production servers 16 | prod-web-01.example.com env=production team=frontend datacenter=us-east-1 17 | prod-web-02.example.com env=production team=frontend datacenter=us-east-1 18 | prod-api-01.example.com env=production team=backend datacenter=us-east-1 19 | prod-api-02.example.com env=production team=backend datacenter=us-east-1 20 | 21 | # Staging servers 22 | stage-web-01.example.com env=staging team=frontend datacenter=us-west-2 23 | stage-api-01.example.com env=staging team=backend datacenter=us-west-2 24 | 25 | # Development servers 26 | dev-web-01.example.com env=development team=frontend datacenter=us-east-1 27 | dev-api-01.example.com env=development team=backend datacenter=us-east-1 28 | 29 | [production] 30 | prod-web-01.example.com 31 | prod-web-02.example.com 32 | prod-api-01.example.com 33 | prod-api-02.example.com 34 | 35 | [staging] 36 | stage-web-01.example.com 37 | stage-api-01.example.com 38 | 39 | [development] 40 | dev-web-01.example.com 41 | dev-api-01.example.com 42 | 43 | [tomcat_servers] 44 | prod-web-01.example.com 45 | prod-web-02.example.com 46 | stage-web-01.example.com 47 | dev-web-01.example.com 48 | 49 | [api_servers] 50 | prod-api-01.example.com 51 | prod-api-02.example.com 52 | stage-api-01.example.com 53 | dev-api-01.example.com 54 | 55 | # Group variables 56 | [production:vars] 57 | contrast_agent_enabled=true 58 | java_heap_size=2048m 59 | contrast_log_level=WARN 60 | assess_enabled=false 61 | protect_enabled=true 62 | 63 | [staging:vars] 64 | contrast_agent_enabled=true 65 | java_heap_size=1024m 66 | contrast_log_level=INFO 67 | assess_enabled=true 68 | protect_enabled=true 69 | 70 | [development:vars] 71 | contrast_agent_enabled=true 72 | java_heap_size=512m 73 | contrast_log_level=DEBUG 74 | assess_enabled=true 75 | protect_enabled=false 76 | 77 | [tomcat_servers:vars] 78 | contrast_service_name=tomcat 79 | app_service_name=tomcat 80 | 81 | [api_servers:vars] 82 | contrast_service_name=tomcat 83 | app_service_name=tomcat 84 | # If using Spring Boot with systemd 85 | # contrast_service_name=myapp 86 | # app_service_name=myapp 87 | 88 | # Example: AWS EC2 dynamic inventory groups 89 | [tag_Environment_Production] 90 | prod-web-01.example.com 91 | prod-web-02.example.com 92 | prod-api-01.example.com 93 | prod-api-02.example.com 94 | 95 | [tag_Application_WebApp] 96 | prod-web-01.example.com 97 | prod-web-02.example.com 98 | stage-web-01.example.com 99 | dev-web-01.example.com 100 | 101 | [tag_Application_API] 102 | prod-api-01.example.com 103 | prod-api-02.example.com 104 | stage-api-01.example.com 105 | dev-api-01.example.com 106 | 107 | # Example host-specific variables 108 | [app_servers:vars] 109 | # You can set host-specific configs here or in host_vars/hostname.yml files 110 | # app_name={{ inventory_hostname_short }} 111 | # app_version=1.0.0 112 | -------------------------------------------------------------------------------- /QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # Contrast Security Ansible Collection - Quick Start Guide 2 | 3 | ## Project Structure Created 4 | 5 | The complete Ansible Collection has been created at: 6 | `/Users/jonathanharper/MCP/contrast.security/` 7 | 8 | ## Key Features Implemented 9 | 10 | ### 1. **Core Role Structure** 11 | - Complete `contrast.security.agent` role with all standard Ansible directories 12 | - Idempotent task execution for installation, configuration, and activation 13 | - Support for both systemd (recommended) and setenv.sh activation methods 14 | 15 | ### 2. **Configuration Options** 16 | - **Environment Variables**: Default method, converts YAML config to env vars 17 | - **YAML File**: Optional method for traditional configuration file approach 18 | - **Flexible Variables**: Comprehensive set of configurable parameters 19 | 20 | ### 3. **Agent Management** 21 | - **Installation**: Downloads from Maven Central or custom repository 22 | - **Activation**: Clean injection of -javaagent flag via systemd or setenv.sh 23 | - **Deactivation**: Complete removal with `contrast_agent_enabled: false` 24 | 25 | ### 4. **Enterprise Features** 26 | - Support for air-gapped environments 27 | - DataDog compatibility configuration 28 | - Version pinning and latest version support 29 | - Comprehensive logging and debugging 30 | 31 | ## Quick Usage 32 | 33 | ### 1. Build the Collection 34 | ```bash 35 | cd /Users/jonathanharper/MCP/contrast.security 36 | ansible-galaxy collection build 37 | ``` 38 | 39 | ### 2. Install Locally 40 | ```bash 41 | ansible-galaxy collection install contrast-security-*.tar.gz 42 | ``` 43 | 44 | ### 3. Basic Playbook 45 | ```yaml 46 | --- 47 | - name: Deploy Contrast 48 | hosts: servers 49 | become: yes 50 | roles: 51 | - role: contrast.security.agent 52 | vars: 53 | contrast_agent_enabled: true 54 | contrast_security_config: 55 | api: 56 | url: "https://app.contrastsecurity.com/Contrast" 57 | api_key: "YOUR_API_KEY" 58 | service_key: "YOUR_SERVICE_KEY" 59 | user_name: "YOUR_USER_NAME" 60 | application: 61 | name: "MyApp" 62 | ``` 63 | 64 | ### 4. Run Playbook 65 | ```bash 66 | ansible-playbook -i inventory playbook.yml 67 | ``` 68 | 69 | ## Testing 70 | 71 | The collection includes Molecule tests: 72 | ```bash 73 | cd roles/agent 74 | molecule test 75 | ``` 76 | 77 | ## Examples Provided 78 | 79 | - `examples/deploy-contrast.yml` - Various deployment scenarios 80 | - `examples/deactivate-contrast.yml` - Safe deactivation examples 81 | - `examples/inventory.ini` - Sample inventory structure 82 | - `examples/group_vars/production.yml` - Production configuration 83 | 84 | ## Next Steps 85 | 86 | 1. Review and customize the configuration for your environment 87 | 2. Update API credentials in the examples 88 | 3. Test in a development environment first 89 | 4. Consider publishing to Ansible Galaxy for wider distribution 90 | 91 | ## Testing 92 | 93 | ### Standard Testing 94 | ```bash 95 | cd roles/agent 96 | molecule test 97 | ``` 98 | 99 | ### Apple Silicon Mac Users 100 | For M1/M2/M3 Macs, use Docker-based testing: 101 | ```bash 102 | ./test-docker.sh ubuntu2204 103 | ``` 104 | See [APPLE_SILICON_SETUP.md](APPLE_SILICON_SETUP.md) for details. 105 | 106 | ## Support 107 | 108 | Refer to the comprehensive documentation in: 109 | - `/README.md` - Collection overview 110 | - `/roles/agent/README.md` - Detailed role documentation 111 | - `/CONTRIBUTING.md` - Contribution guidelines 112 | -------------------------------------------------------------------------------- /roles/agent/molecule/vagrant/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: vagrant 6 | provider: 7 | name: virtualbox 8 | 9 | platforms: 10 | - name: ubuntu2004-vagrant 11 | box: ubuntu/focal64 12 | memory: 1024 13 | cpus: 1 14 | provider_options: 15 | gui: false 16 | groups: 17 | - ubuntu 18 | - systemd_hosts 19 | interfaces: 20 | - network_name: private_network 21 | type: dhcp 22 | 23 | - name: ubuntu2204-vagrant 24 | box: ubuntu/jammy64 25 | memory: 1024 26 | cpus: 1 27 | provider_options: 28 | gui: false 29 | groups: 30 | - ubuntu 31 | - systemd_hosts 32 | interfaces: 33 | - network_name: private_network 34 | type: dhcp 35 | 36 | - name: centos8-vagrant 37 | box: centos/8 38 | memory: 1024 39 | cpus: 1 40 | provider_options: 41 | gui: false 42 | groups: 43 | - centos 44 | - systemd_hosts 45 | interfaces: 46 | - network_name: private_network 47 | type: dhcp 48 | 49 | - name: amazonlinux2-vagrant 50 | box: gbailey/amzn2 51 | memory: 1024 52 | cpus: 1 53 | provider_options: 54 | gui: false 55 | groups: 56 | - amazonlinux 57 | - systemd_hosts 58 | interfaces: 59 | - network_name: private_network 60 | type: dhcp 61 | 62 | provisioner: 63 | name: ansible 64 | inventory: 65 | group_vars: 66 | all: 67 | # Common test variables 68 | contrast_test_mode: true 69 | ansible_python_interpreter: /usr/bin/python3 70 | 71 | ubuntu: 72 | java_package: openjdk-11-jdk 73 | tomcat_package: tomcat9 74 | contrast_service_name: tomcat9 75 | 76 | centos: 77 | java_package: java-11-openjdk-devel 78 | tomcat_package: tomcat 79 | contrast_service_name: tomcat 80 | 81 | amazonlinux: 82 | java_package: java-11-amazon-corretto-devel 83 | tomcat_package: tomcat 84 | contrast_service_name: tomcat 85 | 86 | host_vars: 87 | ubuntu2004-vagrant: 88 | ansible_user: vagrant 89 | ansible_ssh_private_key_file: ~/.vagrant.d/insecure_private_key 90 | 91 | ubuntu2204-vagrant: 92 | ansible_user: vagrant 93 | ansible_ssh_private_key_file: ~/.vagrant.d/insecure_private_key 94 | 95 | centos8-vagrant: 96 | ansible_user: vagrant 97 | ansible_ssh_private_key_file: ~/.vagrant.d/insecure_private_key 98 | 99 | amazonlinux2-vagrant: 100 | ansible_user: vagrant 101 | ansible_ssh_private_key_file: ~/.vagrant.d/insecure_private_key 102 | 103 | config_options: 104 | defaults: 105 | callbacks_enabled: profile_tasks 106 | stdout_callback: yaml 107 | host_key_checking: false 108 | roles_path: /Users/jonathanharper/MCP/contrast.security/roles 109 | ssh_connection: 110 | pipelining: true 111 | 112 | verifier: 113 | name: ansible 114 | 115 | scenario: 116 | name: vagrant 117 | test_sequence: 118 | - dependency 119 | - lint 120 | - destroy 121 | - syntax 122 | - create 123 | - prepare 124 | - converge 125 | - idempotence 126 | - side_effect 127 | - verify 128 | - destroy 129 | 130 | create_sequence: 131 | - dependency 132 | - create 133 | - prepare 134 | 135 | converge_sequence: 136 | - dependency 137 | - create 138 | - prepare 139 | - converge 140 | 141 | destroy_sequence: 142 | - dependency 143 | - destroy 144 | -------------------------------------------------------------------------------- /examples/deactivate-contrast.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Example playbook to deactivate Contrast Security agent 3 | # This cleanly removes the agent configuration and restarts services 4 | 5 | - name: Deactivate Contrast Security Agent 6 | hosts: "{{ target_hosts | default('app_servers') }}" 7 | become: yes 8 | 9 | vars: 10 | # Set to false to deactivate 11 | contrast_agent_enabled: false 12 | 13 | pre_tasks: 14 | - name: Confirm deactivation 15 | pause: 16 | prompt: | 17 | This will deactivate the Contrast agent on the following hosts: 18 | {{ ansible_play_hosts | join(', ') }} 19 | 20 | Press Enter to continue or Ctrl+C to cancel 21 | run_once: true 22 | when: confirm_deactivation | default(true) | bool 23 | 24 | roles: 25 | - role: contrast.security.agent 26 | vars: 27 | contrast_service_name: "{{ app_service_name | default('tomcat') }}" 28 | 29 | post_tasks: 30 | - name: Verify agent is not running 31 | shell: "ps aux | grep -i contrast | grep -v grep || true" 32 | register: contrast_check 33 | changed_when: false 34 | 35 | - name: Check systemd override removal 36 | stat: 37 | path: "/etc/systemd/system/{{ contrast_service_name }}.service.d/10-contrast.conf" 38 | register: override_file 39 | when: contrast_config_method | default('systemd') == 'systemd' 40 | 41 | - name: Display deactivation status 42 | debug: 43 | msg: | 44 | Contrast agent deactivation complete! 45 | 46 | Agent running: {{ 'No' if contrast_check.stdout == '' else 'Still running - will stop on next restart' }} 47 | {% if contrast_config_method | default('systemd') == 'systemd' %} 48 | Override file removed: {{ 'Yes' if not override_file.stat.exists else 'No' }} 49 | {% endif %} 50 | 51 | The {{ contrast_service_name }} service has been restarted without the agent. 52 | 53 | --- 54 | # Example: Selective deactivation based on conditions 55 | 56 | - name: Conditionally Deactivate Contrast Agent 57 | hosts: app_servers 58 | become: yes 59 | 60 | tasks: 61 | - name: Check if we should deactivate (example condition) 62 | set_fact: 63 | should_deactivate: "{{ true if (ansible_memtotal_mb < 2048 or custom_condition | default(false)) else false }}" 64 | 65 | - name: Apply Contrast role with conditional deactivation 66 | include_role: 67 | name: contrast.security.agent 68 | vars: 69 | contrast_agent_enabled: "{{ not should_deactivate }}" 70 | contrast_service_name: "tomcat" 71 | contrast_security_config: "{{ production_contrast_config }}" 72 | when: contrast_management_enabled | default(true) 73 | 74 | --- 75 | # Example: Emergency deactivation across all environments 76 | 77 | - name: Emergency Contrast Agent Deactivation 78 | hosts: all:!excluded_hosts 79 | become: yes 80 | serial: "50%" 81 | max_fail_percentage: 0 82 | 83 | vars: 84 | emergency_deactivation: true 85 | contrast_agent_enabled: false 86 | 87 | tasks: 88 | - name: Include Contrast role for deactivation 89 | include_role: 90 | name: contrast.security.agent 91 | vars: 92 | contrast_service_name: "{{ app_service_name | default('tomcat') }}" 93 | when: 94 | - emergency_deactivation | bool 95 | - inventory_hostname not in critical_hosts | default([]) 96 | 97 | - name: Send notification 98 | mail: 99 | to: "{{ ops_email | default('ops@example.com') }}" 100 | subject: "Contrast Agent Deactivated - {{ inventory_hostname }}" 101 | body: | 102 | The Contrast Security agent has been deactivated on {{ inventory_hostname }}. 103 | 104 | Reason: Emergency deactivation 105 | Time: {{ ansible_date_time.iso8601 }} 106 | Triggered by: {{ ansible_user_id }} 107 | when: 108 | - emergency_deactivation | bool 109 | - send_notifications | default(false) | bool 110 | -------------------------------------------------------------------------------- /roles/agent/tasks/activate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # activate.yml - Agent activation/deactivation tasks 3 | 4 | - name: Agent activation block (systemd method) 5 | block: 6 | - name: Create systemd override directory 7 | ansible.builtin.file: 8 | path: "/etc/systemd/system/{{ contrast_service_name }}.service.d" 9 | state: directory 10 | owner: root 11 | group: root 12 | mode: '0755' 13 | become: true 14 | 15 | - name: Deploy systemd override configuration 16 | ansible.builtin.template: 17 | src: contrast.conf.j2 18 | dest: "/etc/systemd/system/{{ contrast_service_name }}.service.d/10-contrast.conf" 19 | owner: root 20 | group: root 21 | mode: '0644' 22 | backup: yes 23 | become: true 24 | notify: 25 | - Reload systemd 26 | - Restart {{ contrast_service_name }} 27 | when: 28 | - contrast_agent_enabled | bool 29 | - contrast_config_method == 'systemd' 30 | 31 | - name: Agent activation block (setenv method) 32 | block: 33 | - name: Check if setenv.sh exists 34 | ansible.builtin.stat: 35 | path: "/opt/{{ contrast_service_name }}/bin/setenv.sh" 36 | register: setenv_file 37 | become: true 38 | 39 | - name: Create setenv.sh if it doesn't exist 40 | ansible.builtin.file: 41 | path: "/opt/{{ contrast_service_name }}/bin/setenv.sh" 42 | state: touch 43 | owner: "{{ contrast_service_name }}" 44 | group: "{{ contrast_service_name }}" 45 | mode: '0755' 46 | become: true 47 | when: not setenv_file.stat.exists 48 | 49 | - name: Add Contrast configuration to setenv.sh 50 | ansible.builtin.blockinfile: 51 | path: "/opt/{{ contrast_service_name }}/bin/setenv.sh" 52 | marker: "# {mark} ANSIBLE MANAGED BLOCK: Contrast Security" 53 | block: | 54 | # Contrast Security Java Agent Configuration 55 | CONTRAST_OPTS="-javaagent:{{ contrast_agent_path }}" 56 | {% if contrast_config_source == 'yaml' %} 57 | CONTRAST_OPTS="${CONTRAST_OPTS} -Dcontrast.config.path={{ contrast_config_path }}" 58 | {% endif %} 59 | {% if contrast_config_source == 'environment' and contrast_security_config %} 60 | {% for key, value in contrast_security_config.items() recursive %} 61 | {% if value is mapping %} 62 | {% for subkey, subvalue in value.items() %} 63 | CONTRAST_OPTS="${CONTRAST_OPTS} -Dcontrast.{{ key }}.{{ subkey }}={{ subvalue }}" 64 | {% endfor %} 65 | {% else %} 66 | CONTRAST_OPTS="${CONTRAST_OPTS} -Dcontrast.{{ key }}={{ value }}" 67 | {% endif %} 68 | {% endfor %} 69 | {% endif %} 70 | {% for arg in contrast_jvm_args %} 71 | CONTRAST_OPTS="${CONTRAST_OPTS} {{ arg }}" 72 | {% endfor %} 73 | export CATALINA_OPTS="${CATALINA_OPTS} ${CONTRAST_OPTS}" 74 | state: present 75 | become: true 76 | notify: 77 | - Restart {{ contrast_service_name }} for setenv 78 | when: 79 | - contrast_agent_enabled | bool 80 | - contrast_config_method == 'setenv' 81 | 82 | - name: Agent deactivation block (systemd method) 83 | block: 84 | - name: Remove systemd override configuration 85 | ansible.builtin.file: 86 | path: "/etc/systemd/system/{{ contrast_service_name }}.service.d/10-contrast.conf" 87 | state: absent 88 | become: true 89 | notify: 90 | - Reload systemd 91 | - Restart {{ contrast_service_name }} 92 | 93 | - name: Check if override directory is empty 94 | ansible.builtin.find: 95 | paths: "/etc/systemd/system/{{ contrast_service_name }}.service.d" 96 | file_type: file 97 | register: override_files 98 | become: true 99 | 100 | - name: Remove empty override directory 101 | ansible.builtin.file: 102 | path: "/etc/systemd/system/{{ contrast_service_name }}.service.d" 103 | state: absent 104 | become: true 105 | when: override_files.files | length == 0 106 | when: 107 | - not contrast_agent_enabled | bool 108 | - contrast_config_method == 'systemd' 109 | 110 | - name: Agent deactivation block (setenv method) 111 | block: 112 | - name: Remove Contrast configuration from setenv.sh 113 | ansible.builtin.blockinfile: 114 | path: "/opt/{{ contrast_service_name }}/bin/setenv.sh" 115 | marker: "# {mark} ANSIBLE MANAGED BLOCK: Contrast Security" 116 | state: absent 117 | become: true 118 | notify: 119 | - Restart {{ contrast_service_name }} for setenv 120 | when: 121 | - not contrast_agent_enabled | bool 122 | - contrast_config_method == 'setenv' 123 | -------------------------------------------------------------------------------- /tests/vagrant.conf: -------------------------------------------------------------------------------- 1 | # Vagrant Testing Configuration 2 | # This file contains configuration for comprehensive Vagrant-based testing 3 | 4 | # VM Resource Configuration 5 | VM_MEMORY=1024 6 | VM_CPUS=1 7 | VM_DISK_SIZE=10240 8 | 9 | # Test Configuration 10 | DEFAULT_AGENT_VERSION="6.19.0" 11 | DEFAULT_CONFIG_METHOD="systemd" 12 | DEFAULT_SERVICE_NAME="tomcat" 13 | 14 | # Network Configuration 15 | NETWORK_TYPE="private_network" 16 | NETWORK_DHCP=true 17 | BASE_IP="192.168.56" 18 | 19 | # Test Scenarios Configuration 20 | declare -A TEST_CONFIGS=( 21 | # Basic installation test 22 | ["basic"]=" 23 | contrast_agent_enabled: true 24 | contrast_agent_version: '$DEFAULT_AGENT_VERSION' 25 | contrast_config_method: '$DEFAULT_CONFIG_METHOD' 26 | contrast_test_mode: true 27 | " 28 | 29 | # Environment variable configuration test 30 | ["env_config"]=" 31 | contrast_agent_enabled: true 32 | contrast_config_source: 'environment' 33 | contrast_security_config: 34 | api: 35 | url: 'https://app.contrastsecurity.com/Contrast' 36 | api_key: 'test-api-key' 37 | application: 38 | name: 'TestApp' 39 | " 40 | 41 | # YAML configuration test 42 | ["yaml_config"]=" 43 | contrast_agent_enabled: true 44 | contrast_config_source: 'yaml' 45 | contrast_security_config: 46 | api: 47 | url: 'https://app.contrastsecurity.com/Contrast' 48 | api_key: 'test-api-key' 49 | agent: 50 | logger: 51 | level: 'INFO' 52 | " 53 | 54 | # setenv.sh configuration test 55 | ["setenv_config"]=" 56 | contrast_agent_enabled: true 57 | contrast_config_method: 'setenv' 58 | contrast_service_name: '$DEFAULT_SERVICE_NAME' 59 | " 60 | 61 | # Deactivation test 62 | ["deactivation"]=" 63 | contrast_agent_enabled: false 64 | contrast_config_method: '$DEFAULT_CONFIG_METHOD' 65 | " 66 | ) 67 | 68 | # OS-specific configurations 69 | declare -A OS_CONFIGS=( 70 | ["ubuntu2004"]=" 71 | java_package: 'openjdk-11-jdk' 72 | tomcat_package: 'tomcat9' 73 | service_name: 'tomcat9' 74 | package_manager: 'apt' 75 | " 76 | 77 | ["ubuntu2204"]=" 78 | java_package: 'openjdk-11-jdk' 79 | tomcat_package: 'tomcat9' 80 | service_name: 'tomcat9' 81 | package_manager: 'apt' 82 | " 83 | 84 | ["centos8"]=" 85 | java_package: 'java-11-openjdk-devel' 86 | tomcat_package: 'tomcat' 87 | service_name: 'tomcat' 88 | package_manager: 'yum' 89 | " 90 | 91 | ["rocky9"]=" 92 | java_package: 'java-11-openjdk-devel' 93 | tomcat_package: 'tomcat' 94 | service_name: 'tomcat' 95 | package_manager: 'dnf' 96 | " 97 | 98 | ["amazonlinux2"]=" 99 | java_package: 'java-11-amazon-corretto-devel' 100 | tomcat_package: 'tomcat' 101 | service_name: 'tomcat' 102 | package_manager: 'yum' 103 | " 104 | ) 105 | 106 | # Test timeouts (in minutes) 107 | VM_START_TIMEOUT=10 108 | TEST_EXECUTION_TIMEOUT=30 109 | VM_DESTROY_TIMEOUT=5 110 | 111 | # Parallel execution settings 112 | MAX_PARALLEL_VMS=3 113 | PARALLEL_ENABLED=false 114 | 115 | # Logging configuration 116 | LOG_LEVEL="INFO" 117 | LOG_TO_FILE=true 118 | LOG_DIRECTORY="test-results" 119 | 120 | # Cleanup settings 121 | AUTO_CLEANUP=true 122 | KEEP_LOGS=true 123 | SNAPSHOT_BEFORE_TEST=false 124 | 125 | # Advanced settings 126 | VAGRANT_PROVIDER="virtualbox" 127 | ANSIBLE_VERBOSITY=1 128 | MOLECULE_DEBUG=false 129 | 130 | # Test data locations 131 | TEST_DATA_DIR="tests/data" 132 | INVENTORY_FILE="tests/vagrant/inventory" 133 | SETUP_PLAYBOOK="tests/vagrant/setup.yml" 134 | 135 | # Custom test configurations can be added here 136 | # Format: TEST_NAME="ansible_variable: value" 137 | CUSTOM_TESTS=( 138 | # Add your custom test configurations here 139 | # Example: 140 | # "custom_version_test=contrast_agent_version: '6.18.0'" 141 | ) 142 | 143 | # CI/CD specific settings 144 | CI_MODE=false 145 | CI_PARALLEL_JOBS=2 146 | CI_ARTIFACT_RETENTION_DAYS=7 147 | 148 | # Export all variables for use in scripts 149 | export VM_MEMORY VM_CPUS VM_DISK_SIZE 150 | export DEFAULT_AGENT_VERSION DEFAULT_CONFIG_METHOD DEFAULT_SERVICE_NAME 151 | export NETWORK_TYPE NETWORK_DHCP BASE_IP 152 | export VM_START_TIMEOUT TEST_EXECUTION_TIMEOUT VM_DESTROY_TIMEOUT 153 | export MAX_PARALLEL_VMS PARALLEL_ENABLED 154 | export LOG_LEVEL LOG_TO_FILE LOG_DIRECTORY 155 | export AUTO_CLEANUP KEEP_LOGS SNAPSHOT_BEFORE_TEST 156 | export VAGRANT_PROVIDER ANSIBLE_VERBOSITY MOLECULE_DEBUG 157 | export TEST_DATA_DIR INVENTORY_FILE SETUP_PLAYBOOK 158 | export CI_MODE CI_PARALLEL_JOBS CI_ARTIFACT_RETENTION_DAYS 159 | -------------------------------------------------------------------------------- /test-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Docker-based testing for Apple Silicon Macs 4 | # Uses native ARM64 containers for fast, reliable testing 5 | 6 | set -euo pipefail 7 | 8 | log() { 9 | echo "[$(date '+%H:%M:%S')] $*" 10 | } 11 | 12 | check_docker() { 13 | if ! command -v docker &> /dev/null; then 14 | log "ERROR: Docker not found. Please install Docker Desktop." 15 | log "INFO: Download from: https://www.docker.com/products/docker-desktop/" 16 | exit 1 17 | fi 18 | 19 | if ! docker info &> /dev/null; then 20 | log "ERROR: Docker daemon not running. Please start Docker Desktop." 21 | exit 1 22 | fi 23 | 24 | log "SUCCESS: Docker is ready" 25 | } 26 | 27 | test_contrast_agent() { 28 | local os_target=${1:-ubuntu2204} 29 | 30 | # ARM64-compatible images 31 | local image 32 | case $os_target in 33 | ubuntu2004) image="ubuntu:20.04" ;; 34 | ubuntu2204) image="ubuntu:22.04" ;; 35 | ubuntu2404) image="ubuntu:24.04" ;; 36 | centos9) image="quay.io/centos/centos:stream9" ;; 37 | rocky9) image="rockylinux:9" ;; 38 | *) image="ubuntu:22.04" ;; 39 | esac 40 | 41 | log "Running Contrast agent test in $image container (ARM64)" 42 | 43 | # Create test playbook for container 44 | cat > container_test.yml << 'EOF' 45 | --- 46 | - name: Test Contrast Agent in Container 47 | hosts: localhost 48 | connection: local 49 | become: true 50 | vars: 51 | contrast_agent_enabled: true 52 | contrast_agent_version: "6.19.0" 53 | contrast_config_method: "environment" # Use environment instead of systemd 54 | contrast_test_mode: true 55 | contrast_security_config: 56 | api: 57 | url: "https://app.contrastsecurity.com/Contrast" 58 | api_key: "container-test-key" 59 | service_key: "container-test-service" 60 | user_name: "container-test-user" 61 | application: 62 | name: "ContainerTest" 63 | 64 | tasks: 65 | - name: Include contrast agent role 66 | include_role: 67 | name: agent 68 | 69 | - name: Verify agent installation 70 | stat: 71 | path: /opt/contrast/contrast.jar 72 | register: agent_check 73 | 74 | - name: Show test result 75 | debug: 76 | msg: "✅ Container test {{ 'PASSED' if agent_check.stat.exists else 'FAILED' }}" 77 | EOF 78 | 79 | # Run test in container (simplified, no systemd) 80 | docker run --rm \ 81 | --name "contrast-test-$os_target" \ 82 | --platform linux/arm64 \ 83 | -v "$PWD:/workspace" \ 84 | -w /workspace \ 85 | "$image" \ 86 | bash -c " 87 | set -e 88 | 89 | # Install prerequisites based on OS 90 | if command -v apt-get >/dev/null 2>&1; then 91 | export DEBIAN_FRONTEND=noninteractive 92 | apt-get update 93 | 94 | # Check Ubuntu version to handle PEP 668 (externally-managed-environment) 95 | if grep -q 'Ubuntu 24\.' /etc/os-release 2>/dev/null; then 96 | echo '[INFO] Ubuntu 24.04+ detected - using virtual environment for Ansible' 97 | # Ubuntu 24.04+ - use system packages and virtual environment 98 | apt-get install -y python3 python3-pip python3-venv python3-full openjdk-11-jdk curl wget sudo 99 | 100 | # Create and use virtual environment for Ansible 101 | echo '[INFO] Creating Python virtual environment...' 102 | python3 -m venv /opt/ansible-venv 103 | echo '[INFO] Installing Ansible in virtual environment...' 104 | /opt/ansible-venv/bin/pip install ansible 105 | # Make ansible commands available 106 | ln -sf /opt/ansible-venv/bin/ansible-playbook /usr/local/bin/ansible-playbook 107 | ln -sf /opt/ansible-venv/bin/ansible /usr/local/bin/ansible 108 | echo '[INFO] Ansible installation completed' 109 | else 110 | echo '[INFO] Older Ubuntu version detected - installing Ansible directly' 111 | # Older Ubuntu versions - can install directly 112 | apt-get install -y python3 python3-pip openjdk-11-jdk curl wget sudo 113 | python3 -m pip install ansible 114 | fi 115 | elif command -v dnf >/dev/null 2>&1; then 116 | echo '[INFO] Red Hat family OS detected' 117 | # Use --allowerasing to handle curl conflicts in Rocky/RHEL systems 118 | dnf install -y --allowerasing python3 python3-pip java-11-openjdk-devel curl wget sudo 119 | python3 -m pip install ansible 120 | fi 121 | 122 | echo '[INFO] Verifying Ansible installation...' 123 | ansible --version 124 | 125 | # Run the test 126 | ansible-playbook container_test.yml 127 | " 128 | 129 | log "SUCCESS: Container test completed" 130 | } 131 | 132 | main() { 133 | check_docker 134 | test_contrast_agent "$@" 135 | } 136 | 137 | main "$@" 138 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Contrast Security Ansible Collection 2 | 3 | We welcome contributions to the Contrast Security Ansible Collection! This document provides guidelines for contributing to this project. 4 | 5 | ## Getting Started 6 | 7 | 1. Fork the repository on GitHub 8 | 2. Clone your fork locally: 9 | ```bash 10 | git clone https://github.com/YOUR_USERNAME/ansible-collection-contrast.git 11 | cd ansible-collection-contrast 12 | ``` 13 | 3. Add the upstream repository: 14 | ```bash 15 | git remote add upstream https://github.com/Contrast-Security-OSS/ansible-collection-contrast.git 16 | ``` 17 | 18 | ## Development Setup 19 | 20 | ### Prerequisites 21 | 22 | - Python 3.6+ 23 | - Ansible 2.9+ 24 | - Docker (for testing with Molecule) 25 | - Molecule and ansible-lint (for testing) 26 | 27 | ### Installing Development Dependencies 28 | 29 | ```bash 30 | pip install --user ansible molecule ansible-lint yamllint molecule-docker 31 | ``` 32 | 33 | ### Building the Collection 34 | 35 | ```bash 36 | ansible-galaxy collection build 37 | ``` 38 | 39 | ### Installing Locally for Testing 40 | 41 | ```bash 42 | ansible-galaxy collection install contrast-security-*.tar.gz --force 43 | ``` 44 | 45 | ## Making Changes 46 | 47 | ### Branch Naming 48 | 49 | Create a feature branch with a descriptive name: 50 | - `feature/add-xyz-support` 51 | - `bugfix/fix-systemd-handler` 52 | - `docs/update-readme` 53 | 54 | ### Code Style 55 | 56 | - Follow [Ansible Best Practices](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html) 57 | - Use YAML formatting (not JSON) 58 | - Indent with 2 spaces (not tabs) 59 | - Add comments for complex logic 60 | - Use meaningful variable names 61 | 62 | ### Commit Messages 63 | 64 | Follow conventional commit format: 65 | ``` 66 | type(scope): brief description 67 | 68 | Longer explanation if needed. Wrap at 72 characters. 69 | 70 | Fixes #123 71 | ``` 72 | 73 | Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` 74 | 75 | ## Testing 76 | 77 | ### Running Tests Locally 78 | 79 | 1. Navigate to the role directory: 80 | ```bash 81 | cd roles/agent 82 | ``` 83 | 84 | 2. Run Molecule tests: 85 | ```bash 86 | molecule test 87 | ``` 88 | 89 | 3. Run specific test scenarios: 90 | ```bash 91 | molecule test -s systemd 92 | ``` 93 | 94 | ### Test Requirements 95 | 96 | - All new features must include tests 97 | - All bug fixes should include regression tests 98 | - Tests must pass on all supported platforms 99 | 100 | ### Linting 101 | 102 | Run ansible-lint before submitting: 103 | ```bash 104 | ansible-lint roles/agent/ 105 | yamllint . 106 | ``` 107 | 108 | ## Submitting Changes 109 | 110 | ### Pull Request Process 111 | 112 | 1. Update your fork: 113 | ```bash 114 | git checkout main 115 | git pull upstream main 116 | git push origin main 117 | ``` 118 | 119 | 2. Rebase your feature branch: 120 | ```bash 121 | git checkout feature/your-feature 122 | git rebase main 123 | ``` 124 | 125 | 3. Push your changes: 126 | ```bash 127 | git push origin feature/your-feature 128 | ``` 129 | 130 | 4. Create a Pull Request on GitHub 131 | 132 | ### Pull Request Guidelines 133 | 134 | - **Title**: Clear and descriptive 135 | - **Description**: 136 | - What changes were made 137 | - Why were they made 138 | - How were they tested 139 | - Related issues/PRs 140 | - **Tests**: All tests must pass 141 | - **Documentation**: Update if needed 142 | - **Changelog**: Add entry if applicable 143 | 144 | ### PR Template 145 | 146 | ```markdown 147 | ## Description 148 | Brief description of changes 149 | 150 | ## Type of Change 151 | - [ ] Bug fix 152 | - [ ] New feature 153 | - [ ] Documentation update 154 | - [ ] Performance improvement 155 | 156 | ## Testing 157 | - [ ] Tested locally 158 | - [ ] Added/updated tests 159 | - [ ] All tests pass 160 | 161 | ## Checklist 162 | - [ ] Code follows style guidelines 163 | - [ ] Self-review completed 164 | - [ ] Documentation updated 165 | - [ ] Changelog updated 166 | ``` 167 | 168 | ## Documentation 169 | 170 | ### Where to Add Documentation 171 | 172 | - **Role README**: `/roles/agent/README.md` 173 | - **Collection README**: `/README.md` 174 | - **Examples**: `/examples/` 175 | - **Inline**: YAML comments for complex tasks 176 | 177 | ### Documentation Standards 178 | 179 | - Use clear, concise language 180 | - Include examples for all features 181 | - Document all variables 182 | - Keep README files up-to-date 183 | 184 | ## Release Process 185 | 186 | Releases are managed by maintainers: 187 | 188 | 1. Update version in `galaxy.yml` 189 | 2. Update `CHANGELOG.md` 190 | 3. Tag release: `git tag v1.0.0` 191 | 4. Build collection: `ansible-galaxy collection build` 192 | 5. Publish to Galaxy 193 | 194 | ## Getting Help 195 | 196 | - Open an issue for bugs/features 197 | - Join our community chat 198 | - Email: support@contrastsecurity.com 199 | 200 | ## Code of Conduct 201 | 202 | ### Our Standards 203 | 204 | - Be respectful and inclusive 205 | - Welcome newcomers 206 | - Accept constructive criticism 207 | - Focus on what's best for the community 208 | 209 | ### Unacceptable Behavior 210 | 211 | - Harassment or discrimination 212 | - Trolling or insulting comments 213 | - Public or private harassment 214 | - Publishing private information 215 | 216 | ## Recognition 217 | 218 | Contributors will be recognized in: 219 | - Release notes 220 | - Contributors file 221 | - GitHub insights 222 | 223 | Thank you for contributing to the Contrast Security Ansible Collection! 224 | -------------------------------------------------------------------------------- /examples/deploy-contrast.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Example playbook for deploying Contrast Security Java agent 3 | # This playbook demonstrates various configuration options 4 | 5 | - name: Deploy Contrast Security Agent to Application Servers 6 | hosts: app_servers 7 | become: yes 8 | 9 | vars: 10 | # Store sensitive data in vault or environment variables 11 | contrast_api_credentials: 12 | url: "{{ lookup('env', 'CONTRAST_API_URL') | default('https://app.contrastsecurity.com/Contrast') }}" 13 | api_key: "{{ lookup('env', 'CONTRAST_API_KEY') }}" 14 | service_key: "{{ lookup('env', 'CONTRAST_SERVICE_KEY') }}" 15 | user_name: "{{ lookup('env', 'CONTRAST_USER_NAME') }}" 16 | 17 | pre_tasks: 18 | - name: Ensure Tomcat is installed 19 | package: 20 | name: tomcat 21 | state: present 22 | when: ansible_os_family == "RedHat" 23 | 24 | - name: Ensure Tomcat is running 25 | systemd: 26 | name: tomcat 27 | state: started 28 | enabled: yes 29 | 30 | roles: 31 | # Deploy Contrast agent 32 | - role: contrast.security.agent 33 | vars: 34 | contrast_agent_enabled: true 35 | contrast_service_name: "tomcat" 36 | contrast_agent_version: "LATEST" # Or specify version like "6.19.0" 37 | 38 | # Configuration via environment variables (default) 39 | contrast_config_source: "environment" 40 | 41 | # Main configuration 42 | contrast_security_config: 43 | api: 44 | url: "{{ contrast_api_credentials.url }}" 45 | api_key: "{{ contrast_api_credentials.api_key }}" 46 | service_key: "{{ contrast_api_credentials.service_key }}" 47 | user_name: "{{ contrast_api_credentials.user_name }}" 48 | 49 | application: 50 | name: "{{ inventory_hostname_short }}-app" 51 | tags: "env:{{ env | default('dev') }},team:{{ team | default('devops') }}" 52 | 53 | server: 54 | name: "{{ inventory_hostname }}" 55 | environment: "{{ env | default('development') }}" 56 | tags: "datacenter:{{ datacenter | default('us-east-1') }}" 57 | 58 | agent: 59 | logger: 60 | path: "/var/log/contrast/{{ inventory_hostname_short }}.log" 61 | level: "{{ contrast_log_level | default('INFO') }}" 62 | 63 | # Additional JVM arguments 64 | contrast_jvm_args: 65 | - "-Xmx{{ java_heap_size | default('1024m') }}" 66 | - "-Xms{{ java_heap_size | default('1024m') }}" 67 | # Add this if using DataDog 68 | # - "-Ddd.trace.classes.exclude=com.contrast*" 69 | 70 | post_tasks: 71 | - name: Verify Contrast agent is loaded 72 | command: "ps aux | grep -i contrast | grep -v grep" 73 | register: contrast_process 74 | changed_when: false 75 | failed_when: false 76 | 77 | - name: Display agent status 78 | debug: 79 | msg: | 80 | Contrast agent deployment complete! 81 | Service: {{ contrast_service_name }} 82 | Agent installed at: {{ contrast_agent_path }} 83 | {% if contrast_process.rc == 0 %} 84 | Agent is running in the JVM process 85 | {% else %} 86 | Agent will be loaded on next service restart 87 | {% endif %} 88 | 89 | --- 90 | # Alternative example using YAML configuration 91 | 92 | - name: Deploy Contrast with YAML Configuration 93 | hosts: app_servers 94 | become: yes 95 | 96 | roles: 97 | - role: contrast.security.agent 98 | vars: 99 | contrast_agent_enabled: true 100 | contrast_service_name: "tomcat" 101 | 102 | # Use YAML file for configuration 103 | contrast_config_source: "yaml" 104 | contrast_config_path: "/etc/contrast/{{ inventory_hostname_short }}.yaml" 105 | 106 | contrast_security_config: 107 | api: 108 | url: "{{ vault_contrast_url }}" 109 | api_key: "{{ vault_contrast_api_key }}" 110 | service_key: "{{ vault_contrast_service_key }}" 111 | user_name: "{{ vault_contrast_user_name }}" 112 | 113 | application: 114 | name: "{{ app_name }}" 115 | version: "{{ app_version | default('1.0.0') }}" 116 | tags: "{{ app_tags | default('') }}" 117 | 118 | assess: 119 | enable: "{{ assess_enabled | default(false) }}" 120 | tags: "{{ assess_tags | default('') }}" 121 | 122 | protect: 123 | enable: "{{ protect_enabled | default(true) }}" 124 | rules: 125 | disabled_rules: "{{ disabled_rules | default([]) }}" 126 | 127 | --- 128 | # Example: Rolling deployment with verification 129 | 130 | - name: Rolling Contrast Agent Deployment 131 | hosts: app_servers 132 | become: yes 133 | serial: "30%" # Deploy to 30% of servers at a time 134 | 135 | roles: 136 | - role: contrast.security.agent 137 | vars: 138 | contrast_agent_enabled: true 139 | contrast_service_name: "{{ app_service_name | default('tomcat') }}" 140 | contrast_security_config: 141 | api: "{{ contrast_api_config }}" 142 | application: 143 | name: "{{ app_name }}" 144 | tags: "deployment:rolling,date:{{ ansible_date_time.date }}" 145 | 146 | post_tasks: 147 | - name: Wait for application to be healthy 148 | uri: 149 | url: "http://{{ ansible_default_ipv4.address }}:8080/health" 150 | status_code: 200 151 | register: result 152 | until: result.status == 200 153 | retries: 30 154 | delay: 10 155 | 156 | - name: Verify Contrast agent is reporting 157 | pause: 158 | prompt: "Verify agent is reporting in Contrast UI, then press Enter to continue" 159 | when: ansible_play_batch.index(inventory_hostname) == 0 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contrast Security Ansible Collection 2 | 3 | Official Ansible Collection for deploying and managing Contrast Security agents. 4 | 5 | ## Description 6 | 7 | This collection provides enterprise-grade automation for deploying the Contrast Security Java agent across your infrastructure. It supports automated installation, configuration, and lifecycle management with a focus on idempotency and operational safety. 8 | 9 | ## Included Content 10 | 11 | ### Roles 12 | 13 | - **contrast.security.agent** - Deploy and manage the Contrast Security Java agent 14 | 15 | ## Installation 16 | 17 | ### From Ansible Galaxy (TODO: Update when published) 18 | 19 | ```bash 20 | ansible-galaxy collection install contrast.security 21 | ``` 22 | 23 | ### From Source 24 | 25 | ```bash 26 | git clone https://github.com/Contrast-Security-OSS/ansible-collection-contrast.git 27 | cd ansible-collection-contrast 28 | ansible-galaxy collection build 29 | ansible-galaxy collection install contrast-security-*.tar.gz 30 | ``` 31 | 32 | ## Quick Start 33 | 34 | 1. Install the collection: 35 | ```bash 36 | ansible-galaxy collection install contrast.security 37 | ``` 38 | 39 | 2. Create a playbook: 40 | ```yaml 41 | --- 42 | - name: Deploy Contrast Security 43 | hosts: app_servers 44 | become: yes 45 | 46 | roles: 47 | - role: contrast.security.agent 48 | vars: 49 | contrast_agent_enabled: true 50 | contrast_service_name: "tomcat" 51 | contrast_security_config: 52 | api: 53 | url: "https://app.contrastsecurity.com/Contrast" 54 | api_key: "{{ vault_contrast_api_key }}" 55 | service_key: "{{ vault_contrast_service_key }}" 56 | user_name: "{{ vault_contrast_user_name }}" 57 | application: 58 | name: "MyApp" 59 | ``` 60 | 61 | 3. Run the playbook: 62 | ```bash 63 | ansible-playbook -i inventory contrast-deploy.yml 64 | ``` 65 | 66 | ## Requirements 67 | 68 | - Ansible 2.9+ 69 | - Python 3.6+ 70 | - Root or sudo access on target hosts 71 | 72 | ## Supported Platforms 73 | 74 | - Red Hat Enterprise Linux 7, 8, 9 75 | - CentOS 7, 8 76 | - Amazon Linux 2, 2023 77 | - Ubuntu 18.04, 20.04, 22.04 78 | - Debian 10, 11, 12 79 | 80 | ## Features 81 | 82 | - **Idempotent Operations**: Safe to run multiple times 83 | - **Flexible Configuration**: Support for environment variables and YAML files 84 | - **Multiple Activation Methods**: systemd (recommended) and setenv.sh 85 | - **Clean Deactivation**: Easy agent removal without residual configuration 86 | - **Version Management**: Support for specific versions or latest 87 | - **Enterprise Ready**: Support for air-gapped environments and custom repositories 88 | 89 | ## Documentation 90 | 91 | For detailed documentation on the agent role, see: 92 | - [Agent Role Documentation](roles/agent/README.md) 93 | - [Contrast Documentation](https://docs.contrastsecurity.com) 94 | 95 | ## Examples 96 | 97 | ### Deploy to Multiple Environments 98 | 99 | ```yaml 100 | --- 101 | # group_vars/production.yml 102 | contrast_agent_enabled: true 103 | contrast_security_config: 104 | server: 105 | environment: "production" 106 | assess: 107 | enable: false 108 | protect: 109 | enable: true 110 | 111 | # group_vars/staging.yml 112 | contrast_agent_enabled: true 113 | contrast_security_config: 114 | server: 115 | environment: "qa" 116 | assess: 117 | enable: true 118 | protect: 119 | enable: false 120 | ``` 121 | 122 | ### Using with Ansible Vault 123 | 124 | ```yaml 125 | --- 126 | - name: Secure Contrast Deployment 127 | hosts: app_servers 128 | become: yes 129 | 130 | vars_files: 131 | - vault/contrast_credentials.yml 132 | 133 | roles: 134 | - role: contrast.security.agent 135 | vars: 136 | contrast_agent_enabled: true 137 | contrast_security_config: 138 | api: 139 | url: "{{ vault_contrast_url }}" 140 | api_key: "{{ vault_contrast_api_key }}" 141 | service_key: "{{ vault_contrast_service_key }}" 142 | user_name: "{{ vault_contrast_user_name }}" 143 | ``` 144 | 145 | ## Testing 146 | 147 | This collection includes comprehensive testing using Molecule: 148 | 149 | ### Standard Testing (x86_64) 150 | 151 | For most users with Intel/AMD systems: 152 | 153 | ```bash 154 | cd roles/agent 155 | molecule test 156 | ``` 157 | 158 | ### Apple Silicon (M1/M2/M3) Mac Testing 159 | 160 | Apple Silicon users have **two testing options**: 161 | 162 | #### Option 1: Quick Development Testing 163 | Fast Docker-based testing for quick validation: 164 | 165 | ```bash 166 | # Quick test with specific OS 167 | ./test-docker.sh ubuntu2204 # Ubuntu 22.04 (default) 168 | ./test-docker.sh ubuntu2404 # Ubuntu 24.04 LTS 169 | ./test-docker.sh rocky9 # Rocky Linux 9 170 | 171 | # All supported targets: 172 | ./test-docker.sh ubuntu2004 # Ubuntu 20.04 LTS 173 | ./test-docker.sh centos9 # CentOS Stream 9 174 | ``` 175 | 176 | #### Option 2: Full Molecule Testing 177 | Comprehensive ARM64 testing with Molecule: 178 | 179 | ```bash 180 | cd roles/agent 181 | molecule test -s apple-silicon 182 | ``` 183 | 184 | ### Why Two Approaches? 185 | 186 | - **`test-docker.sh`**: Fast feedback for developers (~2 minutes) 187 | - **`molecule test`**: Comprehensive testing with advanced scenarios (~10 minutes) 188 | 189 | For full Apple Silicon setup instructions, see [APPLE_SILICON_SETUP.md](APPLE_SILICON_SETUP.md). 190 | 191 | ## Contributing 192 | 193 | We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. 194 | 195 | ### Development Setup 196 | 197 | 1. Fork the repository 198 | 2. Create a feature branch 199 | 3. Make your changes 200 | 4. Add tests if applicable 201 | 5. Submit a pull request 202 | 203 | ## Support 204 | 205 | - Documentation: https://docs.contrastsecurity.com 206 | - Support Portal: https://support.contrastsecurity.com 207 | - Email: support@contrastsecurity.com 208 | 209 | ## License 210 | 211 | MIT - See [LICENSE](LICENSE) file for details. 212 | 213 | ## Changelog 214 | 215 | See [CHANGELOG.md](CHANGELOG.md) for version history. 216 | 217 | ## Roadmap 218 | 219 | - [ ] Support for .NET Core agent 220 | - [ ] Support for Node.js agent 221 | - [ ] Support for Python agent 222 | - [ ] Kubernetes operator integration 223 | - [ ] Enhanced monitoring and metrics collection 224 | 225 | ## Author 226 | 227 | **Contrast Security** 228 | - Website: https://www.contrastsecurity.com 229 | - GitHub: https://github.com/Contrast-Security-OSS 230 | -------------------------------------------------------------------------------- /roles/agent/molecule/vagrant/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Prepare test environment 3 | hosts: all 4 | become: true 5 | gather_facts: true 6 | 7 | vars: 8 | # Default service names per OS family 9 | default_service_names: 10 | RedHat: tomcat 11 | Debian: tomcat9 12 | 13 | # Default Java packages per OS family 14 | default_java_packages: 15 | RedHat: java-11-openjdk-devel 16 | Debian: openjdk-11-jdk 17 | 18 | pre_tasks: 19 | - name: Set service name based on OS family 20 | set_fact: 21 | contrast_service_name: "{{ tomcat_package | default(default_service_names[ansible_os_family]) }}" 22 | when: contrast_service_name is not defined 23 | 24 | - name: Set Java package based on OS family 25 | set_fact: 26 | java_package: "{{ java_package | default(default_java_packages[ansible_os_family]) }}" 27 | when: java_package is not defined 28 | 29 | tasks: 30 | - name: Update package cache (Debian) 31 | apt: 32 | update_cache: yes 33 | cache_valid_time: 3600 34 | when: ansible_os_family == "Debian" 35 | retries: 3 36 | delay: 10 37 | 38 | - name: Update package cache (RedHat) 39 | yum: 40 | update_cache: yes 41 | when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int < 8 42 | retries: 3 43 | delay: 10 44 | 45 | - name: Update package cache (RHEL 8+) 46 | dnf: 47 | update_cache: yes 48 | when: ansible_os_family == "RedHat" and ansible_distribution_major_version|int >= 8 49 | retries: 3 50 | delay: 10 51 | 52 | - name: Install base packages 53 | package: 54 | name: 55 | - curl 56 | - wget 57 | - unzip 58 | - tar 59 | - python3 60 | state: present 61 | 62 | - name: Install Java 63 | package: 64 | name: "{{ java_package }}" 65 | state: present 66 | 67 | - name: Install Tomcat (Debian) 68 | package: 69 | name: "{{ contrast_service_name }}" 70 | state: present 71 | when: ansible_os_family == "Debian" 72 | 73 | - name: Install Tomcat (RedHat) 74 | package: 75 | name: tomcat 76 | state: present 77 | when: ansible_os_family == "RedHat" 78 | 79 | - name: Create Tomcat directories 80 | file: 81 | path: "{{ item }}" 82 | state: directory 83 | owner: tomcat 84 | group: tomcat 85 | mode: '0755' 86 | loop: 87 | - /opt/tomcat 88 | - /opt/tomcat/bin 89 | - /opt/tomcat/conf 90 | - /opt/tomcat/logs 91 | - /var/log/contrast 92 | ignore_errors: true # Some directories might already exist 93 | 94 | - name: Create setenv.sh script 95 | copy: 96 | dest: /opt/tomcat/bin/setenv.sh 97 | content: | 98 | #!/bin/bash 99 | # Tomcat environment configuration 100 | export JAVA_OPTS="" 101 | export CATALINA_OPTS="" 102 | mode: '0755' 103 | owner: tomcat 104 | group: tomcat 105 | force: no 106 | 107 | - name: Create basic server.xml for Tomcat 108 | copy: 109 | dest: /opt/tomcat/conf/server.xml 110 | content: | 111 | 112 | 113 | 114 | 117 | 118 | 120 | 121 | 122 | 123 | 124 | mode: '0644' 125 | owner: tomcat 126 | group: tomcat 127 | force: no 128 | 129 | - name: Ensure systemd directory exists 130 | file: 131 | path: /etc/systemd/system 132 | state: directory 133 | mode: '0755' 134 | 135 | - name: Create or verify Tomcat systemd service file 136 | copy: 137 | dest: "/etc/systemd/system/{{ contrast_service_name }}.service" 138 | content: | 139 | [Unit] 140 | Description=Apache Tomcat Web Application Container 141 | After=network.target 142 | 143 | [Service] 144 | Type=forking 145 | User=tomcat 146 | Group=tomcat 147 | Environment="JAVA_HOME=/usr/lib/jvm/java-11-openjdk" 148 | Environment="CATALINA_HOME=/opt/tomcat" 149 | Environment="CATALINA_BASE=/opt/tomcat" 150 | ExecStart=/opt/tomcat/bin/startup.sh 151 | ExecStop=/opt/tomcat/bin/shutdown.sh 152 | Restart=on-failure 153 | 154 | [Install] 155 | WantedBy=multi-user.target 156 | mode: '0644' 157 | force: no 158 | 159 | - name: Create startup/shutdown scripts 160 | copy: 161 | dest: "/opt/tomcat/bin/{{ item }}" 162 | content: | 163 | #!/bin/bash 164 | # Mock {{ item }} script for testing 165 | echo "{{ item }} executed" 166 | exit 0 167 | mode: '0755' 168 | owner: tomcat 169 | group: tomcat 170 | force: no 171 | loop: 172 | - startup.sh 173 | - shutdown.sh 174 | 175 | - name: Reload systemd daemon 176 | systemd: 177 | daemon_reload: yes 178 | 179 | - name: Enable Tomcat service 180 | systemd: 181 | name: "{{ contrast_service_name }}" 182 | enabled: yes 183 | state: stopped # Keep stopped for testing 184 | 185 | - name: Verify Java installation 186 | command: java -version 187 | register: java_version 188 | changed_when: false 189 | 190 | - name: Display Java version 191 | debug: 192 | var: java_version.stderr_lines 193 | 194 | - name: Verify Tomcat service status 195 | systemd: 196 | name: "{{ contrast_service_name }}" 197 | register: tomcat_status 198 | 199 | - name: Display environment info 200 | debug: 201 | msg: 202 | - "OS Family: {{ ansible_os_family }}" 203 | - "Distribution: {{ ansible_distribution }} {{ ansible_distribution_version }}" 204 | - "Service Name: {{ contrast_service_name }}" 205 | - "Java Package: {{ java_package }}" 206 | - "Tomcat Status: {{ tomcat_status.status.LoadState }}" 207 | -------------------------------------------------------------------------------- /roles/agent/molecule/default/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | become: true 5 | gather_facts: true 6 | 7 | tasks: 8 | - name: Check if Contrast agent JAR exists 9 | stat: 10 | path: /opt/contrast/contrast.jar 11 | register: agent_jar 12 | 13 | - name: Assert agent JAR was downloaded 14 | assert: 15 | that: 16 | - agent_jar.stat.exists 17 | - agent_jar.stat.isreg 18 | fail_msg: "Contrast agent JAR not found at expected location" 19 | success_msg: "Contrast agent JAR successfully downloaded" 20 | 21 | - name: Check systemd override file 22 | stat: 23 | path: /etc/systemd/system/tomcat.service.d/10-contrast.conf 24 | register: override_file 25 | when: contrast_config_method | default('setenv') == 'systemd' 26 | 27 | - name: Assert override file exists 28 | assert: 29 | that: 30 | - override_file.stat.exists 31 | - override_file.stat.isreg 32 | fail_msg: "Systemd override file not found" 33 | success_msg: "Systemd override file created successfully" 34 | when: contrast_config_method | default('setenv') == 'systemd' 35 | 36 | - name: Read override file content 37 | slurp: 38 | src: /etc/systemd/system/tomcat.service.d/10-contrast.conf 39 | register: override_content 40 | when: contrast_config_method | default('setenv') == 'systemd' 41 | 42 | - name: Decode and check override content 43 | set_fact: 44 | override_text: "{{ override_content.content | b64decode }}" 45 | when: contrast_config_method | default('setenv') == 'systemd' 46 | 47 | - name: Assert override contains javaagent 48 | assert: 49 | that: 50 | - "'-javaagent:/opt/contrast/contrast.jar' in override_text" 51 | - "'JAVA_TOOL_OPTIONS' in override_text" 52 | fail_msg: "Override file doesn't contain expected javaagent configuration" 53 | success_msg: "Override file correctly configured" 54 | when: contrast_config_method | default('setenv') == 'systemd' 55 | 56 | - name: Check setenv.sh file 57 | stat: 58 | path: /opt/tomcat/bin/setenv.sh 59 | register: setenv_file 60 | when: contrast_config_method | default('setenv') == 'setenv' 61 | 62 | - name: Assert setenv.sh file exists 63 | assert: 64 | that: 65 | - setenv_file.stat.exists 66 | - setenv_file.stat.isreg 67 | fail_msg: "setenv.sh file not found" 68 | success_msg: "setenv.sh file exists" 69 | when: contrast_config_method | default('setenv') == 'setenv' 70 | 71 | - name: Read setenv.sh content 72 | slurp: 73 | src: /opt/tomcat/bin/setenv.sh 74 | register: setenv_content 75 | when: contrast_config_method | default('setenv') == 'setenv' 76 | 77 | - name: Decode and check setenv.sh content 78 | set_fact: 79 | setenv_text: "{{ setenv_content.content | b64decode }}" 80 | when: contrast_config_method | default('setenv') == 'setenv' 81 | 82 | - name: Assert setenv.sh contains javaagent 83 | assert: 84 | that: 85 | - "'-javaagent:/opt/contrast/contrast.jar' in setenv_text" 86 | fail_msg: "setenv.sh doesn't contain expected javaagent configuration" 87 | success_msg: "setenv.sh correctly configured" 88 | when: contrast_config_method | default('setenv') == 'setenv' 89 | 90 | - name: Check if service is running (skip for container testing) 91 | debug: 92 | msg: "Skipping service check for container testing" 93 | 94 | - name: Test idempotency - run role again 95 | include_role: 96 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 97 | vars: 98 | contrast_agent_enabled: true 99 | contrast_service_name: "tomcat" 100 | contrast_config_method: "setenv" 101 | contrast_test_mode: true 102 | contrast_agent_version: "6.19.0" 103 | contrast_security_config: 104 | api: 105 | url: "https://app.contrastsecurity.com/Contrast" 106 | api_key: "test-api-key" 107 | service_key: "test-service-key" 108 | user_name: "test-user" 109 | register: second_run 110 | 111 | - name: Assert role is idempotent 112 | assert: 113 | that: 114 | - not second_run.changed 115 | fail_msg: "Role is not idempotent" 116 | success_msg: "Role is idempotent" 117 | 118 | - name: Test deactivation 119 | include_role: 120 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 121 | vars: 122 | contrast_agent_enabled: false 123 | contrast_service_name: "tomcat" 124 | contrast_config_method: "setenv" 125 | contrast_test_mode: true 126 | 127 | - name: Check setenv.sh file after deactivation 128 | stat: 129 | path: /opt/tomcat/bin/setenv.sh 130 | register: setenv_after_deactivation 131 | when: contrast_config_method | default('setenv') == 'setenv' 132 | 133 | - name: Read setenv.sh content after deactivation 134 | slurp: 135 | src: /opt/tomcat/bin/setenv.sh 136 | register: setenv_content_after 137 | when: contrast_config_method | default('setenv') == 'setenv' and setenv_after_deactivation.stat.exists 138 | 139 | - name: Decode setenv.sh content after deactivation 140 | set_fact: 141 | setenv_text_after: "{{ setenv_content_after.content | b64decode }}" 142 | when: contrast_config_method | default('setenv') == 'setenv' and setenv_after_deactivation.stat.exists 143 | 144 | - name: Assert javaagent was removed from setenv.sh 145 | assert: 146 | that: 147 | - "'-javaagent:/opt/contrast/contrast.jar' not in setenv_text_after" 148 | fail_msg: "javaagent configuration not removed from setenv.sh" 149 | success_msg: "javaagent configuration successfully removed" 150 | when: contrast_config_method | default('setenv') == 'setenv' and setenv_after_deactivation.stat.exists 151 | 152 | - name: Check override file removed 153 | stat: 154 | path: /etc/systemd/system/tomcat.service.d/10-contrast.conf 155 | register: override_removed 156 | when: contrast_config_method | default('setenv') == 'systemd' 157 | 158 | - name: Assert override was removed 159 | assert: 160 | that: 161 | - not override_removed.stat.exists 162 | fail_msg: "Override file was not removed during deactivation" 163 | success_msg: "Override file successfully removed" 164 | when: contrast_config_method | default('setenv') == 'systemd' 165 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # ARM64/Apple Silicon compatible Vagrantfile for Contrast Security Ansible Collection 5 | # Uses ARM64-native boxes for better performance and compatibility 6 | 7 | VAGRANTFILE_API_VERSION = "2" 8 | 9 | # ARM64-compatible test scenarios 10 | TEST_SCENARIOS = { 11 | # Ubuntu ARM64 scenarios 12 | "ubuntu2004" => { 13 | box: "bento/ubuntu-20.04-arm64", 14 | memory: 1024, 15 | cpus: 1, 16 | java_package: "openjdk-11-jdk", 17 | service_name: "tomcat9" 18 | }, 19 | "ubuntu2204" => { 20 | box: "bento/ubuntu-22.04-arm64", 21 | memory: 1024, 22 | cpus: 1, 23 | java_package: "openjdk-11-jdk", 24 | service_name: "tomcat9" 25 | }, 26 | "ubuntu2404" => { 27 | box: "bento/ubuntu-24.04-arm64", 28 | memory: 1024, 29 | cpus: 1, 30 | java_package: "openjdk-11-jdk", 31 | service_name: "tomcat10" 32 | }, 33 | 34 | # CentOS/RHEL ARM64 scenarios 35 | "centos9" => { 36 | box: "bento/centos-stream-9-arm64", 37 | memory: 1024, 38 | cpus: 1, 39 | java_package: "java-11-openjdk-devel", 40 | service_name: "tomcat" 41 | }, 42 | "rocky9" => { 43 | box: "bento/rockylinux-9-arm64", 44 | memory: 1024, 45 | cpus: 1, 46 | java_package: "java-11-openjdk-devel", 47 | service_name: "tomcat" 48 | }, 49 | 50 | # Amazon Linux ARM64 scenarios 51 | "amazonlinux2" => { 52 | box: "bento/amazonlinux-2-arm64", 53 | memory: 1024, 54 | cpus: 1, 55 | java_package: "java-11-amazon-corretto-devel", 56 | service_name: "tomcat" 57 | } 58 | } 59 | 60 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 61 | # Global settings optimized for Apple Silicon 62 | config.vm.boot_timeout = 600 63 | config.vm.graceful_halt_timeout = 60 64 | config.vm.box_check_update = false 65 | 66 | # VMware provider (recommended for Apple Silicon) 67 | config.vm.provider "vmware_desktop" do |vmware| 68 | vmware.gui = false 69 | vmware.memory = 1024 70 | vmware.cpus = 1 71 | vmware.ssh_info_public = true 72 | vmware.vmx["ethernet0.virtualdev"] = "vmxnet3" 73 | end 74 | 75 | # Parallels provider (excellent Apple Silicon support) 76 | config.vm.provider "parallels" do |prl| 77 | prl.memory = 1024 78 | prl.cpus = 1 79 | prl.update_guest_tools = true 80 | prl.check_guest_tools = false 81 | end 82 | 83 | # VirtualBox provider (limited ARM64 support) 84 | config.vm.provider "virtualbox" do |vb| 85 | vb.gui = false 86 | vb.memory = 1024 87 | vb.cpus = 1 88 | 89 | # ARM64-specific VirtualBox settings 90 | vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 91 | vb.customize ["modifyvm", :id, "--natdnsproxy1", "on"] 92 | vb.customize ["modifyvm", :id, "--audio", "none"] 93 | vb.customize ["modifyvm", :id, "--uart1", "off"] 94 | vb.customize ["modifyvm", :id, "--uart2", "off"] 95 | end 96 | 97 | # Disable default synced folder 98 | config.vm.synced_folder ".", "/vagrant", disabled: true 99 | config.ssh.forward_agent = true 100 | 101 | TEST_SCENARIOS.each do |name, settings| 102 | config.vm.define name do |vm| 103 | vm.vm.box = settings[:box] 104 | vm.vm.hostname = "#{name}-contrast-test" 105 | 106 | # Network configuration 107 | vm.vm.network "private_network", type: "dhcp" 108 | 109 | # Sync the collection for testing 110 | vm.vm.synced_folder ".", "/home/vagrant/contrast.security", 111 | type: "rsync", 112 | rsync__exclude: [".git/", "*.tar.gz", ".molecule/"] 113 | 114 | # ARM64-optimized bootstrap script 115 | vm.vm.provision "shell", inline: <<-SHELL 116 | set -e 117 | 118 | echo "=== Bootstrapping #{name} (ARM64) for Contrast testing ===" 119 | 120 | # Update package manager 121 | if command -v apt-get >/dev/null 2>&1; then 122 | export DEBIAN_FRONTEND=noninteractive 123 | apt-get update 124 | apt-get install -y python3 python3-pip curl wget unzip 125 | 126 | # Install Java and Tomcat 127 | apt-get install -y #{settings[:java_package]} 128 | 129 | # Install Tomcat based on Ubuntu version 130 | if [[ "#{name}" == *"2404"* ]]; then 131 | apt-get install -y tomcat10 tomcat10-admin 132 | systemctl enable tomcat10 133 | else 134 | apt-get install -y tomcat9 tomcat9-admin 135 | systemctl enable tomcat9 136 | fi 137 | 138 | elif command -v yum >/dev/null 2>&1; then 139 | yum update -y 140 | yum install -y python3 python3-pip curl wget unzip 141 | yum install -y #{settings[:java_package]} 142 | yum install -y tomcat tomcat-webapps tomcat-admin-webapps 143 | systemctl enable tomcat 144 | 145 | elif command -v dnf >/dev/null 2>&1; then 146 | dnf update -y 147 | dnf install -y python3 python3-pip curl wget unzip 148 | dnf install -y #{settings[:java_package]} 149 | dnf install -y tomcat tomcat-webapps tomcat-admin-webapps 150 | systemctl enable tomcat 151 | fi 152 | 153 | # Install Ansible 154 | python3 -m pip install --user ansible 155 | 156 | # Create test directories 157 | mkdir -p /opt/tomcat/bin 158 | mkdir -p /var/log/contrast 159 | 160 | # Create basic setenv.sh 161 | if [[ ! -f /opt/tomcat/bin/setenv.sh ]]; then 162 | cat > /opt/tomcat/bin/setenv.sh << 'SETENV_EOF' 163 | #!/bin/bash 164 | # Tomcat environment configuration 165 | export JAVA_OPTS="" 166 | export CATALINA_OPTS="" 167 | SETENV_EOF 168 | chmod +x /opt/tomcat/bin/setenv.sh 169 | fi 170 | 171 | # Fix permissions 172 | chown -R tomcat:tomcat /opt/tomcat/ 2>/dev/null || true 173 | chown -R tomcat:tomcat /var/log/contrast/ 2>/dev/null || true 174 | 175 | echo "=== Bootstrap complete for #{name} (ARM64) ===" 176 | SHELL 177 | 178 | # Ansible provisioning 179 | vm.vm.provision "ansible_local" do |ansible| 180 | ansible.playbook = "/home/vagrant/contrast.security/tests/vagrant/setup.yml" 181 | ansible.inventory_path = "/home/vagrant/contrast.security/tests/vagrant/inventory" 182 | ansible.limit = "all" 183 | ansible.extra_vars = { 184 | service_name: settings[:service_name], 185 | java_package: settings[:java_package], 186 | test_scenario: name 187 | } 188 | end 189 | end 190 | end 191 | 192 | # Test runner VM 193 | config.vm.define "test-runner", primary: true do |runner| 194 | runner.vm.box = "bento/ubuntu-22.04-arm64" 195 | runner.vm.hostname = "contrast-test-runner" 196 | 197 | runner.vm.provider "vmware_desktop" do |vmware| 198 | vmware.memory = 512 199 | vmware.cpus = 1 200 | end 201 | 202 | runner.vm.provider "parallels" do |prl| 203 | prl.memory = 512 204 | prl.cpus = 1 205 | end 206 | 207 | runner.vm.provider "virtualbox" do |vb| 208 | vb.name = "contrast-test-runner" 209 | vb.memory = 512 210 | vb.cpus = 1 211 | end 212 | 213 | runner.vm.synced_folder ".", "/home/vagrant/contrast.security", 214 | type: "rsync", 215 | rsync__exclude: [".git/", "*.tar.gz", ".molecule/"] 216 | 217 | runner.vm.provision "shell", inline: <<-SHELL 218 | export DEBIAN_FRONTEND=noninteractive 219 | apt-get update 220 | apt-get install -y python3 python3-pip ansible 221 | python3 -m pip install --user molecule pytest testinfra 222 | echo "ARM64 test runner ready for executing test suites" 223 | SHELL 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /roles/agent/README.md: -------------------------------------------------------------------------------- 1 | # Contrast Security Agent Ansible Role 2 | 3 | This is the official Ansible role for deploying and managing the Contrast Security Java agent on Linux systems. The role provides automated installation, configuration, and lifecycle management of the Contrast agent for Java applications. 4 | 5 | ## Requirements 6 | 7 | - Ansible 2.9 or higher 8 | - A Java application running on a supported application server (e.g., Tomcat) 9 | - Valid Contrast Security credentials (API key, service key, user name) 10 | - Root or sudo access on target hosts 11 | 12 | ## Role Variables 13 | 14 | ### Core Variables 15 | 16 | | Variable | Type | Default | Description | 17 | |----------|------|---------|-------------| 18 | | `contrast_agent_enabled` | bool | `false` | Master toggle to enable/disable agent activation | 19 | | `contrast_service_name` | string | `tomcat` | Name of the systemd service to inject the agent into | 20 | | `contrast_agent_version` | string | `LATEST` | Version of agent to download (e.g., '6.19.0' or 'LATEST') | 21 | | `contrast_agent_path` | string | `/opt/contrast/contrast.jar` | Path where the agent JAR will be placed | 22 | | `contrast_config_method` | string | `systemd` | Method for activation: 'systemd' or 'setenv' | 23 | | `contrast_config_source` | string | `environment` | Configuration source: 'environment' or 'yaml' | 24 | 25 | ### Configuration Variables 26 | 27 | The `contrast_security_config` dictionary maps directly to the Contrast YAML configuration structure: 28 | 29 | ```yaml 30 | contrast_security_config: 31 | api: 32 | url: "https://app.contrastsecurity.com/Contrast" 33 | api_key: "your-api-key" 34 | service_key: "your-service-key" 35 | user_name: "your-user-name" 36 | application: 37 | name: "MyApplication" 38 | tags: "env:production,team:backend" 39 | server: 40 | name: "prod-server-01" 41 | environment: "production" 42 | agent: 43 | logger: 44 | path: "/var/log/contrast/contrast.log" 45 | level: "INFO" 46 | ``` 47 | 48 | ### JVM Arguments 49 | 50 | Additional JVM arguments can be specified using: 51 | 52 | ```yaml 53 | contrast_jvm_args: 54 | - "-Xmx1280m" 55 | - "-Ddd.trace.classes.exclude=com.contrast*" # For DataDog compatibility 56 | ``` 57 | 58 | ## Installation 59 | 60 | ### Using Ansible Galaxy 61 | 62 | ```bash 63 | ansible-galaxy collection install contrast.security 64 | ``` 65 | 66 | ### From Source 67 | 68 | ```bash 69 | git clone https://github.com/Contrast-Security-OSS/ansible-collection-contrast.git 70 | cd ansible-collection-contrast 71 | ansible-galaxy collection build 72 | ansible-galaxy collection install contrast-security-*.tar.gz 73 | ``` 74 | 75 | ## Usage Examples 76 | 77 | ### Basic Usage 78 | 79 | ```yaml 80 | --- 81 | - name: Deploy Contrast agent 82 | hosts: app_servers 83 | become: yes 84 | 85 | roles: 86 | - role: contrast.security.agent 87 | vars: 88 | contrast_agent_enabled: true 89 | contrast_service_name: "tomcat" 90 | contrast_security_config: 91 | api: 92 | url: "https://app.contrastsecurity.com/Contrast" 93 | api_key: "{{ vault_contrast_api_key }}" 94 | service_key: "{{ vault_contrast_service_key }}" 95 | user_name: "{{ vault_contrast_user_name }}" 96 | application: 97 | name: "WebApp-Production" 98 | tags: "env:prod,version:2.1.0" 99 | ``` 100 | 101 | ### Using YAML Configuration 102 | 103 | ```yaml 104 | --- 105 | - name: Deploy Contrast agent with YAML config 106 | hosts: app_servers 107 | become: yes 108 | 109 | roles: 110 | - role: contrast.security.agent 111 | vars: 112 | contrast_agent_enabled: true 113 | contrast_config_source: "yaml" 114 | contrast_security_config: 115 | api: 116 | url: "https://app.contrastsecurity.com/Contrast" 117 | api_key: "{{ vault_contrast_api_key }}" 118 | service_key: "{{ vault_contrast_service_key }}" 119 | user_name: "{{ vault_contrast_user_name }}" 120 | application: 121 | name: "{{ app_name }}" 122 | assess: 123 | enable: false 124 | protect: 125 | enable: true 126 | ``` 127 | 128 | ### With DataDog Compatibility 129 | 130 | ```yaml 131 | --- 132 | - name: Deploy Contrast with DataDog 133 | hosts: app_servers 134 | become: yes 135 | 136 | roles: 137 | - role: contrast.security.agent 138 | vars: 139 | contrast_agent_enabled: true 140 | contrast_jvm_args: 141 | - "-Ddd.trace.classes.exclude=com.contrast*" 142 | - "-Xmx1536m" # Increased heap for both agents 143 | contrast_security_config: 144 | api: 145 | url: "{{ contrast_url }}" 146 | api_key: "{{ contrast_api_key }}" 147 | service_key: "{{ contrast_service_key }}" 148 | user_name: "{{ contrast_user_name }}" 149 | ``` 150 | 151 | ### Deactivating the Agent 152 | 153 | To deactivate the agent, simply set `contrast_agent_enabled` to `false` and re-run the playbook: 154 | 155 | ```yaml 156 | --- 157 | - name: Deactivate Contrast agent 158 | hosts: app_servers 159 | become: yes 160 | 161 | roles: 162 | - role: contrast.security.agent 163 | vars: 164 | contrast_agent_enabled: false 165 | contrast_service_name: "tomcat" 166 | ``` 167 | 168 | ## Configuration Methods 169 | 170 | ### systemd Method (Recommended) 171 | 172 | The `systemd` method creates a service override file that doesn't modify vendor-supplied files: 173 | - Creates `/etc/systemd/system/.service.d/10-contrast.conf` 174 | - Injects `JAVA_TOOL_OPTIONS` environment variable 175 | - Clean and non-intrusive 176 | - Survives package updates 177 | 178 | ### setenv Method 179 | 180 | The `setenv` method modifies the application's startup script: 181 | - Adds configuration to `/opt//bin/setenv.sh` 182 | - Uses `CATALINA_OPTS` for Tomcat 183 | - Direct but potentially conflicting with other tools 184 | 185 | ## Environment Variables 186 | 187 | When using `contrast_config_source: environment`, the role converts the configuration dictionary to environment variables: 188 | 189 | - `api.url` → `CONTRAST__API__URL` 190 | - `application.name` → `CONTRAST__APPLICATION__NAME` 191 | - `agent.logger.level` → `CONTRAST__AGENT__LOGGER__LEVEL` 192 | 193 | ## Resource Management 194 | 195 | ### Memory Recommendations 196 | 197 | When running multiple agents (e.g., Contrast + DataDog): 198 | - Increase JVM heap by ~256MB minimum 199 | - Monitor application performance after deployment 200 | - Start with ADR/Protect mode (lower memory usage) 201 | 202 | ### Agent Ordering 203 | 204 | When using with DataDog, ensure Contrast is loaded first: 205 | ```yaml 206 | contrast_jvm_args: 207 | - "-Ddd.trace.classes.exclude=com.contrast*" 208 | ``` 209 | 210 | ## Troubleshooting 211 | 212 | ### Verify Agent Installation 213 | 214 | Check if the agent JAR exists: 215 | ```bash 216 | ls -la /opt/contrast/contrast.jar 217 | ``` 218 | 219 | ### Check Service Configuration 220 | 221 | For systemd method: 222 | ```bash 223 | cat /etc/systemd/system/tomcat.service.d/10-contrast.conf 224 | systemctl status tomcat 225 | ``` 226 | 227 | For setenv method: 228 | ```bash 229 | grep "Contrast Security" /opt/tomcat/bin/setenv.sh 230 | ``` 231 | 232 | ### View Agent Logs 233 | 234 | Default log location: 235 | ```bash 236 | tail -f /var/log/contrast/contrast.log 237 | ``` 238 | 239 | ### Common Issues 240 | 241 | 1. **Service fails to start**: Check JVM arguments syntax and memory settings 242 | 2. **Agent not loading**: Verify file permissions and service configuration 243 | 3. **Configuration not applied**: Ensure service was restarted after changes 244 | 245 | ## License 246 | 247 | MIT 248 | 249 | ## Author Information 250 | 251 | This role is maintained by Contrast Security. 252 | - Website: https://www.contrastsecurity.com 253 | - Support: https://support.contrastsecurity.com 254 | - Documentation: https://docs.contrastsecurity.com 255 | 256 | ## Contributing 257 | 258 | Please submit issues and pull requests to: 259 | https://github.com/Contrast-Security-OSS/ansible-collection-contrast 260 | -------------------------------------------------------------------------------- /roles/agent/molecule/vagrant/side_effect.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Side Effect Tests 3 | hosts: all 4 | become: true 5 | gather_facts: true 6 | 7 | vars: 8 | expected_service_name: "{{ tomcat_package | default('tomcat9' if ansible_os_family == 'Debian' else 'tomcat') }}" 9 | 10 | tasks: 11 | - name: Create pre-existing systemd override for testing 12 | file: 13 | path: "/etc/systemd/system/{{ expected_service_name }}.service.d" 14 | state: directory 15 | mode: '0755' 16 | 17 | - name: Create pre-existing override file with custom configuration 18 | copy: 19 | dest: "/etc/systemd/system/{{ expected_service_name }}.service.d/99-custom.conf" 20 | content: | 21 | # Custom configuration that should be preserved 22 | [Service] 23 | Environment="CUSTOM_VAR=custom_value" 24 | Environment="JAVA_OPTS=-Xmx512m" 25 | mode: '0644' 26 | 27 | - name: Create pre-existing setenv.sh with custom content 28 | copy: 29 | dest: /opt/tomcat/bin/setenv.sh 30 | content: | 31 | #!/bin/bash 32 | # Custom setenv.sh that should be preserved 33 | export CUSTOM_JAVA_OPTS="-Xmx512m -Dfile.encoding=UTF-8" 34 | export CATALINA_OPTS="$CATALINA_OPTS $CUSTOM_JAVA_OPTS" 35 | 36 | # Some custom logic 37 | if [ -f "/opt/custom/config.properties" ]; then 38 | export JAVA_OPTS="$JAVA_OPTS -Dconfig.file=/opt/custom/config.properties" 39 | fi 40 | mode: '0755' 41 | 42 | - name: Record original setenv.sh content 43 | slurp: 44 | src: /opt/tomcat/bin/setenv.sh 45 | register: original_setenv 46 | 47 | - name: Record original override directory contents 48 | find: 49 | paths: "/etc/systemd/system/{{ expected_service_name }}.service.d" 50 | patterns: "*.conf" 51 | register: original_overrides 52 | 53 | - name: Display original configurations 54 | debug: 55 | msg: 56 | - "Original setenv.sh size: {{ original_setenv.content | b64decode | length }}" 57 | - "Original override files: {{ original_overrides.files | map(attribute='path') | list }}" 58 | 59 | - name: Apply Contrast configuration (should preserve existing configs) 60 | include_role: 61 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 62 | vars: 63 | contrast_agent_enabled: true 64 | contrast_agent_version: "6.19.0" 65 | contrast_config_method: "systemd" 66 | contrast_service_name: "{{ expected_service_name }}" 67 | contrast_test_mode: true 68 | contrast_security_config: 69 | api: 70 | url: "https://app.contrastsecurity.com/Contrast" 71 | api_key: "side-effect-test-key" 72 | 73 | - name: Check that custom override file still exists 74 | stat: 75 | path: "/etc/systemd/system/{{ expected_service_name }}.service.d/99-custom.conf" 76 | register: custom_override_after 77 | 78 | - name: Assert custom override file preserved 79 | assert: 80 | that: 81 | - custom_override_after.stat.exists 82 | fail_msg: "Custom override file was removed - this is a side effect!" 83 | success_msg: "Custom override file preserved" 84 | 85 | - name: Read custom override file content 86 | slurp: 87 | src: "/etc/systemd/system/{{ expected_service_name }}.service.d/99-custom.conf" 88 | register: custom_override_content 89 | 90 | - name: Assert custom override content unchanged 91 | assert: 92 | that: 93 | - "'CUSTOM_VAR=custom_value' in (custom_override_content.content | b64decode)" 94 | - "'-Xmx512m' in (custom_override_content.content | b64decode)" 95 | fail_msg: "Custom override file content was modified" 96 | success_msg: "Custom override file content preserved" 97 | 98 | - name: Check Contrast override file was created 99 | stat: 100 | path: "/etc/systemd/system/{{ expected_service_name }}.service.d/10-contrast.conf" 101 | register: contrast_override 102 | 103 | - name: Assert Contrast override exists alongside custom 104 | assert: 105 | that: 106 | - contrast_override.stat.exists 107 | fail_msg: "Contrast override not created" 108 | success_msg: "Contrast override created without affecting existing files" 109 | 110 | - name: Test setenv.sh preservation 111 | include_role: 112 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 113 | vars: 114 | contrast_agent_enabled: true 115 | contrast_agent_version: "6.19.0" 116 | contrast_config_method: "setenv" 117 | contrast_service_name: "{{ expected_service_name }}" 118 | contrast_test_mode: true 119 | contrast_security_config: 120 | api: 121 | url: "https://app.contrastsecurity.com/Contrast" 122 | api_key: "setenv-side-effect-test" 123 | 124 | - name: Read modified setenv.sh 125 | slurp: 126 | src: /opt/tomcat/bin/setenv.sh 127 | register: modified_setenv 128 | 129 | - name: Decode modified setenv.sh content 130 | set_fact: 131 | modified_setenv_text: "{{ modified_setenv.content | b64decode }}" 132 | 133 | - name: Assert original setenv.sh content preserved 134 | assert: 135 | that: 136 | - "'CUSTOM_JAVA_OPTS' in modified_setenv_text" 137 | - "'file.encoding=UTF-8' in modified_setenv_text" 138 | - "'config.file=/opt/custom/config.properties' in modified_setenv_text" 139 | - "'-javaagent:/opt/contrast/contrast.jar' in modified_setenv_text" 140 | fail_msg: "Original setenv.sh content was lost or javaagent not added" 141 | success_msg: "Original setenv.sh content preserved and Contrast agent added" 142 | 143 | - name: Test deactivation preservation 144 | include_role: 145 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 146 | vars: 147 | contrast_agent_enabled: false 148 | contrast_config_method: "setenv" 149 | contrast_service_name: "{{ expected_service_name }}" 150 | contrast_test_mode: true 151 | 152 | - name: Read setenv.sh after deactivation 153 | slurp: 154 | src: /opt/tomcat/bin/setenv.sh 155 | register: deactivated_setenv 156 | 157 | - name: Decode deactivated setenv.sh content 158 | set_fact: 159 | deactivated_setenv_text: "{{ deactivated_setenv.content | b64decode }}" 160 | 161 | - name: Assert original content preserved after deactivation 162 | assert: 163 | that: 164 | - "'CUSTOM_JAVA_OPTS' in deactivated_setenv_text" 165 | - "'file.encoding=UTF-8' in deactivated_setenv_text" 166 | - "'-javaagent:/opt/contrast/contrast.jar' not in deactivated_setenv_text" 167 | fail_msg: "Original content lost during deactivation or Contrast config not removed" 168 | success_msg: "Clean deactivation - original content preserved, Contrast config removed" 169 | 170 | - name: Test systemd deactivation preservation 171 | include_role: 172 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 173 | vars: 174 | contrast_agent_enabled: false 175 | contrast_config_method: "systemd" 176 | contrast_service_name: "{{ expected_service_name }}" 177 | contrast_test_mode: true 178 | 179 | - name: Verify custom override still exists after systemd deactivation 180 | stat: 181 | path: "/etc/systemd/system/{{ expected_service_name }}.service.d/99-custom.conf" 182 | register: final_custom_override 183 | 184 | - name: Verify Contrast override removed 185 | stat: 186 | path: "/etc/systemd/system/{{ expected_service_name }}.service.d/10-contrast.conf" 187 | register: final_contrast_override 188 | 189 | - name: Assert proper selective cleanup 190 | assert: 191 | that: 192 | - final_custom_override.stat.exists 193 | - not final_contrast_override.stat.exists 194 | fail_msg: "Selective cleanup failed - custom config lost or Contrast config not removed" 195 | success_msg: "Perfect selective cleanup - custom configs preserved, Contrast configs removed" 196 | 197 | - name: Test file permissions preservation 198 | stat: 199 | path: /opt/tomcat/bin/setenv.sh 200 | register: final_setenv_perms 201 | 202 | - name: Assert file permissions preserved 203 | assert: 204 | that: 205 | - final_setenv_perms.stat.mode == "0755" 206 | - final_setenv_perms.stat.pw_name == "tomcat" 207 | fail_msg: "File permissions were changed" 208 | success_msg: "File permissions preserved" 209 | 210 | - name: Display side effect test summary 211 | debug: 212 | msg: 213 | - "✅ Side effect tests passed!" 214 | - "✅ Existing systemd override files preserved" 215 | - "✅ Existing setenv.sh content preserved" 216 | - "✅ Clean deactivation without affecting custom configs" 217 | - "✅ File permissions and ownership preserved" 218 | - "✅ No unintended side effects detected" 219 | -------------------------------------------------------------------------------- /roles/agent/molecule/vagrant/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | become: true 5 | gather_facts: true 6 | 7 | vars: 8 | expected_service_name: "{{ tomcat_package | default('tomcat9' if ansible_os_family == 'Debian' else 'tomcat') }}" 9 | 10 | tasks: 11 | - name: Check if Contrast agent JAR exists 12 | stat: 13 | path: /opt/contrast/contrast.jar 14 | register: agent_jar 15 | 16 | - name: Assert agent JAR was downloaded 17 | assert: 18 | that: 19 | - agent_jar.stat.exists 20 | - agent_jar.stat.size > 1000000 # Should be > 1MB 21 | fail_msg: "Contrast agent JAR not found or too small" 22 | success_msg: "Contrast agent JAR downloaded successfully" 23 | 24 | - name: Get agent JAR file info 25 | stat: 26 | path: /opt/contrast/contrast.jar 27 | register: jar_info 28 | 29 | - name: Display agent JAR info 30 | debug: 31 | msg: 32 | - "Agent JAR size: {{ jar_info.stat.size | human_readable }}" 33 | - "Owner: {{ jar_info.stat.pw_name }}" 34 | - "Group: {{ jar_info.stat.gr_name }}" 35 | - "Mode: {{ jar_info.stat.mode }}" 36 | 37 | - name: Check systemd override directory 38 | stat: 39 | path: "/etc/systemd/system/{{ expected_service_name }}.service.d" 40 | register: override_dir 41 | 42 | - name: Assert systemd override directory exists 43 | assert: 44 | that: 45 | - override_dir.stat.exists 46 | - override_dir.stat.isdir 47 | fail_msg: "Systemd override directory not found" 48 | success_msg: "Systemd override directory exists" 49 | 50 | - name: Check systemd override file 51 | stat: 52 | path: "/etc/systemd/system/{{ expected_service_name }}.service.d/10-contrast.conf" 53 | register: override_file 54 | 55 | - name: Assert systemd override file exists 56 | assert: 57 | that: 58 | - override_file.stat.exists 59 | - override_file.stat.size > 0 60 | fail_msg: "Systemd override file not found or empty" 61 | success_msg: "Systemd override file exists" 62 | 63 | - name: Read systemd override file content 64 | slurp: 65 | src: "/etc/systemd/system/{{ expected_service_name }}.service.d/10-contrast.conf" 66 | register: override_content 67 | 68 | - name: Decode override file content 69 | set_fact: 70 | override_text: "{{ override_content.content | b64decode }}" 71 | 72 | - name: Display override file content 73 | debug: 74 | var: override_text 75 | 76 | - name: Assert override file contains javaagent 77 | assert: 78 | that: 79 | - "'-javaagent:/opt/contrast/contrast.jar' in override_text" 80 | - "'[Service]' in override_text" 81 | - "'JAVA_TOOL_OPTIONS' in override_text" 82 | fail_msg: "Override file doesn't contain expected javaagent configuration" 83 | success_msg: "Override file contains correct javaagent configuration" 84 | 85 | - name: Check for environment variables in override 86 | assert: 87 | that: 88 | - "'CONTRAST__API__URL' in override_text" 89 | - "'CONTRAST__API__API_KEY' in override_text" 90 | - "'CONTRAST__APPLICATION__NAME' in override_text" 91 | fail_msg: "Expected environment variables not found in override file" 92 | success_msg: "Environment variables properly configured" 93 | 94 | - name: Verify systemd daemon has been reloaded 95 | command: systemctl is-enabled "{{ expected_service_name }}" 96 | register: service_enabled 97 | changed_when: false 98 | 99 | - name: Assert service is enabled 100 | assert: 101 | that: 102 | - "'enabled' in service_enabled.stdout" 103 | fail_msg: "Service is not enabled" 104 | success_msg: "Service is properly enabled" 105 | 106 | - name: Check service configuration 107 | command: systemctl show "{{ expected_service_name }}" --property=Environment 108 | register: service_env 109 | changed_when: false 110 | 111 | - name: Display service environment 112 | debug: 113 | var: service_env.stdout 114 | 115 | - name: Test deactivation functionality 116 | include_role: 117 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 118 | vars: 119 | contrast_agent_enabled: false 120 | contrast_service_name: "{{ expected_service_name }}" 121 | contrast_test_mode: true 122 | 123 | - name: Verify override file was removed after deactivation 124 | stat: 125 | path: "/etc/systemd/system/{{ expected_service_name }}.service.d/10-contrast.conf" 126 | register: override_after_deactivation 127 | 128 | - name: Assert override file was removed 129 | assert: 130 | that: 131 | - not override_after_deactivation.stat.exists 132 | fail_msg: "Override file still exists after deactivation" 133 | success_msg: "Override file properly removed after deactivation" 134 | 135 | - name: Check if override directory was cleaned up 136 | stat: 137 | path: "/etc/systemd/system/{{ expected_service_name }}.service.d" 138 | register: override_dir_after 139 | 140 | - name: Check override directory contents 141 | find: 142 | paths: "/etc/systemd/system/{{ expected_service_name }}.service.d" 143 | file_type: file 144 | register: override_dir_files 145 | when: override_dir_after.stat.exists 146 | 147 | - name: Assert directory cleanup 148 | assert: 149 | that: 150 | - not override_dir_after.stat.exists or override_dir_files.files | length == 0 151 | fail_msg: "Override directory not properly cleaned up" 152 | success_msg: "Override directory properly cleaned up" 153 | 154 | - name: Test reactivation after deactivation 155 | include_role: 156 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 157 | vars: 158 | contrast_agent_enabled: true 159 | contrast_agent_version: "6.19.0" 160 | contrast_config_method: "systemd" 161 | contrast_service_name: "{{ expected_service_name }}" 162 | contrast_test_mode: true 163 | contrast_security_config: 164 | api: 165 | url: "https://app.contrastsecurity.com/Contrast" 166 | api_key: "reactivation-test-key" 167 | service_key: "reactivation-test-service-key" 168 | user_name: "reactivation-test-user" 169 | 170 | - name: Verify reactivation worked 171 | stat: 172 | path: "/etc/systemd/system/{{ expected_service_name }}.service.d/10-contrast.conf" 173 | register: reactivation_check 174 | 175 | - name: Assert reactivation successful 176 | assert: 177 | that: 178 | - reactivation_check.stat.exists 179 | fail_msg: "Reactivation failed - override file not recreated" 180 | success_msg: "Reactivation successful" 181 | 182 | - name: Test different configuration methods - setenv.sh 183 | include_role: 184 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 185 | vars: 186 | contrast_agent_enabled: true 187 | contrast_agent_version: "6.19.0" 188 | contrast_config_method: "setenv" 189 | contrast_service_name: "{{ expected_service_name }}" 190 | contrast_test_mode: true 191 | contrast_security_config: 192 | api: 193 | url: "https://app.contrastsecurity.com/Contrast" 194 | api_key: "setenv-test-key" 195 | 196 | - name: Check setenv.sh was modified 197 | stat: 198 | path: /opt/tomcat/bin/setenv.sh 199 | register: setenv_file 200 | 201 | - name: Read setenv.sh content 202 | slurp: 203 | src: /opt/tomcat/bin/setenv.sh 204 | register: setenv_content 205 | 206 | - name: Decode setenv.sh content 207 | set_fact: 208 | setenv_text: "{{ setenv_content.content | b64decode }}" 209 | 210 | - name: Display setenv.sh content 211 | debug: 212 | var: setenv_text 213 | 214 | - name: Assert setenv.sh contains javaagent 215 | assert: 216 | that: 217 | - "'-javaagent:/opt/contrast/contrast.jar' in setenv_text" 218 | - "'BEGIN ANSIBLE MANAGED BLOCK' in setenv_text" 219 | fail_msg: "setenv.sh doesn't contain expected javaagent configuration" 220 | success_msg: "setenv.sh contains correct javaagent configuration" 221 | 222 | - name: Final cleanup - deactivate agent 223 | include_role: 224 | name: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') | basename }}" 225 | vars: 226 | contrast_agent_enabled: false 227 | contrast_config_method: "setenv" 228 | contrast_service_name: "{{ expected_service_name }}" 229 | contrast_test_mode: true 230 | 231 | - name: Verify final cleanup 232 | slurp: 233 | src: /opt/tomcat/bin/setenv.sh 234 | register: final_setenv 235 | 236 | - name: Check setenv.sh cleanup 237 | set_fact: 238 | final_setenv_text: "{{ final_setenv.content | b64decode }}" 239 | 240 | - name: Assert final cleanup successful 241 | assert: 242 | that: 243 | - "'-javaagent:/opt/contrast/contrast.jar' not in final_setenv_text" 244 | - "'BEGIN ANSIBLE MANAGED BLOCK' not in final_setenv_text" 245 | fail_msg: "Final cleanup failed - setenv.sh still contains Contrast configuration" 246 | success_msg: "Final cleanup successful - all Contrast configuration removed" 247 | 248 | - name: Display test summary 249 | debug: 250 | msg: 251 | - "✅ All verification tests passed!" 252 | - "✅ Agent installation works correctly" 253 | - "✅ Systemd integration functions properly" 254 | - "✅ setenv.sh method works correctly" 255 | - "✅ Activation and deactivation work as expected" 256 | - "✅ Role is idempotent" 257 | - "✅ Configuration cleanup is thorough" 258 | --------------------------------------------------------------------------------