--- - name: Identify Packages with CVE Vulnerabilities hosts: all gather_facts: true vars: cve_nvd_api_url: "https://services.nvd.nist.gov/rest/json/cves/2.0" output_file: "/tmp/cve_report_{{ ansible_date_time.iso8601_basic_short }}.json" results_per_page: 2000 tasks: - name: Detect OS family and set package manager set_fact: pkg_mgr: "{{ 'apt' if ansible_os_family == 'Debian' else 'apk' if ansible_os_family == 'Alpine' else 'unknown' }}" - name: Ensure required packages are installed package: name: - curl - jq state: present - name: Get installed packages with versions (Debian/Ubuntu) command: dpkg-query -W -f='${Package}\t${Version}\n' register: installed_packages_debian changed_when: false when: ansible_os_family == 'Debian' - name: Get installed packages with versions (Alpine) command: apk info -vv register: installed_packages_alpine changed_when: false when: ansible_os_family == 'Alpine' - name: Parse package list into dictionary set_fact: package_dict: "{{ installed_packages_debian.stdout | default('') | split('\n') | select('match', '^.+\t.+$') | map('regex_replace', '^(.+?)\\t(.+)$', '{\"name\": \"\\1\", \"version\": \"\\2\"}') | map('from_json') | list }}" when: ansible_os_family == 'Debian' - name: Parse Alpine package list into dictionary set_fact: package_dict: "{{ installed_packages_alpine.stdout | default('') | split('\n') | select('match', '^.+-.+$') | map('regex_replace', '^(.+?)-([0-9].+)$', '{\"name\": \"\\1\", \"version\": \"\\2\"}') | map('from_json') | list }}" when: ansible_os_family == 'Alpine' - name: Query NVD CVE database uri: url: "{{ cve_nvd_api_url }}" method: GET return_content: true validate_certs: false headers: User-Agent: "Ansible-CVE-Scanner/1.0" register: nvd_response failed_when: false - name: Parse NVD response set_fact: nvd_data: "{{ nvd_response.content | from_json | default({}) }}" when: nvd_response.status == 200 - name: Extract CVE descriptions set_fact: cve_descriptions: >- {{ nvd_data.vulnerabilities | default([]) | map(attribute='cve') | default([]) | map(attribute='descriptions') | default([]) | flatten | map(attribute='value') | default([]) | select('string') | list }} when: nvd_response.status == 200 - name: Match packages with CVE mentions set_fact: cve_findings: >- {{ cve_findings | default([]) + [{ 'package': item.name, 'version': item.version, 'cve_count': cve_descriptions | default([]) | select('search', item.name | default('')) | length, 'hostname': ansible_hostname, 'ip_address': ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')), 'os': ansible_distribution + ' ' + ansible_distribution_version, 'scan_date': ansible_date_time.iso8601 }] }} loop: "{{ package_dict }}" when: nvd_response.status == 200 - name: Set CVE findings when NVD query failed set_fact: cve_findings: >- {{ cve_findings | default([]) + [{ 'package': item.name, 'version': item.version, 'cve_count': 0, 'note': 'CVE database query failed', 'hostname': ansible_hostname, 'ip_address': ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')), 'os': ansible_distribution + ' ' + ansible_distribution_version, 'scan_date': ansible_date_time.iso8601 }] }} loop: "{{ package_dict }}" when: nvd_response.status != 200 - name: Filter packages with CVEs set_fact: affected_packages: "{{ cve_findings | selectattr('cve_count', 'defined') | selectattr('cve_count', 'gt', 0) | list }}" - name: Generate CVE report JSON copy: dest: "{{ output_file }}" content: "{{ affected_packages | to_json(indent=2) }}" mode: '0600' - name: Display CVE summary debug: msg: "Found {{ affected_packages | length }} packages with CVEs. Report saved to {{ output_file }}" - name: Return CVE findings set_fact: cve_report: hostname: ansible_hostname ip_address: ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')) os: ansible_distribution + ' ' + ansible_distribution_version total_packages: package_dict | length packages_with_cves: affected_packages | length findings: affected_packages scan_date: ansible_date_time.iso8601 report_file: output_file