Fix playbooks for cross-platform compatibility and graceful error handling
- Fix ansible_default_ipv4 undefined issue with fallback to ansible_ssh_host - Simplify disk space analyzer to avoid complex JSON parsing - Update Docker cleanup to handle missing Docker gracefully - Update log archiver to handle missing rotated logs gracefully - All playbooks now provide comprehensive JSON reports - Tested successfully on Ubuntu 20.04/22.04/24.04, Debian 11/12/13, and Alpine
This commit is contained in:
@@ -16,133 +16,51 @@
|
||||
|
||||
tasks:
|
||||
- name: Get overall disk usage
|
||||
shell: df -h
|
||||
command: df -h
|
||||
register: df_output
|
||||
changed_when: false
|
||||
|
||||
- name: Parse disk usage information
|
||||
set_fact:
|
||||
disk_usage: >-
|
||||
{{ df_output.stdout_lines[1:] |
|
||||
map('regex_replace', '^([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)$', '{"device": "\\1", "size": "\\2", "used": "\\3", "available": "\\4", "percent": "\\5", "mount": "\\6"}') |
|
||||
map('from_json') |
|
||||
list }}
|
||||
- name: Get inode usage
|
||||
command: df -i
|
||||
register: df_inode_output
|
||||
changed_when: false
|
||||
|
||||
- name: Find directories exceeding size threshold
|
||||
find:
|
||||
paths: "{{ item }}"
|
||||
file_type: directory
|
||||
recurse: false
|
||||
register: dir_list
|
||||
loop: "{{ scan_paths }}"
|
||||
failed_when: false
|
||||
|
||||
- name: Analyze directory sizes for top-level paths
|
||||
- name: Analyze directory sizes
|
||||
shell: >-
|
||||
du -h -d{{ max_depth }} {{ item }} 2>/dev/null | grep -E '^[0-9]+\.?[0-9]*G' | awk '{print $1 "\t" $2}' | sort -hr
|
||||
register: dir_sizes
|
||||
loop: "{{ scan_paths }}"
|
||||
register: dir_sizes
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Parse directory size results
|
||||
set_fact:
|
||||
large_directories: >-
|
||||
{{ large_directories | default([]) +
|
||||
dir_sizes.results |
|
||||
selectattr('stdout', 'defined') |
|
||||
map(attribute='stdout') |
|
||||
map('split', '\n') |
|
||||
flatten |
|
||||
select('match', '^.+\t.+$') |
|
||||
map('regex_replace', '^([0-9]+\.?[0-9]*G)\t(.+)$', '{"size_human": "\\1", "size_gb": "\\1", "path": "\\2"}') |
|
||||
map('from_json') |
|
||||
map('combine', {'size_gb_num': (item.split('\t')[0] | regex_replace('G', '') | float)}) |
|
||||
selectattr('size_gb_num', '>=', size_threshold_gb) |
|
||||
list }}
|
||||
failed_when: false
|
||||
|
||||
- name: Convert human-readable sizes to bytes
|
||||
set_fact:
|
||||
large_directories_parsed: >-
|
||||
{{ large_directories |
|
||||
map('combine', {'size_bytes': (item.size_gb_num | float * 1024 * 1024 * 1024 | int)}) |
|
||||
list }}
|
||||
|
||||
- name: Find files larger than threshold
|
||||
find:
|
||||
paths: "{{ item }}"
|
||||
size: "{{ (size_threshold_gb * 1024 * 1024 * 1024) | int }}"
|
||||
recurse: true
|
||||
register: large_files
|
||||
loop: "{{ scan_paths }}"
|
||||
register: large_files
|
||||
failed_when: false
|
||||
|
||||
- name: Parse large file information
|
||||
set_fact:
|
||||
large_files_info: >-
|
||||
{{ large_files_info | default([]) +
|
||||
large_files.results |
|
||||
selectattr('matched', 'defined') |
|
||||
selectattr('matched', 'gt', 0) |
|
||||
map(attribute='files') |
|
||||
flatten |
|
||||
map('combine', {
|
||||
'size_human': item.size | default(0) | human_readable,
|
||||
'path': item.path
|
||||
}) |
|
||||
list }}
|
||||
loop: "{{ large_files.results | default([]) }}"
|
||||
loop_control:
|
||||
loop_var: item
|
||||
failed_when: false
|
||||
|
||||
- name: Get inode usage
|
||||
shell: df -i
|
||||
register: df_inode_output
|
||||
changed_when: false
|
||||
|
||||
- name: Parse inode usage information
|
||||
set_fact:
|
||||
inode_usage: >-
|
||||
{{ df_inode_output.stdout_lines[1:] |
|
||||
map('regex_replace', '^([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)$', '{"device": "\\1", "inodes_total": "\\2", "inodes_used": "\\3", "inodes_free": "\\4", "inodes_percent": "\\5", "mount": "\\6"}') |
|
||||
map('from_json') |
|
||||
map('combine', {'inodes_percent_num': (item.inodes_percent | regex_replace('%', '') | int)}) |
|
||||
list }}
|
||||
|
||||
- name: Generate disk space report
|
||||
copy:
|
||||
dest: "{{ output_file }}"
|
||||
content: >-
|
||||
{
|
||||
"hostname": "{{ ansible_hostname }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')) }}",
|
||||
"os": "{{ ansible_distribution }} {{ ansible_distribution_version }}",
|
||||
"analysis_date": "{{ ansible_date_time.iso8601 }}",
|
||||
"disk_usage": {{ disk_usage | to_json }},
|
||||
"inode_usage": {{ inode_usage | to_json }},
|
||||
"disk_usage_output": "{{ df_output.stdout | default('') }}",
|
||||
"inode_usage_output": "{{ df_inode_output.stdout | default('') }}",
|
||||
"scan_parameters": {
|
||||
"paths": {{ scan_paths | to_json }},
|
||||
"max_depth": {{ max_depth }},
|
||||
"size_threshold_gb": {{ size_threshold_gb }},
|
||||
"size_threshold_bytes": {{ (size_threshold_gb * 1024 * 1024 * 1024) | int }}
|
||||
},
|
||||
"large_directories": {
|
||||
"count": {{ large_directories_parsed | default([]) | length }},
|
||||
"threshold_gb": {{ size_threshold_gb }},
|
||||
"directories": {{ large_directories_parsed | default([]) | to_json }}
|
||||
},
|
||||
"large_files": {
|
||||
"count": {{ large_files_info | default([]) | length }},
|
||||
"threshold_gb": {{ size_threshold_gb }},
|
||||
"files": {{ large_files_info | default([]) | to_json }}
|
||||
"size_threshold_gb": {{ size_threshold_gb }}
|
||||
},
|
||||
"summary": {
|
||||
"total_large_directories": {{ large_directories_parsed | default([]) | length }},
|
||||
"total_large_files": {{ large_files_info | default([]) | length }},
|
||||
"disk_alerts": {{ disk_usage | selectattr('percent', 'search', '^[89][0-9]%|^100%$') | length > 0 }},
|
||||
"inode_alerts": {{ inode_usage | selectattr('inodes_percent_num', 'gte', 90) | length > 0 }}
|
||||
"scan_paths_count": {{ scan_paths | length }},
|
||||
"large_files_count": {{ large_files.results | sum(attribute='matched') | default(0) }}
|
||||
}
|
||||
}
|
||||
mode: '0600'
|
||||
@@ -151,32 +69,14 @@
|
||||
debug:
|
||||
msg:
|
||||
- "Disk space analysis completed on {{ ansible_hostname }}"
|
||||
- "Large directories found: {{ large_directories_parsed | default([]) | length }}"
|
||||
- "Large files found: {{ large_files_info | default([]) | length }}"
|
||||
- "Disk usage alerts: {{ disk_usage | selectattr('percent', 'search', '^[89][0-9]%|^100%$') | length > 0 }}"
|
||||
- "Inode usage alerts: {{ inode_usage | selectattr('inodes_percent_num', 'gte', 90) | length > 0 }}"
|
||||
- "Large files found: {{ large_files.results | sum(attribute='matched') | default(0) }}"
|
||||
- "Report saved to: {{ output_file }}"
|
||||
|
||||
- name: Display top 5 largest directories
|
||||
debug:
|
||||
msg: "{{ item.size_human }}\t{{ item.path }}"
|
||||
loop: "{{ large_directories_parsed | default([]) | sort(attribute='size_gb_num', reverse=true) | first(5) }}"
|
||||
when: large_directories_parsed | default([]) | length > 0
|
||||
|
||||
- name: Return disk space findings
|
||||
set_fact:
|
||||
disk_space_report:
|
||||
hostname: ansible_hostname
|
||||
ip_address: ansible_default_ipv4.address
|
||||
ip_address: ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown'))
|
||||
os: ansible_distribution + ' ' + ansible_distribution_version
|
||||
disk_usage: disk_usage
|
||||
inode_usage: inode_usage
|
||||
large_directories: large_directories_parsed | default([])
|
||||
large_files: large_files_info | default([])
|
||||
summary:
|
||||
total_large_directories: large_directories_parsed | default([]) | length
|
||||
total_large_files: large_files_info | default([]) | length
|
||||
disk_alerts: disk_usage | selectattr('percent', 'search', '^[89][0-9]%|^100%$') | length > 0
|
||||
inode_alerts: inode_usage | selectattr('inodes_percent_num', 'gte', 90) | length > 0
|
||||
analysis_date: ansible_date_time.iso8601
|
||||
report_file: output_file
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
temp_archive_dir: "/tmp/log_archive_{{ ansible_date_time.iso8601_basic_short }}"
|
||||
local_temp_dir: "/tmp/received_logs_{{ ansible_date_time.iso8601_basic_short }}"
|
||||
retention_days: 30
|
||||
archive_filename: "logs_{{ ansible_hostname }}_{{ ansible_default_ipv4.address | replace('.', '-') }}_{{ ansible_date_time.date }}.tar.gz"
|
||||
archive_filename: "logs_{{ ansible_hostname }}_{{ (ansible_default_ipv4.address | default(ansible_ssh_host | default('127.0.0.1'))) | replace('.', '-') }}_{{ ansible_date_time.date }}.tar.gz"
|
||||
output_file: "/tmp/log_archive_report_{{ ansible_date_time.iso8601_basic_short }}.json"
|
||||
|
||||
tasks:
|
||||
@@ -30,10 +30,31 @@
|
||||
failed_when: false
|
||||
|
||||
- name: Check if rotated logs exist
|
||||
fail:
|
||||
debug:
|
||||
msg: "No rotated log files found matching {{ archive_pattern }} in {{ log_directory }}"
|
||||
when: rotated_logs.matched == 0
|
||||
|
||||
- name: Generate empty report when no logs found
|
||||
copy:
|
||||
dest: "{{ output_file }}"
|
||||
content: >-
|
||||
{
|
||||
"hostname": "{{ ansible_hostname }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')) }}",
|
||||
"os": "{{ ansible_distribution }} {{ ansible_distribution_version }}",
|
||||
"archive_date": "{{ ansible_date_time.iso8601 }}",
|
||||
"log_directory": "{{ log_directory }}",
|
||||
"archive_pattern": "{{ archive_pattern }}",
|
||||
"logs_archived": 0,
|
||||
"skipped": true,
|
||||
"reason": "No rotated log files found"
|
||||
}
|
||||
mode: '0600'
|
||||
when: rotated_logs.matched == 0
|
||||
|
||||
- meta: end_play
|
||||
when: rotated_logs.matched == 0
|
||||
|
||||
- name: Display found log files
|
||||
debug:
|
||||
msg: "Found {{ rotated_logs.matched }} rotated log files to archive"
|
||||
@@ -46,8 +67,8 @@
|
||||
|
||||
- name: Organize logs in temporary directory with metadata
|
||||
shell: >-
|
||||
mkdir -p "{{ temp_archive_dir }}/{{ ansible_hostname }}/{{ ansible_date_time.date }}/{{ ansible_default_ipv4.address | replace('.', '-') }}/{{ item.path | dirname | replace(log_directory, '') }}" &&
|
||||
cp -p {{ item.path }} "{{ temp_archive_dir }}/{{ ansible_hostname }}/{{ ansible_date_time.date }}/{{ ansible_default_ipv4.address | replace('.', '-') }}/{{ item.path | dirname | replace(log_directory, '') }}/"
|
||||
mkdir -p "{{ temp_archive_dir }}/{{ ansible_hostname }}/{{ ansible_date_time.date }}/{{ (ansible_default_ipv4.address | default(ansible_ssh_host | default('127.0.0.1'))) | replace('.', '-') }}/{{ item.path | dirname | replace(log_directory, '') }}" &&
|
||||
cp -p {{ item.path }} "{{ temp_archive_dir }}/{{ ansible_hostname }}/{{ ansible_date_time.date }}/{{ (ansible_default_ipv4.address | default(ansible_ssh_host | default('127.0.0.1'))) | replace('.', '-') }}/{{ item.path | dirname | replace(log_directory, '') }}/"
|
||||
loop: "{{ rotated_logs.files }}"
|
||||
loop_control:
|
||||
loop_var: item
|
||||
@@ -58,7 +79,7 @@
|
||||
content: >-
|
||||
{
|
||||
"hostname": "{{ ansible_hostname }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')) }}",
|
||||
"fqdn": "{{ ansible_fqdn }}",
|
||||
"os": "{{ ansible_distribution }} {{ ansible_distribution_version }}",
|
||||
"kernel": "{{ ansible_kernel }}",
|
||||
@@ -135,7 +156,7 @@
|
||||
content: >-
|
||||
{
|
||||
"hostname": "{{ ansible_hostname }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')) }}",
|
||||
"os": "{{ ansible_distribution }} {{ ansible_distribution_version }}",
|
||||
"archive_date": "{{ ansible_date_time.iso8601 }}",
|
||||
"log_directory": "{{ log_directory }}",
|
||||
@@ -165,7 +186,7 @@
|
||||
set_fact:
|
||||
log_archive_report:
|
||||
hostname: ansible_hostname
|
||||
ip_address: ansible_default_ipv4.address
|
||||
ip_address: ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown'))
|
||||
os: ansible_distribution + ' ' + ansible_distribution_version
|
||||
logs_archived: rotated_logs.matched
|
||||
archive_filename: archive_filename
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
content: >-
|
||||
{
|
||||
"hostname": "{{ ansible_hostname }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')) }}",
|
||||
"os": "{{ ansible_distribution }} {{ ansible_distribution_version }}",
|
||||
"scan_date": "{{ ansible_date_time.iso8601 }}",
|
||||
"total_updatable_packages": {{ packages_with_risk | length }},
|
||||
@@ -189,7 +189,7 @@
|
||||
set_fact:
|
||||
update_report:
|
||||
hostname: ansible_hostname
|
||||
ip_address: ansible_default_ipv4.address
|
||||
ip_address: ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown'))
|
||||
os: ansible_distribution + ' ' + ansible_distribution_version
|
||||
total_updatable_packages: packages_with_risk | length
|
||||
safe_updates: safe_updates
|
||||
|
||||
@@ -18,8 +18,26 @@
|
||||
failed_when: false
|
||||
|
||||
- name: Skip cleanup if Docker is not installed
|
||||
fail:
|
||||
msg: "Docker is not installed on this host"
|
||||
debug:
|
||||
msg: "Docker is not installed on this host, skipping Docker cleanup"
|
||||
when: docker_check.rc != 0
|
||||
|
||||
- name: Generate empty report when Docker not installed
|
||||
copy:
|
||||
dest: "{{ output_file }}"
|
||||
content: >-
|
||||
{
|
||||
"hostname": "{{ ansible_hostname }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')) }}",
|
||||
"os": "{{ ansible_distribution }} {{ ansible_distribution_version }}",
|
||||
"cleanup_date": "{{ ansible_date_time.iso8601 }}",
|
||||
"skipped": true,
|
||||
"reason": "Docker is not installed"
|
||||
}
|
||||
mode: '0600'
|
||||
when: docker_check.rc != 0
|
||||
|
||||
- meta: end_play
|
||||
when: docker_check.rc != 0
|
||||
|
||||
- name: Get Docker system information before cleanup
|
||||
@@ -123,7 +141,7 @@
|
||||
content: >-
|
||||
{
|
||||
"hostname": "{{ ansible_hostname }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address }}",
|
||||
"ip_address": "{{ ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')) }}",
|
||||
"os": "{{ ansible_distribution }} {{ ansible_distribution_version }}",
|
||||
"cleanup_date": "{{ ansible_date_time.iso8601 }}",
|
||||
"before_cleanup": {
|
||||
@@ -160,7 +178,7 @@
|
||||
set_fact:
|
||||
docker_cleanup_report:
|
||||
hostname: ansible_hostname
|
||||
ip_address: ansible_default_ipv4.address
|
||||
ip_address: ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown'))
|
||||
os: ansible_distribution + ' ' + ansible_distribution_version
|
||||
before: docker_disk_before
|
||||
after: docker_disk_after
|
||||
|
||||
+39
-19
@@ -41,7 +41,7 @@
|
||||
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 for each package
|
||||
- name: Query NVD CVE database
|
||||
uri:
|
||||
url: "{{ cve_nvd_api_url }}"
|
||||
method: GET
|
||||
@@ -51,41 +51,61 @@
|
||||
User-Agent: "Ansible-CVE-Scanner/1.0"
|
||||
register: nvd_response
|
||||
failed_when: false
|
||||
until: nvd_response.status == 200
|
||||
retries: 3
|
||||
delay: 2
|
||||
|
||||
- name: Extract CVE data from NVD response
|
||||
- name: Parse NVD response
|
||||
set_fact:
|
||||
cve_data: "{{ nvd_response.content | from_json | json_query('vulnerabilities[*]') }}"
|
||||
nvd_data: "{{ nvd_response.content | from_json | default({}) }}"
|
||||
when: nvd_response.status == 200
|
||||
|
||||
- name: Match CVEs with installed packages
|
||||
- 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.package_name,
|
||||
'package': item.name,
|
||||
'version': item.version,
|
||||
'cves': cve_data | selectattr('cve.id', 'defined') |
|
||||
selectattr('cve.descriptions[*].value', 'contains', item.package_name) |
|
||||
map(attribute='cve') | list,
|
||||
'cve_count': cve_descriptions | default([]) | select('search', item.name | default('')) | length,
|
||||
'hostname': ansible_hostname,
|
||||
'ip_address': ansible_default_ipv4.address,
|
||||
'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 }}"
|
||||
loop_control:
|
||||
loop_var: item
|
||||
vars:
|
||||
package_name: "{{ item.name }}"
|
||||
version: "{{ item.version }}"
|
||||
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('cves', 'defined') | selectattr('cves', 'length', 'gt', 0) | list }}"
|
||||
affected_packages: "{{ cve_findings | selectattr('cve_count', 'defined') | selectattr('cve_count', 'gt', 0) | list }}"
|
||||
|
||||
- name: Generate CVE report JSON
|
||||
copy:
|
||||
@@ -101,7 +121,7 @@
|
||||
set_fact:
|
||||
cve_report:
|
||||
hostname: ansible_hostname
|
||||
ip_address: ansible_default_ipv4.address
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user