├── .travis.yml ├── Dockerfile.travis ├── LICENSE ├── README.md ├── etc ├── config.json.example ├── polscan.conf ├── probes.json ├── scanners.conf └── standalone.conf ├── lib ├── host-group-providers │ ├── Domain │ ├── McollectiveRoleFact │ ├── Primary-Network │ ├── Subdomain-Prefix │ └── auto_detect ├── host-list-providers │ ├── Ansible │ ├── Chef │ ├── Icinga2-Config │ ├── MCollective │ ├── PreviousDay │ ├── Puppet │ ├── Saltstack │ └── auto_detect ├── polscan-common.inc ├── scanner-functions.inc ├── scanner-header.inc ├── scanners │ ├── network-all-log-martians.sh │ ├── network-black-hole-routes.sh │ ├── network-connections.sh │ ├── network-empty-hosts.sh │ ├── network-hostname-fqdn.sh │ ├── network-hostname-resolve.sh │ ├── network-ignore-broadcast-requests.sh │ ├── network-ignore-icmp-requests.sh │ ├── network-internet-reachable.sh │ ├── network-inventory.sh │ ├── network-mount-inventory.sh │ ├── network-nfs-safe-exports.sh │ ├── network-nfs4-idmapd.sh │ ├── network-nftables-inventory.sh │ ├── network-no-dhcp-client.sh │ ├── network-no-ip-src-routing.sh │ ├── network-primary-net.sh │ ├── network-rp-filter.sh │ ├── network-syn-cookies-on.sh │ ├── network-tcp-max-syn-backlog.sh │ ├── network-tcp-wrapper.sh │ ├── network-time-wait.sh │ ├── network-webserver-upstream-inventory.sh │ ├── packages-apt-check.sh │ ├── packages-dpkg-errors.sh │ ├── packages-inventory.sh │ ├── performance-cpu.sh │ ├── performance-swappiness.sh │ ├── performance-tz-set.sh │ ├── perl-no-self-compiled.sh │ ├── php-ini-no-display-errors.sh │ ├── php-ini-no-expose.sh │ ├── php-ini-no-file-uploads.sh │ ├── php-ini-no-url-fopen.sh │ ├── php-ini-no-url-include.sh │ ├── php-ini-open-basedir.sh │ ├── php-modules-load.sh │ ├── php-no-pecl.sh │ ├── php-pecl-upgrades.sh │ ├── puppet-apt-repos-managed.sh │ ├── puppet-crons-managed.sh │ ├── puppet-limits-managed.sh │ ├── puppet-mounts-managed.sh │ ├── puppet-run-no-failed.sh │ ├── puppet-run-not-disabled.sh │ ├── puppet-ssh-keys-managed.sh │ ├── puppet-sudoers-managed.sh │ ├── puppet-users-managed.sh │ ├── python-no-pip.sh │ ├── security-apache-XCTO-nosniff.sh │ ├── security-apache-XFO-sameorigin.sh │ ├── security-apache-certs.sh │ ├── security-apache-forbidden-ciphers.sh │ ├── security-apache-server-tokens.sh │ ├── security-apache-sslprotocol.sh │ ├── security-apparmor-no-complain.sh │ ├── security-aslr-enabled.sh │ ├── security-cpu-vulnerabilities.sh │ ├── security-dmesg-restrict.sh │ ├── security-enforce-history.sh │ ├── security-ipv4-forwarding.sh │ ├── security-ipv6-forwarding.sh │ ├── security-kptr-restrict.sh │ ├── security-nginx-certs.sh │ ├── security-nginx-forbidden-ciphers.sh │ ├── security-nginx-no-sslv3.sh │ ├── security-nginx-prefer-server-ciphers.sh │ ├── security-nginx-server-tokens.sh │ ├── security-nginx-size-limits.sh │ ├── security-no-at.sh │ ├── security-no-autofs.sh │ ├── security-no-avahi.sh │ ├── security-no-compiler.sh │ ├── security-no-core-dumps.sh │ ├── security-no-ctrlaltdel.sh │ ├── security-no-portmap.sh │ ├── security-no-promisc.sh │ ├── security-no-public-portmap.sh │ ├── security-no-root-aliases.sh │ ├── security-no-ssh-user-equivalency.sh │ ├── security-no-telnetd.sh │ ├── security-ntpd-active.sh │ ├── security-nx-enabled.sh │ ├── security-pam-cracklib.sh │ ├── security-pam-no-nullok.sh │ ├── security-pcid-present.sh │ ├── security-pending-restarts.sh │ ├── security-pending-updates.sh │ ├── security-pending-updates2.sh │ ├── security-pending-updates3.sh │ ├── security-pending-updates4.sh │ ├── security-remote-fs-mounts.sh │ ├── security-repo-enabled.sh │ ├── security-securetty.sh │ ├── security-selinux-enabled.sh │ ├── security-sysrq-disabled.sh │ ├── security-tomcat-admin-password.sh │ ├── security-unpatched-cve.sh │ ├── ssh-hashknownhosts.sh │ ├── ssh-key-inventory.sh │ ├── ssh-key-permissions.sh │ ├── ssh-legacy-disabled.sh │ ├── ssh-no-keyboard.sh │ ├── ssh-no-root.sh │ ├── ssh-no-tcp-forward.sh │ ├── ssh-no-x11-forward.sh │ ├── ssh-privilege-separation.sh │ ├── ssh-sftp-disabled.sh │ ├── ssh-strict-mode.sh │ ├── system-208day-kernel-freeze.sh │ ├── system-disk-space.sh │ ├── system-home-partition.sh │ ├── system-hung-tasks.sh │ ├── system-immutable-files.sh │ ├── system-inventory.sh │ ├── system-mce-logged.sh │ ├── system-mounts.sh │ ├── system-no-local-root-mail.sh │ ├── system-no-oom-panic.sh │ ├── system-ntpd-slew.sh │ ├── system-oom-logged.sh │ ├── system-oom-panic.sh │ ├── system-raid-state.sh │ ├── system-readonly-fs.sh │ ├── system-segfaults.sh │ ├── system-sysctl-settings.sh │ ├── system-tmp-overflow.sh │ ├── system-tmp-partition.sh │ ├── system-unattended-upgrades.sh │ ├── system-usb-drives.sh │ ├── system-usb-keyboard.sh │ ├── system-var-partition.sh │ ├── systemd-no-failed.sh │ ├── systemd-no-masked.sh │ ├── updates-release.sh │ └── virtualization-inventory.sh ├── standalone │ ├── kubernetes-helm2-status.sh │ ├── kubernetes-kube-bench.sh │ ├── kubernetes-kube2iam-allowed-roles.sh │ ├── kubernetes-node-inventory.sh │ ├── mcollective-facter2-inventory-disk-devices.sh │ ├── mcollective-facter2-inventory-nic-names.sh │ ├── network-rbl-listings.sh │ └── network-reverse-lookup.sh └── target-providers │ └── kubernetes-contexts ├── package.json ├── polscan ├── server.js ├── tests ├── host-group-providers │ ├── Domain.sh │ ├── Subdomain-Prefix.sh │ └── auto_detect.sh └── run.sh └── www ├── api.js ├── filter.js ├── fonts ├── Muli-Bold.ttf ├── Muli-Regular.ttf └── Muli-SemiBold.ttf ├── group_list.js ├── img ├── cog_icon.png ├── failed.svg ├── grips-horizontal.png ├── grips-vertical.png ├── home_icon.png ├── hostmap_icon.png ├── itable_icon.png ├── lightning_icon.png ├── net_comp_icon.png ├── netedge.png ├── netmap.png ├── netmap_icon.png ├── netrad_icon.png ├── network.png ├── ok.svg ├── server_icon.png ├── servicemap.png ├── table_icon.png ├── treemap_icon.png ├── vtable_icon.png ├── warning.svg └── zoom_icon.png ├── index.html ├── pie_chart.js ├── polscan_view.js ├── probeapi.js ├── renderer_settings.js ├── renderers ├── dashboard.js ├── hostmap.js ├── itable.js ├── netgraph.js ├── netmap.js ├── netrad.js ├── proberender.js ├── ptable.js ├── table.js ├── treemap.js └── vtable.js ├── styles.css ├── templates.js ├── templates ├── dashboard_top_changes.html ├── group_list_item.html ├── inventory_list_item.html └── puppetdb.html ├── tooltip.js ├── view_netedge.js ├── view_netmenu.js ├── view_puppetdb.js ├── view_puppetdb_facts.js ├── view_results.js ├── view_servicemap.js ├── view_vulnerabilities.js └── views ├── Dashboard.js ├── Inventory.js ├── Network.js ├── Policies.js ├── ScanResults.js └── Vulnerabilities.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | sudo: required 3 | 4 | services: 5 | - docker 6 | 7 | before_install: 8 | - docker pull ubuntu:16.04 9 | 10 | script: 11 | - docker build -f Dockerfile.travis . 12 | -------------------------------------------------------------------------------- /Dockerfile.travis: -------------------------------------------------------------------------------- 1 | # This Dockerfile is only to run TravisCI build tests 2 | 3 | FROM ubuntu:16.04 4 | MAINTAINER lars.windolf@gmx.de 5 | 6 | RUN apt-get update 7 | RUN apt-get install -y bash bats 8 | 9 | RUN mkdir -p /src/ 10 | WORKDIR /src/ 11 | 12 | COPY . /src/ 13 | 14 | WORKDIR /src/tests/ 15 | RUN ./run.sh 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/lwindolf/polscan.svg?branch=master)](https://travis-ci.org/lwindolf/polscan) 2 | 3 | # polscan 4 | 5 | *polscan* (short for "Policy Scanner") 6 | * Makes your DevOps server configuration/security/automation policies explicit 7 | * Easily detects configuration drift (Puppet 2/3/4) 8 | * Provides details on package updates (Debian, PHP, Gem, CVEs via debsecan) 9 | * Provides basic security checks (SSH, NFS, sysctl) 10 | * Explains policies by 11 | * linking references 12 | * having reasonable descriptions 13 | * suggesting quick fixes 14 | * referencing to security standards 15 | * Agent less scanner with zero setup, no dependencies: Bash 4.2, SSH 16 | * Scales up to at least 2000 hosts * 50 scanners ~ 100k findings 17 | 18 | Policies are implemented by [small shell snippets](http://lzone.de/polscan/) and thus polscan is easily extensible by your own specific policies. To make it easy to use it comes with host discovery solutions for typical automation setups (Chef, Puppet, MCollective). 19 | 20 | Features 21 | -------------- 22 | 23 | Detecting automation issues... 24 | 25 | Product | Host Discovery | Resource Coverage 26 | ----------- | -------------- | ----------------- 27 | kube-bench | y | kube-bench results per host 28 | Puppet2/3/4 | y | Mounts, Users, SSH Keys, ulimit, sysctl, sudoers, 3rd party APT repos, Crons 29 | Chef | y | % 30 | Ansible | y | % 31 | SaltStack | y | % 32 | Mcollective | y | % 33 | 34 | Detecting package issues... 35 | 36 | Providers | Detection | Upgrade Check | Error Check | CVE Check 37 | --------- | --------- | ------------- | ----------- | --------- 38 | Helm2 | yes | no | 39 | apt | % | yes | yes 40 | dpkg | % | % | yes | yes (debsecan) 41 | Gem | yes | yes | 42 | PECL | yes | yes | 43 | PIP | yes | yes | 44 | CPAN | no | 45 | NPM | no | 46 | 47 | Collects inventories for 48 | 49 | * kubernetes clusters (node count, sizing) 50 | * NTP / DNS Servers 51 | * OS Releases, Kernel Version 52 | * External IPs, IPv6 Adresses 53 | * 3rd party APT repos used 54 | * CPU-RAM size, CPU type, Server type 55 | * RAID Vendor 56 | ... 57 | 58 | Graphs network topologies 59 | 60 | * TCP Connections 61 | * Remote FS Mounts 62 | * Nginx Upstreams / Apache ProxyPass 63 | * SSH Key Equivalencies 64 | * Network Routes 65 | 66 | Provides vulnerabilities statistics per CVE using debsecan. 67 | 68 | Screenshots 69 | ----------- 70 | 71 | *Overview Page* 72 | 73 | ![screenshot](http://lzone.de/images/polscan-overview.png) 74 | 75 | *Host Map per Finding Type* 76 | 77 | ![screenshot](http://lzone.de/images/polscan-hostmap-group-by-domain.png) 78 | 79 | *Visualizing Network Connections* 80 | 81 | ![screenshot](http://lzone.de/images/polscan-netviews.png) 82 | 83 | Note: polscan is intentionally limited to Debian and for simplicity tries not to implement any distro-specific dependencies. 84 | 85 | Running the Scanner 86 | ------------------- 87 | 88 | polscan keeps results on a daily basis so it makes sense to set up a daily cron. 89 | 90 | Or just run it from the source directory 91 | 92 | ./polscan # To re-scan all hosts 93 | ./polscan -l 'server1 server2' # To scan specific hosts 94 | 95 | ./polscan -t systemd-no-failed.sh # Test scanner on all hosts 96 | ./polscan -t systemd-no-failed.sh -l server1 # Test scanner on single host 97 | ./polscan -t all -l server1 # Test results on single host 98 | 99 | ./polscan -r 2017-10-09 # Recreate result JSON 100 | 101 | 102 | Running the GUI 103 | --------------- 104 | 105 | Start the GUI server with 106 | 107 | npm start 108 | 109 | -------------------------------------------------------------------------------- /etc/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "monitoring": { 4 | "hostname": "icinga.example.com", 5 | "port": 5665, 6 | "path": "/v1/objects/services?filter=service.state!=0&attrs=__name&attrs=type&attrs=acknowledgement&attrs=state&attrs=last_check_result", 7 | "ssl_key": "../etc/ssl/icinga.key", 8 | "ssl_cert": "../etc/ssl/icinga.crt" 9 | }, 10 | "puppetdb": { 11 | "hostname": "puppetdb.example.com", 12 | "port": 443 13 | }, 14 | "puppetdb/nodes": { 15 | "path": "/api/pdb/query/v4/nodes?include_total=true&offset=0&order_by=%5B%7B%22field%22:%22certname%22,%22order%22:%22asc%22%7D%5D&query=null" 16 | }, 17 | "puppetdb/node_facts": { 18 | "path": "/api/pdb/query/v4/fact-contents?order_by=%5B%7B%22field%22:%22name%22,%22order%22:%22asc%22%7D%5D&query=%5B%22%3D%22,%22certname%22,%22{{hostname}}%22%5D", 19 | "params": [ "hostname" ] 20 | }, 21 | "puppetdb/changed": { 22 | "path": "/api/pdb/query/v4/event-counts?query=[%22and%22,[%22=%22,%22latest_report?%22,true]]&summarize_by=containing_class" 23 | }, 24 | "puppetdb/reports": { 25 | "path": "/api/pdb/query/v4/reports?include_total=true&limit=10&offset=0&order_by=%5B%7B%22field%22:%22end_time%22,%22order%22:%22desc%22%7D%5D&query=%5B%22%3D%22,%22certname%22,%22{{hostname}}%22%5D", 26 | "params": [ "hostname" ] 27 | } 28 | }, 29 | "static": { 30 | "rootdir": "www", 31 | "results": "results" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /etc/polscan.conf: -------------------------------------------------------------------------------- 1 | # TARGET_PROVIDER: 2 | # 3 | # Defines a set of target providers to use to determine 4 | # what we want to scan (e.g. cloud providers or kubernetes 5 | # contexts). 6 | # 7 | # You can provide multiple providers using whitespaces. 8 | # 9 | TARGET_PROVIDER=kubernetes-contexts 10 | 11 | # HOST_LIST_PROVIDER: 12 | # 13 | # Defines which host list provider to use to determine 14 | # which hosts you want to check. Check subdirectory 15 | # lib/host-list-providers/ for supported variants 16 | # 17 | # You can provide multiple providers. For example: 18 | # 19 | # HOST_LIST_PROVIDER=Puppet 20 | # HOST_LIST_PROVIDER="Puppet PreviousDay" 21 | # 22 | # If you do not mind polscan probing set to "auto_detect" 23 | HOST_LIST_PROVIDER=auto_detect 24 | 25 | # HOST_GROUP_PROVIDERS: 26 | # 27 | # Define which host group provider(s) to use to determine 28 | # which host-hostgroup relations there are. 29 | # 30 | # You can provide multiple providers 31 | # 32 | # HOST_GROUP_PROVIDERS="Nagios Chef" 33 | # 34 | # If you do not mind polscan probing and collecting as much 35 | # groups as possible set this to "auto_detect" 36 | HOST_GROUP_PROVIDERS=auto_detect 37 | 38 | # SCAN_CONCURRENCY 39 | # 40 | # Set to a non-zero number to define how many remote scans are run in parallel. 41 | SCAN_CONCURRENCY=50 42 | 43 | # SSH_CMD 44 | # 45 | # The SSH command to be used. Change this if you want 46 | # different timeouts, keys... 47 | # 48 | # Examples: 49 | # 50 | # SSH_CMD="ssh" 51 | # SSH_CMD="ssh -i my_special_key/id_rsa" 52 | # SSH_CMD="ssh -F polscan_ssh_config" 53 | SSH_CMD="timeout -k 15 10 ssh -o StrictHostKeyChecking=no -o PreferredAuthentications=publickey -o ConnectTimeout=3" 54 | 55 | # SSH_USER: 56 | # 57 | # The default user to use to connect remotely. If a 58 | # host list provider returns user@host values this user 59 | # is not used. 60 | # 61 | # Don't use this if you have different users for accessing 62 | # your hosts. In this case provide an SSH config in SSH_CMD 63 | # above 64 | SSH_USER= 65 | 66 | # SUDO_CMD: 67 | # 68 | # A command used to elevate privileges on remote node. 69 | # If you do not want this and use only scanners that do 70 | # not care you can set it to empty to prevent sudo. 71 | SUDO_CMD=sudo 72 | 73 | # RESULT_BASE_DIR: 74 | # 75 | # Set to any data dir location you want. Will default 76 | # to $BASE/results if empty. 77 | RESULT_BASE_DIR= 78 | 79 | # RESULT_JSON_GZIP: 80 | # 81 | # polscan_html can run "gzip -9" on the produced output 82 | # to minimized data size. Compression ratio is 1:20 as there 83 | # is a lot of duplication. If enabled result files are named 84 | # .jsongz instead of .json and you need to configure your 85 | # webserver to deliver them with correct Content-Encoding 86 | # and JSON mime type. See vhost examples in extra/ subdir. 87 | # 88 | # Set to 1 to enable compression 89 | RESULT_JSON_GZIP=0 90 | 91 | # REMOTE_STDERR_WHITELIST 92 | # 93 | # egrep expression of stuff to ignore on stderr when running remote scanner. 94 | # Everything else will be logged as a finding in group "Polscan" 95 | REMOTE_STDERR_WHITELIST='Warning: Permanently added.*to the list of known hosts.' 96 | 97 | # MCOLLECTIVE_ROLE_FACT 98 | # 99 | # Configures the name of the fact queried by the mcollective host 100 | # group provider. As there is no standard in typical automation 101 | # tool on how to expose the host role you probably need to set your 102 | # own fact name here. 103 | MCOLLECTIVE_ROLE_FACT=role 104 | -------------------------------------------------------------------------------- /etc/scanners.conf: -------------------------------------------------------------------------------- 1 | network-all-log-martians.sh 2 | network-connections.sh 3 | network-empty-hosts.sh 4 | network-internet-reachable.sh 5 | network-hostname-fqdn.sh 6 | network-hostname-resolve.sh 7 | network-ignore-broadcast-requests.sh 8 | network-ignore-icmp-requests.sh 9 | network-inventory.sh 10 | network-nftables-inventory.sh 11 | network-mount-inventory.sh 12 | network-nfs-safe-exports.sh 13 | network-nfs4-idmapd.sh 14 | network-no-dhcp-client.sh 15 | network-no-ip-src-routing.sh 16 | network-primary-net.sh 17 | network-rp-filter.sh 18 | network-tcp-max-syn-backlog.sh 19 | network-tcp-wrapper.sh 20 | network-webserver-upstream-inventory.sh 21 | packages-apt-check.sh 22 | packages-dpkg-errors.sh 23 | packages-inventory.sh 24 | performance-cpu.sh 25 | performance-tz-set.sh 26 | perl-no-self-compiled.sh 27 | php-ini-no-display-errors.sh 28 | php-ini-no-expose.sh 29 | php-ini-no-file-uploads.sh 30 | php-ini-no-url-fopen.sh 31 | php-ini-no-url-include.sh 32 | php-ini-open-basedir.sh 33 | php-modules-load.sh 34 | php-pecl-upgrades.sh 35 | puppet-apt-repos-managed.sh 36 | puppet-crons-managed.sh 37 | puppet-limits-managed.sh 38 | puppet-mounts-managed.sh 39 | puppet-run-no-failed.sh 40 | puppet-run-not-disabled.sh 41 | puppet-ssh-keys-managed.sh 42 | puppet-sudoers-managed.sh 43 | puppet-users-managed.sh 44 | security-apache-certs.sh 45 | security-apache-forbidden-ciphers.sh 46 | security-apache-sslprotocol.sh 47 | security-apache-server-tokens.sh 48 | security-apache-XCTO-nosniff.sh 49 | security-apache-XFO-sameorigin.sh 50 | security-apparmor-no-complain.sh 51 | security-aslr-enabled.sh 52 | security-dmesg-restrict.sh 53 | security-enforce-history.sh 54 | security-ipv4-forwarding.sh 55 | security-ipv6-forwarding.sh 56 | security-kptr-restrict.sh 57 | security-nginx-certs.sh 58 | security-nginx-forbidden-ciphers.sh 59 | security-nginx-no-sslv3.sh 60 | security-nginx-prefer-server-ciphers.sh 61 | security-nginx-server-tokens.sh 62 | security-nginx-size-limits.sh 63 | security-no-at.sh 64 | security-no-autofs.sh 65 | security-no-avahi.sh 66 | security-no-core-dumps.sh 67 | security-no-ctrlaltdel.sh 68 | security-no-portmap.sh 69 | security-no-promisc.sh 70 | security-no-root-aliases.sh 71 | security-no-ssh-user-equivalency.sh 72 | security-no-telnetd.sh 73 | security-ntpd-active.sh 74 | security-nx-enabled.sh 75 | security-pam-no-nullok.sh 76 | security-pending-updates4.sh 77 | security-remote-fs-mounts.sh 78 | security-repo-enabled.sh 79 | security-securetty.sh 80 | security-selinux-enabled.sh 81 | security-sysrq-disabled.sh 82 | security-tomcat-admin-password.sh 83 | security-unpatched-cve.sh 84 | ssh-key-inventory.sh 85 | ssh-key-permissions.sh 86 | ssh-no-keyboard.sh 87 | ssh-no-root.sh 88 | ssh-no-tcp-forward.sh 89 | ssh-no-x11-forward.sh 90 | ssh-privilege-separation.sh 91 | ssh-sftp-disabled.sh 92 | ssh-strict-mode.sh 93 | system-disk-space.sh 94 | system-home-partition.sh 95 | system-hung-tasks.sh 96 | system-mce-logged.sh 97 | system-mounts.sh 98 | system-no-local-root-mail.sh 99 | system-ntpd-slew.sh 100 | system-inventory.sh 101 | system-readonly-fs.sh 102 | system-raid-state.sh 103 | system-segfaults.sh 104 | system-sysctl-settings.sh 105 | system-tmp-partition.sh 106 | system-tmp-overflow.sh 107 | system-usb-drives.sh 108 | system-usb-keyboard.sh 109 | system-var-partition.sh 110 | systemd-no-failed.sh 111 | systemd-no-masked.sh 112 | virtualization-inventory.sh 113 | -------------------------------------------------------------------------------- /etc/standalone.conf: -------------------------------------------------------------------------------- 1 | kubernetes-helm2-status.sh 2 | kubernetes-kube-bench.sh 3 | kubernetes-kube2iam-allowed-roles.sh 4 | kubernetes-node-inventory.sh 5 | mcollective-facter2-inventory-nic-names.sh 6 | mcollective-facter2-inventory-disk-devices.sh 7 | network-reverse-lookup.sh 8 | -------------------------------------------------------------------------------- /lib/host-group-providers/Domain: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # name: Domain 4 | # description: Hostgroup provider based on domain names. Simply groups all hosts by domain. 5 | 6 | cat $RESULT_DIR/.hosts |\ 7 | xargs -n 1 |\ 8 | grep -v "^[^.]*$" |\ 9 | sed 's/^\([^.]*\.\(.*\)\)/Domain::\2 \1/' 10 | -------------------------------------------------------------------------------- /lib/host-group-providers/McollectiveRoleFact: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # name: Mcollective Role Fact 4 | # description: Fetch Host Role from a Mcollective fact. This requires a 1-to-1 host/role mapping available via an Mcollective reported fact. The name of the fact is configurable via $MCOLLECTIVE_ROLE_FACT (default is "role") 5 | 6 | if [ "$MCOLLECTIVE_ROLE_FACT" != "" ]; then 7 | # Run with 3 retries, as mcollective is quite unreliable 8 | i=1 9 | while [ $i -lt 3 ]; do 10 | mco inventory -t 15 --script <(echo " 11 | inventory do 12 | format 'Role::%s %s' 13 | fields { [ facts['$MCOLLECTIVE_ROLE_FACT'], identity ] } 14 | end 15 | ") | grep -v "Role:: " 16 | i=$((i + 1)) 17 | done | sort | uniq 18 | fi 19 | -------------------------------------------------------------------------------- /lib/host-group-providers/Primary-Network: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # name: Primary-Network 4 | # description: Hostgroup provider based on the primary network inventory scanner. Allows grouping by internal networks 5 | 6 | if [ -d "$RESULT_DIR" ]; then 7 | for f in ${RESULT_DIR}/*; do 8 | sed "/Network INVENTORY ...Primary Net/!d;s/.* /Primary-Network::/;s/\$/ ${f/*\/}/" $f 9 | done | grep -v ":: " 10 | fi 11 | -------------------------------------------------------------------------------- /lib/host-group-providers/Subdomain-Prefix: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # name: Subdomain-Prefix 4 | # description: Hostgroup provider based on domain names. Simply groups all hosts by first subdomain prefix 5 | 6 | cat $RESULT_DIR/.hosts |\ 7 | xargs -n 1 |\ 8 | grep -v "^[^.]*$" |\ 9 | sed 's/^\([^.]*\.\([^.]*\)\)/Subdomain-Prefix::\2 \1/' 10 | -------------------------------------------------------------------------------- /lib/host-group-providers/auto_detect: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Host group provider auto detection: 4 | # 5 | # Probes all host group providers and returns all results 6 | 7 | # Copyright (C) 2015 Lars Windolf 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program. If not, see . 21 | 22 | found_some=0 23 | echo "Probing host group providers..." >&2 24 | cd $(dirname $0) || exit 1 25 | for p in $(ls | grep -v auto_detect); do 26 | echo -n " - $p " >&2 27 | results=$(./$p 2>/dev/null) 28 | count=$(echo "${results}" | grep -v '^$' | wc -l) 29 | echo "($count results)" >&2 30 | 31 | if [ $count -gt 0 ]; then 32 | found_some=1 33 | fi 34 | 35 | echo "$results" 36 | done 37 | 38 | if [ $found_some -ne 1 ]; then 39 | echo "ERROR: No suitable host group provider!" >&2 40 | fi 41 | -------------------------------------------------------------------------------- /lib/host-list-providers/Ansible: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # name: Ansible 4 | # description: Ansible ping based host list provider 5 | 6 | ansible all -m ping 2>/dev/null 7 | -------------------------------------------------------------------------------- /lib/host-list-providers/Chef: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # name: Chef 4 | # description: knife based host list provider. Just "knife node list". 5 | 6 | knife node list 7 | -------------------------------------------------------------------------------- /lib/host-list-providers/Icinga2-Config: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # name: Icinga2 Config 5 | # description: Provides host names from Icinga2 configuration files. Note that his might yield unwanted domain names and logical names you might not like. Consider filtering this output. 6 | 7 | host_match_pattern="object[[:space:]][[:space:]]*Host[[:space:]]" 8 | icinga2_locations="/etc/icinga2 /usr/local/icinga2/etc" 9 | 10 | rgrep -h "$host_match_pattern" $icinga2_locations 2>/dev/null |\ 11 | sed "s/.*$host_match_pattern\"//;s/\".*//" | 12 | -------------------------------------------------------------------------------- /lib/host-list-providers/MCollective: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # name: MCollective 5 | # description: List all hosts via 'mco find' 6 | 7 | mco find 8 | -------------------------------------------------------------------------------- /lib/host-list-providers/PreviousDay: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # name: PreviousDay 4 | # description: Result directory base host list provider. Provides all still resolvable hosts that were in the previous result set. Using this provider along with a primary CM based source helps to track hosts that are somehow lost to the CM 5 | 6 | if [ -d "$RESULT_BASE_DIR/$ONE_DAY_AGO" ]; then 7 | cd "$RESULT_BASE_DIR/$ONE_DAY_AGO" || exit 8 | ls | while read name; do 9 | if [ "$(dig +short $name)" != "" ]; then 10 | echo $name 11 | fi 12 | done 13 | else 14 | printf "No previous day data found!\n" >&2 15 | fi 16 | -------------------------------------------------------------------------------- /lib/host-list-providers/Puppet: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # name: Puppet 4 | # description: Puppet master based host list provider. Can be used when running from a Puppet master. Should work with Puppet 2.x and 3.x 5 | 6 | puppet cert list --all 2>/dev/null | awk '{print $2}' | sed 's/"//g' 7 | -------------------------------------------------------------------------------- /lib/host-list-providers/Saltstack: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # name: Saltstack 4 | # description: Salt master based host list provider. 5 | 6 | salt-key -L 2>/dev/null | grep -v ":" 7 | -------------------------------------------------------------------------------- /lib/host-list-providers/auto_detect: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Host list provider auto detection: 4 | # 5 | # Probes all host list providers and returns results of the 6 | # one with most results. 7 | 8 | # Copyright (C) 2015 Lars Windolf 9 | # 10 | # This program is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | 23 | 24 | declare -A results_by_provider 25 | 26 | selected= 27 | maxCount=0 28 | 29 | echo "Probing host list providers..." >&2 30 | cd $(dirname $0) || exit 1 31 | for p in $(ls | grep -v auto_detect); do 32 | echo -n " - $p " >&2 33 | results_by_provider[$p]=$(./$p 2>/dev/null) 34 | count=$(echo ${results_by_provider[$p]} | wc -w) 35 | echo "($count results)" >&2 36 | if [ $maxCount -lt $count ]; then 37 | selected=$p 38 | maxCount=$count 39 | fi 40 | done 41 | 42 | if [ "$selected" != "" ]; then 43 | echo "Selected host list provider: $selected" >&2 44 | echo ${results_by_provider[$selected]} 45 | else 46 | echo "WARNING: No suitable host list provider!" >&2 47 | fi 48 | -------------------------------------------------------------------------------- /lib/polscan-common.inc: -------------------------------------------------------------------------------- 1 | # FIXME: Check $LIB_DIR, $CONF_DIR, $BASE_DIR 2 | 3 | CONF_FILE=${CONF_DIR}/polscan.conf 4 | REMOTE_SCANNERS_FILE=${CONF_DIR}/scanners.conf 5 | STANDALONE_SCANNERS_FILE=${CONF_DIR}/standalone.conf 6 | 7 | if [ ! -f $CONF_FILE ]; then 8 | echo "ERROR: Could not find $CONF_FILE" 9 | exit 1 10 | fi 11 | set -a 12 | source $CONF_FILE 13 | set +a 14 | 15 | # Select result dir 16 | if [ "$RESULT_BASE_DIR" == "" ]; then 17 | RESULT_BASE_DIR=${BASE}/results 18 | fi 19 | 20 | # Syntax: polscan [-l ] [] 21 | 22 | if [ "$1" == "-l" ]; then 23 | shift 24 | HOST_LIST=$1 25 | shift 26 | else 27 | HOST_LIST= 28 | fi 29 | if [ "$1" == "-t" ]; then 30 | shift 31 | TEST=$1 32 | shift 33 | fi 34 | 35 | LATEST=0 36 | if [ "$1" == "" ]; then 37 | DATE=$(date +%Y/%m/%d) 38 | LATEST=1 39 | fi 40 | 41 | # Determine previous date (going back up to 7 days) 42 | i=1 43 | ONE_DAY_AGO=nonsense 44 | while [ ! -d "$RESULT_BASE_DIR/$ONE_DAY_AGO" -a $i -lt 30 ]; do 45 | ONE_DAY_AGO=$(date -d "$DATE $i day ago" +%Y/%m/%d) 46 | i=$(( $i + 1 )) 47 | done 48 | 49 | RESULT_DIR="${RESULT_BASE_DIR}/${DATE}" 50 | JSON_DIR="${RESULT_BASE_DIR}/json/${DATE}" 51 | 52 | ################################################################################ 53 | # get_policy_info 54 | # 55 | # $1 file name 56 | # $2 info type (e.g. 'tags', 'solution', 'solution-cmd') 57 | # 58 | # Returns list of tag string of policy checked by that file 59 | ################################################################################ 60 | get_policy_info() { 61 | sed " 62 | /# $2:/!d; 63 | s/^[^:]*: *//; 64 | " "$1" 65 | } 66 | 67 | ################################################################################ 68 | # get_scanners 69 | # 70 | # $1 type "standalone" or "remote" 71 | # 72 | # Returns scanner filename 73 | ################################################################################ 74 | get_scanners() { 75 | scanner_type=$1 76 | 77 | case $scanner_type in 78 | remote) 79 | conf_file="$REMOTE_SCANNERS_FILE" 80 | ;; 81 | standalone) 82 | conf_file="$STANDALONE_SCANNERS_FILE" 83 | ;; 84 | *) 85 | echo "ERROR: get_scanners() called with invalid scanner type!" 86 | exit 1 87 | esac 88 | 89 | grep -v "^ *#" "$conf_file" 2>/dev/null 90 | } 91 | 92 | ################################################################################ 93 | # get_available_scanners 94 | # 95 | # $1 type "standalone" or "remote" 96 | # 97 | # Returns scanner file names (relative to root dir) for all available scanners 98 | # of a type 99 | ################################################################################ 100 | get_available_scanners() { 101 | scanner_type=$1 102 | 103 | case $scanner_type in 104 | remote) 105 | dir="scanners" 106 | ;; 107 | standalone) 108 | dir="standalone" 109 | ;; 110 | *) 111 | echo "ERROR: get_available_scanners() called with invalid scanner type!" 112 | exit 1 113 | esac 114 | 115 | (cd "$LIB_DIR"; grep -l "^# group: [^[:space:]]" "${dir}"/* 2>/dev/null) 116 | } 117 | -------------------------------------------------------------------------------- /lib/scanner-functions.inc: -------------------------------------------------------------------------------- 1 | 2 | # Provide easy jq wrapper 3 | JQ=$(which jq) 4 | json() { 5 | printf "%s" "$1" | "$JQ" -r "$2" 6 | } 7 | 8 | # Provide k8s context iterating to be used for k8s standalone scanners 9 | foreach_kube_context() { 10 | cmd="$1" 11 | shift 12 | for t in $(echo "$TARGET_LIST" | grep "^K8S " | cut -d" " -f2); do 13 | kubectl config use-context "$t" >&2 || exit 1 14 | $cmd 15 | done 16 | kubectl config unset current-context >&2 || exit 1 17 | } 18 | 19 | # Override "echo" to prefix current host and policy infos 20 | echo() { 21 | /bin/echo $current_host $policy_group "$@" 22 | } 23 | 24 | # Reporting findings 25 | result_ok() { 26 | echo "OK |||$policy_name||| $@" 27 | } 28 | 29 | result_failed() { 30 | echo "FAILED |||$policy_name||| $@" 31 | } 32 | 33 | result_warning() { 34 | echo "WARNING |||$policy_name||| $@" 35 | } 36 | 37 | # Inventory handling 38 | result_inventory() { 39 | name=$1; shift 40 | 41 | echo "INVENTORY |||$name||| $@" 42 | } 43 | 44 | result_vulnerability() { 45 | cve=$1; shift 46 | pkg=$1; shift 47 | tags=$1; shift 48 | tags=${tags//, /,} 49 | tags=${tags//,/\",\"} 50 | 51 | cat </dev/null 79 | return $? 80 | } 81 | 82 | puppet_resource_exists() { 83 | resource_type=$1; shift 84 | value=$1; shift 85 | 86 | grep -q "resource: $resource_type\[$value\]" "$puppet_report" 2>/dev/null 87 | return $? 88 | } 89 | -------------------------------------------------------------------------------- /lib/scanner-header.inc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | current_host=$1 4 | export LC_ALL=C 5 | 6 | # Detect Puppet version 7 | if [ -f /var/lib/puppet/state/last_run_report.yaml ]; then 8 | # Puppet 2/3 9 | puppet_report=/var/lib/puppet/state/last_run_report.yaml 10 | puppet_version=23 11 | elif [ -f /opt/puppetlabs/puppet/cache/state/last_run_report.yaml ]; then 12 | # Puppet 4 13 | puppet_report=/opt/puppetlabs/puppet/cache/state/last_run_report.yaml 14 | puppet_version=4 15 | fi 16 | -------------------------------------------------------------------------------- /lib/scanners/network-all-log-martians.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: All Log Martians 5 | # description: Ensures logging of all suspicious packages 6 | # source: http://people.redhat.com/swells/scap-security-guide/RHEL/7/output/table-rhel7-cces.html 7 | # solution-cmd: echo 'net.ipv4.conf.all.log_martians=1' >/etc/sysctl.d/50-net.ipv4.conf.all.log_martians.conf && sysctl -p 8 | 9 | if [[ $(/sbin/sysctl -n net.ipv4.conf.all.log_martians 2>/dev/null) == 1 ]]; then 10 | result_failed "net.ipv4.conf.all.log_martians is not 1" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/network-black-hole-routes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Black Hole Routes 5 | # description: Ensure that we do not forget about black hole routes. Those can be leftovers of DoS mitigation attempts and are easily forgotten producing issues for DSL customers. 6 | 7 | ips=$(/sbin/route -n | grep "UGH.* lo$" | awk '{print $1}' | head -25) 8 | if [ "$ips" == "" ]; then 9 | result_ok 10 | else 11 | result_warning "Black hole routes found: $ips" 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/network-connections.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Connections 5 | # description: Pseudo scanner collecting connections using "netstat -talp --numeric-hosts". Results are used for graphing host connections. This scanner enables the 'Net Map' feature. If scanners run as root/sudo will resolve local program names. 6 | # feature: netmap 7 | 8 | OUR_NETWORKS=${OUR_NETWORKS-127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16} 9 | LISTEN_FILTER="^(${LISTEN_FILTER-ssh|nrpe|22|5666|4949})\$" 10 | fqdn=$(hostname -f) 11 | 12 | declare -a netmasks 13 | 14 | # Load configured whitelisted networks 15 | # 16 | # Uses global $OUR_NETWORKS 17 | load_nets() { 18 | # Note: configuration contains CIDR netmasks that we need 19 | # to convert to hex values for easy checking 20 | for cidr in $OUR_NETWORKS 21 | do 22 | if [[ $cidr =~ ([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/([0-9]+) ]]; then 23 | netmasks+=($(printf 'network=0x%02x%02x%02x%02x;netmask=0x%08x\n' ${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]} ${BASH_REMATCH[4]} $((2**32-2**(32-${BASH_REMATCH[5]}))))) 24 | else 25 | echo "ERROR: Invalid network address '$cidr'!" >&2 26 | fi 27 | done 28 | } 29 | 30 | # Match all whitelisted netmask against a given IP 31 | # 32 | # $1 IPv4 address 33 | # 34 | # Returns 1 if any netmask matches, 0 otherwise 35 | match_nets() { 36 | local ip=$1 37 | 38 | if [[ $ip =~ ([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then 39 | ip=$(printf '0x%02x%02x%02x%02x' ${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]} ${BASH_REMATCH[4]}) 40 | else 41 | echo "ERROR: Invalid IP '$ip'!" >&2 42 | return 0 43 | fi 44 | 45 | for entry in ${!netmasks[*]} 46 | do 47 | eval "${netmasks[$entry]}" 48 | if [[ $(($ip & $netmask)) -eq $network ]]; then 49 | return 1 50 | fi 51 | done 52 | 53 | return 0 54 | } 55 | 56 | load_nets 57 | 58 | # Analyze listening services 59 | listen_inventory= 60 | unset listen_ports 61 | declare -A listen_ports 62 | while read proto recvq sendq localaddr remoteaddr state program rest; do 63 | localport=${localaddr##*:} 64 | program=${program/ */} 65 | program=${program/:*/} 66 | program=${program/#*\//} 67 | 68 | if [[ ! $localport =~ $LISTEN_FILTER ]]; then 69 | listen_ports[$localport]=$program 70 | listen_inventory="$listen_inventory $program" 71 | fi 72 | done < <(/bin/netstat -tlp --numeric-hosts | grep -v " 127" | grep "^tcp.*LISTEN") 73 | 74 | # Analyze connections 75 | while read proto recvq sendq localaddr remoteaddr state program rest; do 76 | localip=${localaddr%%:*} 77 | localport=${localaddr##*:} 78 | remoteip=${remoteaddr%%:*} 79 | remoteport=${remoteaddr##*:} 80 | program=${program/ */} 81 | program=${program/:*/} 82 | program=${program/#*\//} 83 | 84 | if [[ $localport =~ $LISTEN_FILTER ]]; then 85 | continue 86 | fi 87 | 88 | if [[ $remoteport =~ ^[0-9]*$ ]] && [ "$remoteport" -gt 1023 ]; then 89 | remoteport=high # reduce client ports 90 | fi 91 | if [[ $localport =~ ^[0-9]*$ ]] && [ "${listen_ports[$localport]}" == "" ] && [ "$localport" -gt 1023 ]; then 92 | localport=high # reduce client ports 93 | fi 94 | 95 | # Guess at connection direction 96 | if [ "${listen_ports[$localport]}" != "" ]; then 97 | direction=in 98 | if [ "$program" == "-" ]; then 99 | program=${listen_ports[$localport]} 100 | fi 101 | elif [ $remoteport == "high" ]; then 102 | direction=out # probably true 103 | else 104 | direction=out 105 | fi 106 | printf "%s\n" "$program:$localip:$localport:$remoteip:$remoteport:$direction" 107 | done < <(/bin/netstat -tap --numeric-hosts | egrep -v "( 127| ::1|LISTEN)" | grep "^tcp") |\ 108 | 109 | # Reduce and write TCP connection edges 110 | xargs -n 1 | sort | uniq -c | 111 | awk '{print $2 " " $1}' | sed "s/:/ /g" |\ 112 | while read program localip localport remoteip remoteport direction count; do 113 | if [ "$program" == "-" ]; then 114 | program="(unknown)" 115 | fi 116 | result_network_edge "TCP connection" "$program" "$localip" "$localport" "$remoteip" "$remoteport" "$direction" "$count" 117 | done 118 | 119 | # Write port inventory 120 | result_inventory "running TCP services" $(/bin/echo $listen_inventory | xargs -n 1 | sort -u) 121 | -------------------------------------------------------------------------------- /lib/scanners/network-empty-hosts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: No entries in /etc/hosts 5 | # description: Ensures that there are no extra entries in /etc/hosts 6 | 7 | output=$(cat /etc/hosts | egrep -v "localhost|$HOSTNAME|^#|ip6|^ *$") 8 | if [ "$output" != "" ]; then 9 | result_failed "Unexpected entries in /etc/hosts: $output" 10 | fi 11 | -------------------------------------------------------------------------------- /lib/scanners/network-hostname-fqdn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: FQDN 5 | # description: Ensure that the server has an FQDN configured. 6 | 7 | if [[ $(/bin/hostname -f) =~ \. ]]; then 8 | result_ok 9 | else 10 | result_failed "'/bin/hostname -f' doesn't return an FQDN!" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/network-hostname-resolve.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Hostname and FQDN resolve local 5 | # description: Ensures that the hostname and the FQDN resolve to an IP of a local interface 6 | 7 | local_ips=$(ip a | sed '/inet[6]* /!d; s/^.* inet[6]* \([a-f0-9\:][a-f0-9\.\:\/]*\)\/.*$/\1/' | egrep -v "^(127.|::)") 8 | short=$(hostname -s) 9 | fqdn=$(hostname -f) 10 | result_short=$(getent hosts $short) 11 | result_fqdn=$(getent hosts $fqdn) 12 | if [ "$result_short" != "$result_fqdn" ] ; then 13 | result_failed "/etc/hosts resolves '$short' differently than '$fqdn'" 14 | fi 15 | 16 | if [ "$result_short" == "" ]; then 17 | result_failed "NSS doesn't resolve '$short'" 18 | else 19 | ip=${result_short/ */} 20 | if ! /bin/echo "$local_ips" | grep -q "$ip"; then 21 | result_warning "$short doesn't resolve to a NIC IP ($ip)" 22 | fi 23 | fi 24 | 25 | if [ "$result_fqdn" == "" ]; then 26 | result_failed "NSS doesn't resolve '$fqdn'" 27 | else 28 | ip=${result_fqdn/ */} 29 | if ! /bin/echo "$local_ips" | grep -q "$ip"; then 30 | result_warning "$fqdn doesn't resolve to a NIC IP ($ip)" 31 | fi 32 | fi 33 | -------------------------------------------------------------------------------- /lib/scanners/network-ignore-broadcast-requests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Ignore Broadcasts 5 | # description: Ensures that ICMP broadcast requests are ignored 6 | # source: http://people.redhat.com/swells/scap-security-guide/RHEL/7/output/table-rhel7-cces.html 7 | # solution-cmd: echo 'net.ipv4.icmp_echo_ignore_broadcasts=1' >/etc/sysctl.d/50-icmp_echo_ignore_broadcasts.conf && sysctl -p 8 | 9 | if [[ $(/sbin/sysctl -n net.ipv4.icmp_echo_ignore_broadcasts 2>/dev/null) == 0 ]]; then 10 | result_failed "net.ipv4.icmp_echo_ignore_broadcasts is not 1" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/network-ignore-icmp-requests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Ignore ICMP 5 | # description: Ensures that ICMP requests are ignored 6 | # source: http://www.linuxsecurity.com/content/view/111337/65/ 7 | # source: http://www.thegeekstuff.com/2010/07/how-to-disable-ping-replies-in-linux/ 8 | # source: http://www.linuxhowtos.org/Security/disable_ping.htm 9 | # solution-cmd: echo 'net.ipv4.icmp_echo_ignore_all=1' >/etc/sysctl.d/50-net.ipv4.icmp_echo_ignore_all.conf && sysctl -p 10 | 11 | if [[ $(/sbin/sysctl -n net.ipv4.icmp_echo_ignore_all 2>/dev/null) == 1 ]]; then 12 | result_failed "net.ipv4.icmp_echo_ignore_all is not 1" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/network-internet-reachable.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Internet Reachable 5 | # description: Ensures that outbound internet traffic is possible. Useful when you use external APT repos other other internet services. 6 | 7 | if /bin/ping -W 1 -c 1 8.8.8.8 >/dev/null 2>&1; then 8 | result_ok 9 | else 10 | result_failed "Ping 8.8.8.8 failed" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/network-inventory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Inventory 5 | # description: Inventory only scanner determining external IPv4 IPs, all routes, all nameservers and the number of configured scope global IPv6 addresses 6 | 7 | ips=$( 8 | # FIXME: Extremly cheap private net matching... 9 | /sbin/ip a |\ 10 | /usr/bin/awk '/scope (global|host)/ && !/inet6/ && !/inet (10\.|192\.168\.|172\.|127\.)/ {printf "%s ", $2}' 11 | ) 12 | 13 | result_inventory "External IPs" "$ips" 14 | 15 | result_inventory "Routes" "$(/sbin/ip route | awk '{printf "%s_%s_%s ", $1, $2, $3}')" 16 | 17 | result_inventory "Name Servers" $(awk '/^nameserver/ {print $2}' /etc/resolv.conf) 18 | result_inventory "Time Servers" $(awk '/^server/ {print $2}' /etc/ntp.conf) 19 | 20 | result_inventory "IPv6 Address" "$(ip a |grep 'inet6 .*scope global' | wc -l)" 21 | 22 | /sbin/ip route | grep "via" |\ 23 | while read net via gateway dev if; do 24 | result_network_edge "Route" "$net" "$(hostname -f)" "$if" "$gateway" "" "out" 1 25 | done 26 | -------------------------------------------------------------------------------- /lib/scanners/network-mount-inventory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Inventory 5 | # description: Inventory only scanner determining distributed filesystem mount points from /proc/mounts and mount clients on NFS exports using showmount 6 | 7 | awk '$1 ~ /:/ {print}' /proc/mounts |\ 8 | while read remote local type rest; do 9 | result_network_edge "Mount Point" "$local" "$(hostname -f)" "mount" "$remote" "$type" "out" 1 10 | done 11 | 12 | while read ip share; do 13 | result_network_edge "Mount Point" "nfs" "$(hostname -f)" "high" "$ip" "NFS Share $share" "in" 1 14 | done < <(test -f /sbin/showmount && /sbin/showmount --no-headers 2>/dev/null | sed "s/\:/ /") 15 | -------------------------------------------------------------------------------- /lib/scanners/network-nfs-safe-exports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Safe NFS Exports 5 | # description: Ensures no NFS export has no_root_squash set 6 | # solution: Remove no_root_squash from /etc/exports and run 'exportfs -a' 7 | # tags: CCE-27138-7 CCE-27121-3 CCE-27167-6 8 | 9 | results= 10 | 11 | for i in no_root_squash insecure insecure_locks; do 12 | if /bin/grep -q "$i" /etc/exports >/dev/null 2>&1; then 13 | results="$results no_root_squash" 14 | fi 15 | done 16 | 17 | if [ "$results" != "" ]; then 18 | result_failed "Unsafe NFS export options: $results" 19 | else 20 | result_ok 21 | fi 22 | -------------------------------------------------------------------------------- /lib/scanners/network-nfs4-idmapd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: NFS4 Idmapd 5 | # description: On all NFS4 clients idmapd should be running 6 | 7 | if /bin/mount | grep -q "type nfs4"; then 8 | if [ "$(pgrep -f idmapd)" == "" ]; then 9 | result_failed "idmapd is not running!" 10 | else 11 | result_ok "idmapd is running" 12 | fi 13 | fi 14 | 15 | -------------------------------------------------------------------------------- /lib/scanners/network-nftables-inventory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: nftables Inventory 5 | # description: Inventory only scanner determining which firewall modules are active and how many rules are there 6 | 7 | # Check for kernel modules 8 | # 9 | # - ebtables 10 | # - ip_tables (indicating iptables) 11 | # - iptable_nat (to indicate iptables with NAT active) 12 | # - nf_tables (to indicate active nf_tables 13 | 14 | modules=$(lsmod|grep -E "^(ebtables|nf_tables|ip_tables|iptable_nat)" | awk '{print $1}') 15 | 16 | result_inventory "Firewall Modules" $modules 17 | 18 | result_inventory "ip_tables Rule Count" $( 19 | ( 20 | if [[ $modules =~ ip_tables ]]; then 21 | iptables -L -n 22 | fi 23 | 24 | # To avoid accidentily loading nfconntrack on servers with 25 | # many connections we do not do "iptables -L -t nat" 26 | ) | wc -l 27 | ) 28 | 29 | result_inventory "nf_tables Rule Count" $( 30 | ( 31 | if [[ $modules =~ nf_tables ]]; then 32 | nft list tables 33 | fi 34 | ) | wc -l 35 | ) 36 | 37 | result_inventory "ebtables Rule Count" $( 38 | ( 39 | if [[ $modules =~ ebtables ]]; then 40 | ebtables -L 41 | fi 42 | ) | wc -l 43 | ) 44 | -------------------------------------------------------------------------------- /lib/scanners/network-no-dhcp-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: No DHCP Clients 5 | # description: Production servers should not use DHCP to assign IPs 6 | # solution: Configure a static IP 7 | # tags: CCE-27021-5 8 | 9 | if /bin/grep -q '^iface.*dhcp' /etc/network/interfaces /etc/network/interfaces.d/* >/dev/null 2>&1; then 10 | result_failed "Found DHCP configured interfaces." 11 | else 12 | result_ok 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/network-no-ip-src-routing.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: No IP Source Routing 5 | # description: Ensures that IP Source Routing is disabled 6 | # source: http://people.redhat.com/swells/scap-security-guide/RHEL/7/output/table-rhel7-cces.html 7 | # solution-cmd: echo 'net.ipv4.conf.all.accept_source_route = 0' >/etc/sysctl.d/50-net.ipv4.conf.all.accept_source_route.conf && sysctl -p 8 | 9 | if [[ $(/sbin/sysctl -n net.ipv4.conf.all.accept_source_route 2>/dev/null) != 0 ]]; then 10 | result_failed "net.ipv4.conf.all.accept_source_route is not 0" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/network-primary-net.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Primary Net 5 | # description: Inventory only scanner determining the network the FQDN resolves to 6 | 7 | ip=$(/usr/bin/getent hosts $(/bin/hostname -f)) 8 | ip=${ip/ */} 9 | 10 | if [ "$ip" != "" ]; then 11 | net=$(ip route | grep $ip) 12 | result_inventory "Primary Net" ${net/ */} 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/network-rp-filter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: IP Spoofing 5 | # description: Ensures that IP spoofing protection is enabled 6 | # source: http://people.redhat.com/swells/scap-security-guide/RHEL/7/output/table-rhel7-cces.html 7 | # source: http://www.linuxtopia.org/online_books/linux_system_administration/securing_and_optimizing_linux/chap5sec60.html 8 | # source: http://www.linuxsecurity.com/resource_files/documentation/tcpip-security.html 9 | # source: http://www.cyberciti.biz/tips/linux-iptables-8-how-to-avoid-spoofing-and-bad-addresses-attack.html 10 | # solution-cmd: echo 'net.ipv4.conf.all.rp_filter=1' >/etc/sysctl.d/50-net.ipv4.conf.all.rp_filter.conf 11 | 12 | if [[ $(/sbin/sysctl -n net.ipv4.conf.all.rp_filter 2>/dev/null) == 1 ]]; then 13 | result_failed "net.ipv4.conf.all.rp_filter is not 1" 14 | fi 15 | -------------------------------------------------------------------------------- /lib/scanners/network-syn-cookies-on.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: SYN Cookie Protection 5 | # description: Ensures that SYN cookies are enabled. 6 | # source: http://people.redhat.com/swells/scap-security-guide/RHEL/7/output/table-rhel7-cces.html 7 | # solution-cmd: echo 'net.ipv4.tcp_syncookies=1' >/etc/sysctl.d/50-net.ipv4.tcp_syncookies.conf 8 | 9 | if [[ $(/sbin/sysctl -n net.ipv4.tcp_syncookies 2>/dev/null) == 0 ]]; then 10 | result_failed "net.ipv4.tcp_syncookies is not enabled" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/network-tcp-max-syn-backlog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: SYN flooding 5 | # description: Ensures that SYN cookies are enabled and SYN backlog is configured on hosts with dmesg reporting SYN flood 6 | # source: https://www.frozentux.net/ipsysctl-tutorial/ipsysctl-tutorial.html#AEN398 7 | 8 | logged=$(/bin/dmesg | /bin/grep -i 'possible SYN flooding' | /usr/bin/tail -10) 9 | if [ "$logged" != "" ]; then 10 | if [[ $(/sbin/sysctl -n net.ipv4.tcp_syncookies 2>/dev/null) == 0 ]]; then 11 | result_warning "SYN flood warning in dmesg and net.ipv4.tcp_syncookies is not enabled." 12 | else 13 | result_warning "$logged" 14 | fi 15 | if [[ $(/sbin/sysctl -n net.ipv4.tcp_max_syn_backlog 2>/dev/null) -gt 1024 ]]; then 16 | result_failed "SYN flood warning in dmesg and net.ipv4.tcp_max_syn_backlog <= 1024." 17 | fi 18 | fi 19 | -------------------------------------------------------------------------------- /lib/scanners/network-tcp-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: TCP wrapper 5 | # description: Ensure sane TCP wrapper config with "ALL: ALL" in hosts.deny and no "ALL: ALL" in hosts.allow 6 | 7 | if ! grep "^ALL:[[:space:]]*ALL" /etc/hosts.deny 2>/dev/null; then 8 | result_failed "/etc/hosts.deny does not contain 'ALL: ALL'" 9 | fi 10 | 11 | if grep "^ALL:[[:space:]]*ALL" /etc/hosts.allow 2>/dev/null; then 12 | result_failed "/etc/hosts.allow does contain 'ALL: ALL'" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/network-time-wait.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: TCP Time Wait 5 | # description: On frontend servers exposed to DoS you want basic settings to improve the pool of connections in state TIME_WAIT 6 | # source: https://rtcamp.com/tutorials/linux/sysctl-conf/ 7 | # solution-cmd: echo 'net.ipv4.tcp_tw_recycle = 1\nnet.ipv4.tcp_tw_reuse = 1' >/etc/sysctl.d/50-network-time-wait.conf && sysctl -p 8 | 9 | if [[ $(/sbin/sysctl -n net.ipv4.tcp_tw_recycle 2>/dev/null) == 0 ]]; then 10 | result_failed "net.ipv4.tcp_tw_recycle is not 1" 11 | fi 12 | 13 | if [[ $(/sbin/sysctl -n net.ipv4.tcp_tw_reuse 2>/dev/null) == 0 ]]; then 14 | result_failed "net.ipv4.tcp_tw_reuse is not 1" 15 | fi 16 | 17 | if [[ $(/sbin/sysctl -n net.ipv4.tcp_max_tw_buckets 2>/dev/null) -le 16384 ]]; then 18 | result_failed "net.ipv4.tcp_max_tw_buckets <= 16384. You should increase it significantly." 19 | fi 20 | -------------------------------------------------------------------------------- /lib/scanners/network-webserver-upstream-inventory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Inventory 5 | # description: Inventory only scanner determining webserver upstreams (nginx server definitions, Apache proxy passes). Relies on nginx upstream definitions to be found in /etc/nginx/conf.d/* and Apache BalancerMember directives found in {/etc/apache2,/usr/local/apache2/conf}/sites-enabled. 6 | 7 | if [ -d /etc/nginx/conf.d ]; then 8 | rgrep -h "^[[:space:]]*server[[:space:]][^{]" /etc/nginx/conf.d |\ 9 | sed 's/^[[:space:]]*//;s/[\:;]/ /g' |\ 10 | while read server name port rest; do 11 | result_network_edge "Webserver Upstream" "nginx" "$(hostname -f)" "upstream" "$name" "$port" "out" 1 12 | done 13 | fi 14 | 15 | for d in /etc/apache2/sites-enabled /usr/local/apache2/conf/sites-enabled; do 16 | if [ -d "$d" ]; then 17 | grep -h "^[[:space:]]*BalancerMember.*://" "$d"/* 2>/dev/null 18 | fi 19 | done | sed "s/.*:\/\///;s/[[:space:]].*//;s/:/ /g" | sort -u |\ 20 | while read name port rest; do 21 | result_network_edge "Webserver Upstream" "Apache" "$(hostname -f)" "upstream" "$name" "$port" "out" 1 22 | done 23 | 24 | 25 | -------------------------------------------------------------------------------- /lib/scanners/packages-apt-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Packages 4 | # name: No APT errors 5 | # description: Checks if all APT dependencies are fine (using 'apt-get check') 6 | # solution-cmd: apt-get -f install 7 | 8 | output=$(/usr/bin/apt-get check 2>/dev/null) 9 | if [ $? -eq 0 ]; then 10 | result_ok 11 | else 12 | result_failed "'apt-get check' reports problems: $output" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/packages-dpkg-errors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Packages 4 | # name: No dpkg errors 5 | # description: Checks no installed package has F flag (for half-configured) and wether dpkg reports audit issues. 6 | # solution: Half-configured packages usually need reinstallation. For audit problems the reported packages themselves often need fixing. 7 | 8 | audit_output=$(/usr/bin/dpkg -C 2>/dev/null | awk '/^ / {print $1}') 9 | halfconf_output=$(/usr/bin/dpkg -l 2>/dev/null | awk '/^iF/ {print $2}') 10 | error=0 11 | 12 | if [ "$halfconf_output" != "" ]; then 13 | error=1 14 | result_failed "dpkg half configured: $halfconf_output" 15 | fi 16 | 17 | if [ "$audit_output" != "" ]; then 18 | error=1 19 | result_failed "dpkg audit reports: $audit_output" 20 | fi 21 | 22 | if [ $error -ne 0 ]; then 23 | result_ok 24 | fi 25 | -------------------------------------------------------------------------------- /lib/scanners/packages-inventory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Packages 4 | # name: Inventory 5 | # description: Inventory only scanner for Debian 3rd party repos 6 | 7 | PACKAGES_INVENTORY_3RD_PARTY_REPOS_WHITELIST=${PACKAGES_INVENTORY_3RD_PARTY_REPOS_WHITELIST-nowhitelistconfigured} 8 | 9 | if [ -d /etc/apt/ ]; then 10 | result_inventory "3rd Party Repos" $(egrep -hv "^#|debian.org|aptrepo" /etc/apt/sources.list /etc/apt/sources.list.d/* | sed 's/.*htt[ps]*:\/\///;s/[ \/].*//;s/^deb\.//;s/^packages\.//' | sort -u) 11 | fi 12 | 13 | -------------------------------------------------------------------------------- /lib/scanners/performance-cpu.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Performance 4 | # name: Powersave Off 5 | # description: On production hardware servers we do not want any type of power saving active. Checks both the CPU scaling governor /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor and if the Intel pstate driver is active with min_perf_pct at 100 6 | # solution: For non-Intel: Install and configure cpufrequtils, For Intel: set /sys/devices/system/cpu/intel_pstate/min_perf_pct to 100 7 | # source: https://wiki.debian.org/HowTo/CpuFrequencyScaling 8 | # source: https://wiki.archlinux.org/index.php/CPU_frequency_scaling 9 | 10 | if grep -q powersave /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null; then 11 | if [ -f /sys/devices/system/cpu/intel_pstate/min_perf_pct ]; then 12 | min_perf_pct=$(cat /sys/devices/system/cpu/intel_pstate/min_perf_pct) 13 | if [ "$min_perf_pct" = 100 ]; then 14 | result_ok "Intel pstate is active and min_perf_pct=100" 15 | else 16 | result_warning "Intel pstate is active and min_perf_pct!=100 (is $min_perf_pct)" 17 | fi 18 | else 19 | result_failed "Some CPUs have powersave enabled!" 20 | fi 21 | else 22 | result_ok "Scaling governor != powersave" 23 | fi 24 | 25 | result_inventory "cpufreq scaling_governor" $(cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null | sort | uniq) 26 | result_inventory "intel pstate" $(cd /sys/devices/system/cpu/intel_pstate/ 2>/dev/null && grep . * | sort | xargs) 27 | -------------------------------------------------------------------------------- /lib/scanners/performance-swappiness.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Performance 4 | # name: Swappiness = 1 5 | # description: Swappiness should be reduced as much as possible on databases of all kinds. Note: due to OOM behaviour since Linux 2.6.32+ swappiness=0 is dangerous, so to be safe we stay with swappiness=1 and throw a warning on swappiness=0. 6 | # solution-cmd: echo 'swappiness = 1' >/etc/sysctl.d/50-swappiness.conf && sysctl -p 7 | # source: http://blog.couchbase.com/often-overlooked-linux-os-tweaks 8 | # source: https://www.elastic.co/guide/en/elasticsearch/guide/current/heap-sizing.html 9 | # source: https://www.percona.com/blog/2014/04/28/oom-relation-vm-swappiness0-new-kernel/ 10 | # source: http://www.cloudera.com/content/cloudera/en/documentation/cloudera-search/v1-latest/Cloudera-Search-User-Guide/csug_tuning_solr.html 11 | # source: https://mariadb.com/kb/en/mariadb/configuring-swappiness/ 12 | # source: http://docs.oracle.com/cd/E24290_01/coh.371/e22838/tune_perftune.htm#COHAG223 13 | # source: http://java.dzone.com/articles/OOM-relation-to-swappiness 14 | 15 | value=$(/sbin/sysctl -n swappiness 2>/dev/null) 16 | if [ "$value" -gt 1 ]; then 17 | result_failed "sysctl swappiness != 1" 18 | elif [ "$value" -eq 0 ]; then 19 | result_warning "sysctl swappiness=0, this can cause OOM kills!" 20 | elif [ "$value" -eq 1 ]; then 21 | result_ok 22 | fi 23 | 24 | -------------------------------------------------------------------------------- /lib/scanners/performance-tz-set.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Performance 4 | # name: TZ is set 5 | # description: On production servers the TZ env variable should be set to avoid filesystem stat() syscalls for timezone change detection via /etc/localtime on each call to Glibc localtime() 6 | # solution-cmd: echo 'TZ=:/etc/localtime' >>/etc/environment.txt 7 | # https://blog.packagecloud.io/eng/2017/02/21/set-environment-variable-save-thousands-of-system-calls/ 8 | 9 | if [ "$TZ" == "" ]; then 10 | result_failed "Environment variable TZ is not set!" 11 | else 12 | result_ok "TZ is set to '$TZ'" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/perl-no-self-compiled.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Perl 4 | # name: No self-compiled 5 | # description: Checks there are no self-compiled Perl modules. Fuzzy check that takes existing directories as indication for those. 6 | 7 | bad_dirs= 8 | for s in $( 9 | perl -e 'print qq(@INC)' 2>/dev/null |\ 10 | xargs -n 1 |\ 11 | egrep -v '^/usr/share/|^/usr/lib|^/etc|^\.$' 12 | ); do 13 | if [ -d "$s" ]; then 14 | bad_dirs="${bad_dirs}$s " 15 | fi 16 | done 17 | 18 | if [ "$bad_dirs" != "" ]; then 19 | result_failed "Unexpected Perl module directories: $bad_dirs" 20 | else 21 | result_ok 22 | fi 23 | -------------------------------------------------------------------------------- /lib/scanners/php-ini-no-display-errors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: PHP 4 | # name: display_errors = Off 5 | # description: Prevent exposing PHP errors. 6 | # source: http://www.cyberciti.biz/tips/php-security-best-practices-tutorial.html 7 | 8 | # We fuzzy check for at least one occurence of display_errors=Off 9 | # 10 | # Debian default is 11 | # 12 | # /etc/php5/apache2/php.ini:display_errors = Off 13 | # /etc/php5/cli/php.ini:display_errors = Off 14 | 15 | found_php=0 16 | bad_dirs= 17 | for d in /etc/php5 /usr/local/php*/conf; do 18 | if [ -d "$d" ]; then 19 | found_php=1 20 | if ! rgrep -q "^display_errors[[:space:]]*=[[:space:]]*Off" $d; then 21 | bad_dirs="$bad_dirs $d" 22 | fi 23 | fi 24 | done 25 | 26 | if [ "$found_php" == 1 ]; then 27 | if [ "$bad_dirs" ]; then 28 | result_failed "display_errors=Off not found in: $bad_dirs" 29 | else 30 | result_ok 31 | fi 32 | fi 33 | -------------------------------------------------------------------------------- /lib/scanners/php-ini-no-expose.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: PHP 4 | # name: expose_php = Off 5 | # description: Prevent expose the PHP version in 'X-Powered-By' HTTP header 6 | # source: http://www.cyberciti.biz/tips/php-security-best-practices-tutorial.html 7 | 8 | # We fuzzy check for at least one occurence of expose_php=Off 9 | # 10 | # Debian default is 11 | # 12 | # /etc/php5/apache2/php.ini:expose_php = On 13 | # /etc/php5/cli/php.ini:expose_php = On 14 | 15 | found_php=0 16 | bad_dirs= 17 | for d in /etc/php5 /usr/local/php*/conf; do 18 | if [ -d "$d" ]; then 19 | found_php=1 20 | if ! rgrep -q "^expose_php[[:space:]]*=[[:space:]]*Off" $d; then 21 | bad_dirs="$bad_dirs $d" 22 | fi 23 | fi 24 | done 25 | 26 | if [ "$found_php" == 1 ]; then 27 | if [ "$bad_dirs" ]; then 28 | result_failed "expose_php=Off not found in: $bad_dirs" 29 | else 30 | result_ok 31 | fi 32 | fi 33 | -------------------------------------------------------------------------------- /lib/scanners/php-ini-no-file-uploads.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: PHP 4 | # name: file_uploads = Off 5 | # description: Prevent file uploads. 6 | # source: http://www.cyberciti.biz/tips/php-security-best-practices-tutorial.html 7 | 8 | # We fuzzy check for at least one occurence of file_uploads = Off 9 | # 10 | # Debian default is (none) 11 | 12 | found_php=0 13 | bad_dirs= 14 | for d in /etc/php5 /usr/local/php*/conf; do 15 | if [ -d "$d" ]; then 16 | found_php=1 17 | if ! rgrep -q "^file_uploads[[:space:]]*=[[:space:]]*Off" $d; then 18 | bad_dirs="$bad_dirs $d" 19 | fi 20 | fi 21 | done 22 | 23 | if [ "$found_php" == 1 ]; then 24 | if [ "$bad_dirs" ]; then 25 | result_failed "file_uploads=Off not found in: $bad_dirs" 26 | else 27 | result_ok 28 | fi 29 | fi 30 | -------------------------------------------------------------------------------- /lib/scanners/php-ini-no-url-fopen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: PHP 4 | # name: allow_url_fopen = Off 5 | # description: Prevent PHP launching remote PHP scripts 6 | # source: http://www.cyberciti.biz/tips/php-security-best-practices-tutorial.html 7 | 8 | # We fuzzy check for at least one occurence of allow_url_fopen=Off 9 | # 10 | # Debian default is 11 | # 12 | # /etc/php5/apache2/php.ini:allow_url_fopen = On 13 | # /etc/php5/cli/php.ini:allow_url_fopen = On 14 | 15 | found_php=0 16 | bad_dirs= 17 | for d in /etc/php5 /usr/local/php*/conf; do 18 | if [ -d "$d" ]; then 19 | found_php=1 20 | if ! rgrep -q "^allow_url_fopen[[:space:]]*=[[:space:]]*Off" $d; then 21 | bad_dirs="$bad_dirs $d" 22 | fi 23 | fi 24 | done 25 | 26 | if [ "$found_php" == 1 ]; then 27 | if [ "$bad_dirs" ]; then 28 | result_failed "allow_url_fopen=Off not found in: $bad_dirs" 29 | else 30 | result_ok 31 | fi 32 | fi 33 | -------------------------------------------------------------------------------- /lib/scanners/php-ini-no-url-include.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: PHP 4 | # name: allow_url_include = Off 5 | # description: Prevent PHP launching remote PHP scripts 6 | # source: http://www.cyberciti.biz/tips/php-security-best-practices-tutorial.html 7 | 8 | # We fuzzy check for at least one occurence of allow_url_include=Off 9 | # 10 | # Debian default is 11 | # 12 | # /etc/php5/apache2/php.ini:allow_url_include = Off 13 | # /etc/php5/cli/php.ini:allow_url_include = Off 14 | 15 | found_php=0 16 | bad_dirs= 17 | for d in /etc/php5 /usr/local/php*/conf; do 18 | if [ -d "$d" ]; then 19 | found_php=1 20 | if ! rgrep -q "^allow_url_include[[:space:]]*=[[:space:]]*Off" $d; then 21 | bad_dirs="$bad_dirs $d" 22 | fi 23 | fi 24 | done 25 | 26 | if [ "$found_php" == 1 ]; then 27 | if [ "$bad_dirs" ]; then 28 | result_failed "allow_url_include=Off not found in: $bad_dirs" 29 | else 30 | result_ok 31 | fi 32 | fi 33 | -------------------------------------------------------------------------------- /lib/scanners/php-ini-open-basedir.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: PHP 4 | # name: open_basedir is set 5 | # description: PHP code should be restricted to its code directory. 6 | # source: http://www.cyberciti.biz/tips/php-security-best-practices-tutorial.html 7 | 8 | # We fuzzy check for at least one occurence of openbase_dir 9 | # 10 | # Debian default is (none) 11 | 12 | found_php=0 13 | bad_dirs= 14 | for d in /etc/php5 /usr/local/php*/conf; do 15 | if [ -d "$d" ]; then 16 | found_php=1 17 | if ! rgrep -q "^open_basedir" $d; then 18 | bad_dirs="$bad_dirs $d" 19 | fi 20 | fi 21 | done 22 | 23 | if [ "$found_php" == 1 ]; then 24 | if [ "$bad_dirs" ]; then 25 | result_failed "open_basedir not found in: $bad_dirs" 26 | else 27 | result_ok 28 | fi 29 | fi 30 | -------------------------------------------------------------------------------- /lib/scanners/php-modules-load.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: PHP 4 | # name: Modules Load 5 | # description: Checks if all modules do load. Fuzzy check that uses the "php" from path. This might not always be the one you want. 6 | 7 | php=$(which php 2>/dev/null) 8 | if [ -f "$php" ]; then 9 | fails=$($php -m 2>&1 >/dev/null) 10 | if [ "$fails" != "" ]; then 11 | result_failed "$php module load failures: $fails" 12 | else 13 | result_ok 14 | fi 15 | fi 16 | -------------------------------------------------------------------------------- /lib/scanners/php-no-pecl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: PHP 4 | # name: No PECL 5 | # description: Checks there are no PECL installed modules at all. This makes sense when using disto packages or self-build packages exclusively. 6 | 7 | packages=$(pecl list 2>/dev/null |grep -A1000 ^PACKAGE | grep -v "PACKAGE") 8 | if [ "$packages" != "" ]; then 9 | result_failed "Unexpected PECL packages: $packages" 10 | else 11 | result_ok 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/php-pecl-upgrades.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: PHP 4 | # name: PECL Upgrades 5 | # description: Checks there are PECL installed modules that have pending upgrades 6 | # solution-cmd: pecl upgrade-all 7 | 8 | packages=$(pecl list 2>/dev/null |grep -A1000 ^PACKAGE | grep -v "PACKAGE") 9 | if [ "$packages" != "" ]; then 10 | upgrades=$(pecl list-upgrades | cat | grep -A1000 PACKAGE | grep -v "PACKAGE") 11 | if [ "$upgrades" != "" ]; then 12 | result_warning "Pending PECL upgrades: $upgrades" 13 | else 14 | result_ok 15 | fi 16 | fi 17 | -------------------------------------------------------------------------------- /lib/scanners/puppet-apt-repos-managed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Puppet 4 | # name: APT repos managed 5 | # description: Checks for Puppet 2/3/4 wether all APT repos with the exception of the default debian definition are managed 6 | 7 | if puppet_enabled; then 8 | if puppet_run_ok; then 9 | repo_files=$(ls /etc/apt/sources.list.d/* 2>/dev/null || grep -v '^default_debian*') 10 | unmanaged= 11 | for f in $repo_files; do 12 | # Note: puppetlabs-apt module has file resources with just the file name... 13 | # also allow absolute filenames for other e.g. self-written definitions 14 | if puppet_resource_exists "File" "$f" || puppet_resource_exists "File" "$(basename "$f")"; then 15 | : 16 | else 17 | unmanaged="${unmanaged} $f" 18 | fi 19 | done 20 | 21 | if [ "$unmanaged" != "" ]; then 22 | result_failed "Unmanaged files found in /etc/apt/sources.list.d/:$unmanaged" 23 | else 24 | result_ok 25 | fi 26 | fi 27 | fi 28 | -------------------------------------------------------------------------------- /lib/scanners/puppet-crons-managed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Puppet 4 | # name: Cron Jobs managed 5 | # description: Checks for Puppet 2/3/4 wether all cron jobs are managed using the Puppet cron resource 6 | 7 | if puppet_enabled; then 8 | if puppet_run_ok; then 9 | unmanaged= 10 | for f in /etc/crontab /var/spool/cron/crontabs/*; do 11 | results=$( 12 | egrep -v '^[ #]*$|^# [^P]|^#[^ ]|^[^ ]*=|run-parts' "$f" |\ 13 | sed '/^# Puppet Name:/,+1d' 14 | ) 15 | if [ "$results" != "" ]; then 16 | unmanaged="${unmanaged}#${f}:${results}" 17 | fi 18 | done 19 | 20 | if [ "$unmanaged" != "" ]; then 21 | result_failed "Unmanaged cron jobs found:$unmanaged" 22 | else 23 | result_ok 24 | fi 25 | fi 26 | fi 27 | -------------------------------------------------------------------------------- /lib/scanners/puppet-limits-managed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Puppet 4 | # name: /etc/security/limits* managed 5 | # description: Checks for Puppet 2/3/4 wether all /etc/security/limits.d/* definitions are managed using drop files 6 | 7 | if puppet_enabled; then 8 | if puppet_run_ok; then 9 | files=$(ls /etc/security/limits.d/* 2>/dev/null) 10 | unmanaged= 11 | for f in $files; do 12 | if ! puppet_resource_exists "File" "$f"; then 13 | unmanaged="${unmanaged} $f" 14 | fi 15 | done 16 | 17 | if [ "$unmanaged" != "" ]; then 18 | result_failed "Unmanaged files found:$unmanaged" 19 | else 20 | result_ok 21 | fi 22 | fi 23 | 24 | tmp=$(egrep -v "^#|^[[:space:]]*$" /etc/security/limits.conf | paste -d'#' -s) 25 | if [ "$tmp" != "" ]; then 26 | result_failed "Unexpected entries in /etc/security/limits.conf (all entries should be Puppet managed and in separate files in /etc/security/limits.d/):#$tmp" 27 | fi 28 | fi 29 | -------------------------------------------------------------------------------- /lib/scanners/puppet-mounts-managed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Puppet 4 | # name: Mounts managed 5 | # description: Checks for Puppet 2/3/4 wether all mount points are managed as a File resource. This is no perfect check, but gives an indication that they are not rogue mounts. 6 | 7 | if puppet_enabled; then 8 | if puppet_run_ok; then 9 | dfs_mounts=$(mount | grep ':/' | sed 's/^.* on \([^ ]*\) type.*/\1/') 10 | unmanaged= 11 | for f in $dfs_mounts; do 12 | if ! puppet_resource_exists "File" "$f"; then 13 | unmanaged="${unmanaged}#$f" 14 | fi 15 | done 16 | 17 | if [ "$unmanaged" != "" ]; then 18 | result_failed "Unmanaged mounts found:$unmanaged" 19 | else 20 | result_ok 21 | fi 22 | fi 23 | fi 24 | -------------------------------------------------------------------------------- /lib/scanners/puppet-run-no-failed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Puppet-Run 4 | # name: No failed 5 | # description: Checks for Puppet 2/3/4 last_run_report.yaml for failed resources 6 | 7 | if puppet_enabled; then 8 | if [ "$(find "$(dirname $puppet_report)" -name "$(basename $puppet_report)" -mtime +1)" != "" ]; then 9 | result_failed "No run in the last 24 hours!" 10 | else 11 | failed=$(nl $puppet_report 2>/dev/null | grep 'status: failure') 12 | resources= 13 | while read f; do 14 | # Ugly YAML parsing by backtracking resource... 15 | resources="$resources,$(nl $puppet_report | grep -B100 "$f" | grep 'resource:' | sed 's/.*resource://;s/"//g' | tail -1)" 16 | done < <(/bin/echo "$failed" | grep -v "^$") 17 | 18 | if [ "$resources" != "" ]; then 19 | result_failed "Failed resources:" ${resources/,/} 20 | else 21 | result_ok 22 | fi 23 | fi 24 | fi 25 | -------------------------------------------------------------------------------- /lib/scanners/puppet-run-not-disabled.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Puppet-Run 4 | # name: Not disabled 5 | # description: Checks for Puppet 2/3/4 lock file indicating Puppet runs are disabled. Also checks if the puppet daemon is running. 6 | # solution-cmd: /usr/bin/puppet agent --enable 7 | 8 | puppet_state_dir=$(dirname "$puppet_report" 2>/dev/null) 9 | if [ -d "$puppet_state_dir" ]; then 10 | if ! pgrep puppet >/dev/null; then 11 | result_failed "No Puppet process running!" 12 | fi 13 | 14 | if [ ! -f "$puppet_state_dir/puppetdlock" -a ! -f "$puppet_state_dir/agent_disabled.lock" ]; then 15 | result_ok 16 | else 17 | comment=$(sed 's/.*disabled_message":"\([^"]*\)"/\1/' "$puppet_state_dir/agent_disabled.lock" 2>/dev/null) 18 | result_failed "Agent disabled (reason: ${comment-unknown})!" 19 | fi 20 | fi 21 | -------------------------------------------------------------------------------- /lib/scanners/puppet-ssh-keys-managed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Puppet 4 | # name: SSH Keys managed 5 | # description: Checks for Puppet 2/3/4 wether all SSH keys are managed 6 | 7 | if puppet_enabled; then 8 | if puppet_run_ok; then 9 | authorized_keys_files=$(awk -F : '{ print $6 "/.ssh/authorized_keys" }' /etc/passwd | xargs -n1 ls 2>/dev/null) 10 | unmanaged= 11 | for f in $authorized_keys_files; do 12 | if ! puppet_resource_exists "File" "$f"; then 13 | unmanaged="${unmanaged} $f" 14 | fi 15 | done 16 | 17 | if [ "$unmanaged" != "" ]; then 18 | result_failed "Unmanaged files found:$unmanaged" 19 | else 20 | result_ok 21 | fi 22 | fi 23 | fi 24 | -------------------------------------------------------------------------------- /lib/scanners/puppet-sudoers-managed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Puppet 4 | # name: /etc/sudoers* managed 5 | # description: Checks for Puppet 2/3/4 wether all sudoers definitions are managed 6 | 7 | if puppet_enabled; then 8 | if puppet_run_ok; then 9 | sudoers_files=$(ls /etc/sudoers.d/* 2>/dev/null | grep -v README) 10 | unmanaged= 11 | for f in $sudoers_files; do 12 | if ! puppet_resource_exists "File" "$f"; then 13 | unmanaged="${unmanaged} $f" 14 | fi 15 | done 16 | 17 | if [ "$unmanaged" != "" ]; then 18 | result_failed "Unmanaged files found:$unmanaged" 19 | else 20 | result_ok 21 | fi 22 | fi 23 | 24 | tmp=$(egrep -v "^#|^Defaults|^[[:space:]]*$|^(root|admin|%sudo)[[:space:]]" /etc/sudoers | paste -d'#' -s) 25 | if [ "$tmp" != "" ]; then 26 | result_failed "Unexpected entries in /etc/sudoers (all entries should be Puppet managed and in separate files in /etc/sudoers.d/):#$tmp" 27 | fi 28 | fi 29 | -------------------------------------------------------------------------------- /lib/scanners/puppet-users-managed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Puppet 4 | # name: Users managed 5 | # description: Checks for Puppet 2/3/4 wether all UID > 1000 are managed using Puppet resources 6 | 7 | if puppet_enabled; then 8 | if puppet_run_ok; then 9 | users=$(awk -F: '{if(($3 >= 1000) && ($3 < 65534)) { print $1 }}' /etc/passwd) 10 | unmanaged= 11 | for u in $users; do 12 | if ! puppet_resource_exists "User" "$u"; then 13 | unmanaged="${unmanaged} $u" 14 | fi 15 | done 16 | 17 | if [ "$unmanaged" != "" ]; then 18 | result_failed "Unmanaged users found:$unmanaged" 19 | else 20 | result_ok 21 | fi 22 | fi 23 | fi 24 | -------------------------------------------------------------------------------- /lib/scanners/python-no-pip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Python 4 | # name: No Pip 5 | # description: Checks there are no Pip installed packages at all. This makes sense when using distro packages or self-build packages exclusively. 6 | 7 | packages=$(pip freeze 2>/dev/null) 8 | if [ "$packages" != "" ]; then 9 | result_failed "Unexpected Python packages: $packages" 10 | else 11 | result_ok 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/security-apache-XCTO-nosniff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Apache XCTO nosniff 5 | # description: An Apache production webserver might want to prevent MSIE from file type sniffing. Only set if you are sure all your documents have proper MIME types, otherwise you might break your site for MSIE users. 6 | # source: https://blogs.msdn.microsoft.com/ie/2008/09/02/ie8-security-part-vi-beta-2-update/ 7 | # source: https://www.owasp.org/index.php/List_of_useful_HTTP_headers 8 | 9 | for dir in /etc/apache2 /usr/local/apache2/conf /usr/local/apache/conf; do 10 | if [ -d $dir ]; then 11 | if ! rgrep -qRP "Header\s+set\s+X-Content-Type-Options:\s+.nosniff." $dir/*-enabled; then 12 | result_failed "XCTO Header is not set to 'nosniff' in $dir" 13 | else 14 | result_ok "$dir has XCTO header set to 'nosniff'" 15 | fi 16 | fi 17 | done 18 | -------------------------------------------------------------------------------- /lib/scanners/security-apache-XFO-sameorigin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Apache XFO sameorigin 5 | # description: An Apache production webserver might want to prevent embedding its pages as frames. This prevents clickjacking. 6 | # source: https://www.owasp.org/index.php/List_of_useful_HTTP_headers 7 | 8 | for dir in /etc/apache2 /usr/local/apache2/conf /usr/local/apache/conf; do 9 | if [ -d $dir ]; then 10 | if ! rgrep -qRP "Header\s+set\s+X-Frame-Options:\s+.sameorigin." $dir/*-enabled; then 11 | result_failed "XFO Header is not set to 'sameorigin' in $dir" 12 | else 13 | result_ok "$dir has XFO header set to 'sameorigin'" 14 | fi 15 | fi 16 | done 17 | -------------------------------------------------------------------------------- /lib/scanners/security-apache-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Apache SSL Certs 5 | # description: An Apache production webserver should not use weak certificates. Checks for weak signatures (better than SHA-256) and RSA public key size (>=4096) 6 | # solution: You need to create a new certificate with stronger signing signature and/or RSA public key size 7 | # tags: NIST-800-57 8 | # source: http://csrc.nist.gov/publications/nistpubs/800-57/sp800-57_part1_rev3_general.pdf 9 | # source: https://www.cabforum.org/wp-content/uploads/Baseline_Requirements_V1.pdf 10 | # source: https://blog.mozilla.org/security/2014/09/23/phasing-out-certificates-with-sha-1-based-signature-algorithms/ 11 | # source: http://social.technet.microsoft.com/wiki/conatents/articles/32288.windows-enforcement-of-sha1-certificates.aspx 12 | # source: http://security.stackexchange.com/questions/109629/deprecation-of-sha1-code-signing-certificates-on-windows 13 | # source: https://support.microsoft.com/en-us/kb/2661254 14 | 15 | 16 | for dir in /etc/apache2 /usr/local/apache2/conf /usr/local/apache/conf; do 17 | if [ -d $dir ]; then 18 | while read -r c; do 19 | c=${c//[\'\"]/} 20 | x=$(openssl x509 -in "$c" -text) 21 | 22 | # Check for weak signature algorithm 23 | if echo "$x" | grep -q "Signature Algorithm: sha1WithRSAEncryption"; then 24 | result_failed "$c is SHA-1 signed which is insecure!" 25 | elif echo "$x" | grep -q "Signature Algorithm: sha256WithRSAEncryption"; then 26 | result_warning "$c is SHA-256 signed and might be insecure." 27 | fi 28 | 29 | # Check for insufficient RSA key sizes 30 | key_size=$( 31 | echo "$x" | grep "RSA Public Key: ([0-9][0-9]* bit)" |\ 32 | sed 's/.*\(([0-9][0-9]*\) bit).*/\1/' 33 | ) 34 | if [ "$key_size" != "" ]; then 35 | if [ "$key_size" -lt 1024 ]; then 36 | result_failed "$c has public key size '$key_size' which is insecure!" 37 | elif [ "$key_size" -lt 4096 ]; then 38 | result_warning "$c has public key size '$key_size' which is insufficient (should be >=4096)." 39 | fi 40 | fi 41 | 42 | # Check for expired/expiring certs (1 week) 43 | if ! openssl x509 -checkend 604800 -noout -in "$c"; then 44 | result_failed "$c expired/expire soon ($(openssl x509 -enddate -noout -in "$c"))." 45 | fi 46 | done < <( 47 | grep -h "^[^#]*SSLCertificateFile" "$dir/"*-enabled/* 2>/dev/null |\ 48 | sed 's/^.*SSLCertificateFile *//' 49 | ) 50 | fi 51 | done 52 | -------------------------------------------------------------------------------- /lib/scanners/security-apache-forbidden-ciphers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Apache Forbidden Ciphers 5 | # description: An Apache production webserver should not allow those ciphers: aNULL, eNULL, EXPORT, DES, MD5, PSK, RC4 6 | # source: https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html 7 | # source: http://security.stackexchange.com/questions/51680/optimal-web-server-ssl-cipher-suite-configuration 8 | 9 | for dir in /etc/apache2 /usr/local/apache2/conf /usr/local/apache/conf; do 10 | if [ -d $dir ]; then 11 | if rgrep -q "^[^#]*SSLCipherSuite" $dir/*-enabled $dir/conf.d 2>/dev/null; then 12 | missing= 13 | for c in aNULL eNULL EXPORT DES MD5 PSK RC4; do 14 | if ! rgrep -q "ssl_ciphers[[:space:]][[:space:]].*\!$c" $dir/*-enabled $dir/conf.d 2>/dev/null; then 15 | missing="${missing}!$c " 16 | fi 17 | done 18 | result_failed "$dir: ssl_ciphers does not have $missing" 19 | fi 20 | fi 21 | done 22 | -------------------------------------------------------------------------------- /lib/scanners/security-apache-server-tokens.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Apache ServerTokens 5 | # description: An Apache production webserver should not give details in the 'Server:' header 6 | # tags: SV-36672r1_rule CCE-27425-8 7 | # solution-cmd: a2enconf security 8 | 9 | for dir in /etc/apache2 /usr/local/apache2/conf /usr/local/apache/conf; do 10 | if [ -d $dir ]; then 11 | if ! rgrep -qR "ServerTokens[[:space:]][[:space:]]*Prod" $dir/*-enabled; then 12 | result_failed "ServerTokens is not set to 'Prod' in $dir" 13 | else 14 | result_ok "$dir has ServerTokens set to 'Prod'" 15 | fi 16 | fi 17 | done 18 | -------------------------------------------------------------------------------- /lib/scanners/security-apache-sslprotocol.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Apache SSL Protocol 5 | # description: An Apache production webserver should disable legacy protocols 6 | 7 | failed=0 8 | for dir in /etc/apache2 /usr/local/apache2/conf /usr/local/apache/conf; do 9 | if [ -d $dir ]; then 10 | # There should be no SSLProtocol lines without the following patterns 11 | for m in "-SSLv2" "-SSLv3" 12 | do 13 | if [ "$(rgrep SSLProtocol $dir/ | grep -v -- "$m")" != "" ]; then 14 | result_failed "SSLProtocol should include $m ($dir)" 15 | failed=1 16 | fi 17 | done 18 | 19 | if ! rgrep -q "[^#]*SSLHonorCipherOrder.*on" $dir; then 20 | result_failed "SSLHonorCipherOrder should be on ($dir)" 21 | fi 22 | 23 | # POODLE disabled 24 | if ! rgrep -q "[^#]*SSLCompression.*off" $dir; then 25 | result_failed "SSLCompression should be off ($dir)" 26 | fi 27 | fi 28 | done 29 | 30 | if [ "$failed" = 0 ]; then 31 | result_ok 32 | fi 33 | -------------------------------------------------------------------------------- /lib/scanners/security-apparmor-no-complain.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: AppArmor no complain 5 | # description: Checks if there are no AppArmor profiles in complain mode 6 | 7 | output=$(aa-status 2>/dev/null) 8 | if [ "$output" != "" ]; then 9 | if ! /bin/echo "$output" | grep -q "0 profiles are in complain mode"; then 10 | result_warning 11 | fi 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/security-aslr-enabled.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: ASLR enabled 5 | # description: Address Space Layout Randomization is to be enabled. 6 | # tags: CCE-27007-4 7 | # solution-cmd: echo 'kernel.randomize_va_space=2' >/etc/sysctl.d/50-kernel.randomize_va_space.conf 8 | 9 | if [[ $(/sbin/sysctl -n kernel.randomize_va_space 2>/dev/null) != "2" ]]; then 10 | result_failed "sysctl kernel.randomize_va_space != 2" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/security-cpu-vulnerabilities.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: CPU vulnerabilities 5 | # description: Checks for Meltdown and Specte mitigations 6 | # solution: Upgrade kernel to 4.15+ 7 | 8 | if grep -q "Mitigation: PTI" /sys/devices/system/cpu/vulnerabilities/meltdown 2>/dev/null; then 9 | result_ok 10 | else 11 | result_failed "No Meltdown mitigation found!" 12 | fi 13 | 14 | if grep -q "Vulnerable: Minimal generic ASM retpoline" /sys/devices/system/cpu/vulnerabilities/spectre_v2 2>/dev/null; then 15 | result_ok 16 | else 17 | result_failed "No Spectre v2 mitigation found!" 18 | fi 19 | -------------------------------------------------------------------------------- /lib/scanners/security-dmesg-restrict.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: dmesg restricted 5 | # description: Non-root users should have no access to sensitive infos in dmesg 6 | # solution-cmd: echo 'kernel.dmesg_restrict = 1' >/etc/sysctl.d/50-dmesg-restrict.conf && sysctl -p 7 | # source: https://wiki.archlinux.org/index.php/Security 8 | 9 | if [ $(/sbin/sysctl -n kernel.dmesg_restrict) == "1" ]; then 10 | result_ok 11 | else 12 | result_failed "dmesg is not restricted to root only!" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/security-enforce-history.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Enforce Bash History 5 | # description: Enforce safe Bash history for all users. Enforce timestamp for history. 6 | # source: http://akyl.net/securing-bashhistory-file-make-sure-your-linux-system-users-won%E2%80%99t-hide-or-delete-their-bashhistory 7 | 8 | patterns="HISTIGNORE=[\"'][\"'] 9 | HISTCONTROL=[\"'][\"'] 10 | HISTTIMEFORMAT=[\"'][^[:space:]] 11 | HISTFILE=~/\.bash_history 12 | HISTFILESIZE=[0-9][0-9]* 13 | readonly\\ HISTFILE 14 | readonly\\ HISTSIZE 15 | readonly\\ HISTFILESIZE 16 | readonly\\ HISTIGNORE 17 | readonly\\ HISTCONTROL" 18 | 19 | /bin/echo "$patterns" | while read p; do 20 | if ! grep -q "$p" /etc/profile /etc/bash.bashrc; then 21 | result_warning "Missing setting. Check for '$p' failed." 22 | fi 23 | done 24 | 25 | getent passwd |\ 26 | cut -d : -f 6 |\ 27 | xargs --replace={} ls {}/.bash_history 2>/dev/null |\ 28 | xargs lsattr |\ 29 | while read attr file; do 30 | if [[ ! $attr =~ a ]]; then 31 | result_warning "$file is not append-only." 32 | fi 33 | done 34 | -------------------------------------------------------------------------------- /lib/scanners/security-ipv4-forwarding.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No IPv4 forwarding 5 | # description: IPv4 forwarding is to be disabled 6 | # tags: CCE-26866-4 7 | 8 | if /sbin/sysctl -a 2>/dev/null | grep -q "^net\.ipv4\.*\.forwarding = 1" 2>/dev/null; then 9 | result_failed "IPv4 forwarding is enabled" 10 | fi 11 | -------------------------------------------------------------------------------- /lib/scanners/security-ipv6-forwarding.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No IPv6 forwarding 5 | # description: IPv6 forwarding is to be disabled 6 | 7 | if /sbin/sysctl -a 2>/dev/null | grep -q "^net\.ipv6\..*\.forwarding = 1" 2>/dev/null; then 8 | result_failed "IPv6 forwarding is enabled" 9 | fi 10 | -------------------------------------------------------------------------------- /lib/scanners/security-kptr-restrict.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: kptr restricted 5 | # description: Non-root users should have no access to kernel symbols in /proc/kallsyms 6 | # solution-cmd: echo 'kernel.kptr_restrict = 1' >/etc/sysctl.d/50-kptr-restrict.conf && sysctl -p 7 | # source: https://wiki.archlinux.org/index.php/Security 8 | 9 | if [ $(/sbin/sysctl -n kernel.kptr_restrict 2>/dev/null) == "1" ]; then 10 | result_ok 11 | else 12 | result_failed "Kernel symbols in /proc/kallsyms are not restricted to root only!" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/security-nginx-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: nginx SSL Certs 5 | # description: An nginx production webserver should not use weak certificates. Checks for weak signatures (better than SHA-256) and RSA public key size (>=4096) 6 | # solution: You need to create a new certificate with stronger signing signature and/or RSA public key size 7 | # tags: NIST-800-57 8 | # source: http://csrc.nist.gov/publications/nistpubs/800-57/sp800-57_part1_rev3_general.pdf 9 | # source: https://www.cabforum.org/wp-content/uploads/Baseline_Requirements_V1.pdf 10 | # source: https://blog.mozilla.org/security/2014/09/23/phasing-out-certificates-with-sha-1-based-signature-algorithms/ 11 | # source: http://social.technet.microsoft.com/wiki/conatents/articles/32288.windows-enforcement-of-sha1-certificates.aspx 12 | # source: http://security.stackexchange.com/questions/109629/deprecation-of-sha1-code-signing-certificates-on-windows 13 | # source: https://support.microsoft.com/en-us/kb/2661254 14 | 15 | 16 | 17 | for dir in /etc/nginx /usr/local/nginx/conf; do 18 | if [ -d $dir ]; then 19 | while read -r c; do 20 | c=${c//[\'\"]} 21 | x=$(openssl x509 -in "$c" -text) 22 | 23 | # Check for weak signature algorithm 24 | if echo "$x" | grep -q "Signature Algorithm: sha1WithRSAEncryption"; then 25 | result_failed "$c is SHA-1 signed which is insecure!" 26 | elif echo "$x" | grep -q "Signature Algorithm: sha256WithRSAEncryption"; then 27 | result_warning "$c is SHA-256 signed and might be insecure." 28 | fi 29 | 30 | # Check for insufficient RSA key sizes 31 | key_size=$( 32 | echo "$x" | grep "RSA Public Key: ([0-9][0-9]* bit)" |\ 33 | sed 's/.*(\([0-9][0-9]*\) bit).*/\1/' 34 | ) 35 | if [ "$key_size" != "" ]; then 36 | if [ "$key_size" -lt 1024 ]; then 37 | result_failed "$c has public key size '$key_size' which is insecure!" 38 | elif [ "$key_size" -lt 4096 ]; then 39 | result_warning "$c has public key size '$key_size' which is insufficient (should be >=4096)" 40 | fi 41 | fi 42 | 43 | # Check for expired/expiring certs (1 week) 44 | if ! openssl x509 -checkend 604800 -noout -in "$c"; then 45 | result_failed "$c expired/expire soon ($(openssl x509 -enddate -noout -in "$c"))." 46 | fi 47 | done < <( 48 | grep -h "^[^#]*ssl_certificate[^_]" "$dir/"*-enabled/* 2>/dev/null |\ 49 | sed 's/^.*ssl_certificate *//;s/;//' 50 | ) 51 | fi 52 | done 53 | -------------------------------------------------------------------------------- /lib/scanners/security-nginx-forbidden-ciphers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: nginx Forbidden Ciphers 5 | # description: An nginx production webserver should not allow those ciphers: aNULL, eNULL, EXPORT, DES, MD5, PSK, RC4 6 | # source: https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html 7 | 8 | for dir in /etc/nginx /usr/local/nginx/conf; do 9 | if [ -d $dir ]; then 10 | if rgrep -q "^[^#]*ssl_ciphers" $dir/*-enabled $dir/conf.d 2>/dev/null; then 11 | missing= 12 | for c in aNULL eNULL EXPORT DES MD5 PSK RC4; do 13 | if ! rgrep -q "ssl_ciphers[[:space:]][[:space:]].*\!$c" $dir/*-enabled $dir/conf.d 2>/dev/null; then 14 | missing="${missing}!$c " 15 | fi 16 | done 17 | result_failed "$dir: ssl_ciphers does not have $missing" 18 | fi 19 | fi 20 | done 21 | -------------------------------------------------------------------------------- /lib/scanners/security-nginx-no-sslv3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: nginx Prefer Server Ciphers 5 | # description: An nginx production webserver should prefer the server side ciphers. 6 | # source: https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html 7 | 8 | for dir in /etc/nginx /usr/local/nginx/conf; do 9 | if [ -d $dir ]; then 10 | if rgrep -q "ssl_protocols[[:space:]][[:space:]]*SSLv[23]" $dir/*-enabled $dir/conf.d; then 11 | result_failed "ssl_protocols includes SSLv[2/3]" 12 | fi 13 | fi 14 | done 15 | -------------------------------------------------------------------------------- /lib/scanners/security-nginx-prefer-server-ciphers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: nginx Prefer Server Ciphers 5 | # description: An nginx production webserver should prefer the server side ciphers. 6 | # source: https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html 7 | 8 | for dir in /etc/nginx /usr/local/nginx/conf; do 9 | if [ -d $dir ]; then 10 | if ! rgrep -q "ssl_prefer_server_ciphers[[:space:]][[:space:]]*on" $dir/*-enabled $dir/conf.d; then 11 | result_failed "ssl_prefer_server_ciphers is not set to 'on'" 12 | fi 13 | fi 14 | done 15 | -------------------------------------------------------------------------------- /lib/scanners/security-nginx-server-tokens.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: nginx ServerTokens 5 | # description: An nginx production webserver should not give details in the 'Server:' header 6 | 7 | for dir in /etc/nginx /usr/local/nginx/conf; do 8 | if [ -d $dir ]; then 9 | if ! rgrep -q "server_tokens[[:space:]][[:space:]]*off" $dir/*-enabled $dir/conf.d; then 10 | result_failed "server_tokens is not set to 'off'" 11 | fi 12 | fi 13 | done 14 | -------------------------------------------------------------------------------- /lib/scanners/security-nginx-size-limits.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: nginx size limits 5 | # description: An nginx production webserver should prevent buffer attacks. While there are defaults for the different client request buffer setting and size limits it might be worth minimizing the as much as possible. 6 | # solution-cmd: printf "client_body_buffer_size 1K;\nclient_header_buffer_size 1k;\nclient_max_body_size 1k;\nlarge_client_header_buffers 2 1k;\n" >/etc/nginx/conf.d/50-size-limits.conf 7 | # source: http://www.cyberciti.biz/tips/linux-unix-bsd-nginx-webserver-security.html 8 | # source: http://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_buffer_size 9 | # source: http://nginx.org/en/docs/http/ngx_http_core_module.html#client_header_buffer_size 10 | # source: http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size 11 | # source: http://nginx.org/en/docs/http/ngx_http_core_module.html#large_client_header_buffers 12 | 13 | settings=" 14 | client_body_buffer_size 15 | client_header_buffer_size 16 | client_max_body_size 17 | large_client_header_buffers 18 | " 19 | 20 | locations="/etc/nginx /usr/local/nginx/conf" 21 | 22 | for dir in $locations; do 23 | if [ -d $dir ]; then 24 | for s in $settings; do 25 | if ! rgrep -q "$s[[:space:]][[:space:]]*[1-9]" $dir/*-enabled $dir/conf.d; then 26 | result_failed "$s is not set anywhere in $locations" 27 | fi 28 | done 29 | fi 30 | done 31 | -------------------------------------------------------------------------------- /lib/scanners/security-no-at.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No at daemon 5 | # description: The atd daemon must not be installed 6 | # tags: CCE-27249-2 NIST-800-53-CM-7 7 | # solution-cmd: apt-get purge at 8 | 9 | if dpkg -l at 2>/dev/null | grep -q '^ii'; then 10 | result_failed "Package at is installed" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/security-no-autofs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No automounter 5 | # description: The automounter must not be installed 6 | # tags: CCE-26976-1 7 | # solution-cmd: apt-get purge autofs 8 | 9 | if dpkg -l 'autofs*' 2>/dev/null | grep -q '^ii'; then 10 | result_failed "Package autofs is installed" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/security-no-avahi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No Avahi 5 | # description: No Avahi services should be running 6 | # tags: CCE-27087-6 7 | # solution-cmd: apt-get purge avahi-daemon 8 | 9 | if pgrep -f "avahi*" >/dev/null; then 10 | result_failed "Avahi processes are running!" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/security-no-compiler.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No compiler 5 | # description: Production systems, especially frontends should have no compiler to prevent overly easy privilege escalation. 6 | # tags: SV-32956r2_rule 7 | # solution-cmd: apt-get purge c-compiler 8 | 9 | if [ -f /usr/bin/cc ]; then 10 | result_failed "Compiler found: /usr/bin/cc" 11 | else 12 | result_ok 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/security-no-core-dumps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No Core Dumps 5 | # description: Checks if core dumps are disabled in /etc/security/limits.conf and /etc/security/limits.d/* 6 | 7 | output=$(grep "core[:space:][:space:]*[^0]" /etc/security/limits.conf /etc/security/limits.d/* 2>/dev/null) 8 | if [ "$output" != "" ]; then 9 | result_warning "Core dumps are not disabled!" 10 | fi 11 | -------------------------------------------------------------------------------- /lib/scanners/security-no-ctrlaltdel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Ctrl-Alt-Del disabled 5 | # description: Reboot via console Ctrl-Alt-Del should be disabled 6 | # solution-cmd: sed -i "/^[^#]ctrlaltdel/s/^/#/" /etc/inittab 7 | 8 | if grep -q "^[[:space:]]*[^#].*:ctrlaltdel:" /etc/inittab 2>/dev/null; then 9 | result_failed "Ctrl-Alt-Del is allowed in /etc/inittab" 10 | fi 11 | -------------------------------------------------------------------------------- /lib/scanners/security-no-portmap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No portmapper 5 | # description: Ensures that portmap is not installed 6 | # solution-cmd: apt-get purge portmap 7 | 8 | if dpkg -l portmap 2>/dev/null | grep -q '^ii'; then 9 | result_failed "portmap must not be installed!" 10 | else 11 | result_ok 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/security-no-promisc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No promiscuous interface 5 | # description: Ensures there are no interfaces in promiscuous mode indicating package capturing 6 | # solution-cmd: /sbin/ip a | grep PROMISC | awk -F: '{print $2}' | xargs -n 1 -I "{}" ifconfig "{}" -promisc 7 | 8 | interfaces=$(/sbin/ip a | grep PROMISC | awk -F: '{print $2}') 9 | if [ "$interfaces" != "" ]; then 10 | result_warning "Interfaces in promiscuous mode: $interfaces" 11 | else 12 | result_ok 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/security-no-public-portmap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No public portmap 5 | # description: The portmap port must no be visible on external IPs. 6 | 7 | if netstat -tlpn | egrep -q "0.0.0.0:(111|836).*LISTEN.*(portmap|rpcbind)"; then 8 | ext_ips=$(ip a | grep "inet " | egrep -v "inet (172|10|192|127)") 9 | if [ "$ext_ips" != "" ]; then 10 | result_failed "Portmap port bound on 0.0.0.0" 11 | fi 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/security-no-root-aliases.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No root aliases 5 | # description: Ensures there are no users with UID 0 besides root. 6 | # tags: CCE-26971-2 7 | 8 | tmp=$(awk -F: '($3 == "0") {print}' /etc/passwd | grep -v "^root:") 9 | if [ "$tmp" != "" ]; then 10 | result_failed "There should be no users with UID 0 besides root (but there are: $tmp)" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/security-no-ssh-user-equivalency.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No SSH User Equivalency 5 | # description: There must be no direct user equivalency. To detect this we match all SSH keys of all users against all authorized_keys to find network wide user equivalency. 6 | # solution: Remove reported keys from authorized_keys 7 | # source: https://www.usenix.org/legacy/event/lisa04/tech/full_papers/napier/napier.pdf 8 | # 9 | 10 | homes=$(/usr/bin/getent passwd |/bin/egrep -v "(false|nologin)$" | /usr/bin/cut -d : -f 6) 11 | keys= 12 | result= 13 | authorized_keys_files= 14 | for h in $homes; do 15 | if [ -d "$h/.ssh" ]; then 16 | keys="$keys $(find "$h/.ssh" -name "*.pub")" 17 | if [ -f "$h/.ssh/authorized_keys" ]; then 18 | authorized_keys_files="$authorized_keys_files $h/.ssh/authorized_keys" 19 | fi 20 | fi 21 | done 22 | 23 | for k in $keys; do 24 | kvalue=$(cut -d " " -f 2 "$k") 25 | if grep -q "$kvalue" $authorized_keys_files; then 26 | result="$result $k" 27 | fi 28 | done 29 | 30 | if [ "$result" == "" ]; then 31 | result_ok 32 | else 33 | result_warning "Keys with possible user equivalency:" $result 34 | fi 35 | -------------------------------------------------------------------------------- /lib/scanners/security-no-telnetd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No telnetd 5 | # description: Ensures that telnetd is not installed 6 | # tags: CCE-27073-6 CCE-26836-7 NIST-800-53-AC-17(8) NIST-800-53-CM-7 7 | # solution-cmd: apt-get purge telnetd 8 | 9 | if dpkg -l telnetd 2>/dev/null | grep -q '^ii'; then 10 | result_failed "telnetd must not be installed!" 11 | else 12 | result_ok 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/security-ntpd-active.sh: -------------------------------------------------------------------------------- 1 | # group: Security 2 | # name: ntpd on 3 | # description: There must be a running NTP server (as ntpd or chronyd) 4 | # tags: CCE-27093-4 5 | # solution-cmd: apt-get install ntp; /etc/init.d/ntp start 6 | 7 | if ! pgrep -f "(ntpd|chronyd)" >/dev/null; then 8 | result_failed "No running NTP process found!" 9 | else 10 | result_ok 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/security-nx-enabled.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Execute Disable Support 5 | # description: On Intel CPUs execute disable protection should be active 6 | # tags: CCE-27001-4 7 | 8 | if [[ $(dmesg | grep '[NX|DX]*protection: active') == "" ]]; then 9 | result_failed "Intel Execute Disable support not active!" 10 | fi 11 | 12 | -------------------------------------------------------------------------------- /lib/scanners/security-pam-cracklib.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: PAM cracklib 5 | # description: pam_cracklib.so is to be enabled in /etc/pam.d/* to enforce better passwords 6 | # solution-cmdline: apt-get install libpam-cracklib 7 | 8 | if ! rgrep -q "^password[[:space:]]*requisite[[:space:]]*pam_cracklib.so" /etc/pam.d/; then 9 | result_failed "pam_cracklib.so is not enabled in /etc/pam.d/" 10 | else 11 | result_ok 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /lib/scanners/security-pam-no-nullok.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: PAM nullok 5 | # description: PAM should not allow nullok or nullok_secure 6 | # tags: CCE-27038-9 7 | 8 | if grep -q nullok /etc/pam.d/common-auth; then 9 | result_failed "Found nullok in /etc/pam.d/common-auth" 10 | else 11 | result_ok 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /lib/scanners/security-pcid-present.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: pcid CPU flag 5 | # description: /proc/cpuinfo must contain pcid 6 | # reference: https://groups.google.com/forum/m/#!topic/mechanical-sympathy/L9mHTbeQLNU 7 | # reference: https://www.theregister.co.uk/2018/01/09/meltdown_spectre_slowdown/ 8 | # solution: For VMs ensure your hyporvisor passes the CPU feature flag. For HW host: get a CPU supporting PCID 9 | 10 | if grep -q pcid /proc/cpuinfo; then 11 | result_ok "pcid feature present" 12 | else 13 | result_failed "Processor feature PCID is not present!" 14 | fi 15 | 16 | -------------------------------------------------------------------------------- /lib/scanners/security-pending-restarts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Pending Restarts 5 | # description: There should be no pending service restarts after library upgrades. This is checked using the needstart tool which needs to be installed (Available starting with Jessie/Wheezy Backports). 6 | 7 | services=$(/usr/sbin/needrestart -b -r l 2>/dev/null) 8 | if [ "$services" != "" ]; then 9 | result_warning "Services requiring restart: $services" 10 | else 11 | result_ok 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/security-pending-updates.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Pending Updates 5 | # description: There should be no pending security updates. We consider all package upgrades with 'Debian-Security' string as providing security upgrades that need to be installed 6 | 7 | TMPFILE=$(mktemp) && { 8 | /bin/echo "$security_repos" >$TMPFILE 9 | pkgs=$( 10 | # Typical apt-get -s dist-upgrade output: 11 | # 12 | # Inst liberror-perl (0.17-1 Debian:7.9/oldstable [all]) 13 | # Inst libfcgi0ldbl [2.4.0-8.1] (2.4.0-8.1+deb7u1 Debian:7.9/oldstable [amd64]) 14 | # 15 | # Note the extra version in line 2 16 | apt-get -s dist-upgrade |\ 17 | grep "^Inst.*Security" |\ 18 | awk '{if($3 ~ /\[/) { version=$4} else { version=$3 }; gsub(/\(/, "", version); print $2 "=" version}' 19 | ) 20 | if [ "$pkgs" != "" ]; then 21 | result_warning "Security upgrades pending:" $pkgs 22 | else 23 | result_ok 24 | fi 25 | rm $TMPFILE 26 | } 27 | -------------------------------------------------------------------------------- /lib/scanners/security-pending-updates2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Pending Updates 5 | # description: There should be no pending updates. This check uses apt-show-versions which needs to be installed. 6 | 7 | pkgs=$( 8 | apt-show-versions 2>/dev/null |\ 9 | grep ' upgradeable to ' |\ 10 | awk '{package=$1; gsub(/:.*/, "", package); print package "=" $5}' 11 | ) 12 | if [ "$pkgs" != "" ]; then 13 | result_warning "Security upgrades pending:" $pkgs 14 | else 15 | result_ok 16 | fi 17 | -------------------------------------------------------------------------------- /lib/scanners/security-pending-updates3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Pending Updates 5 | # description: There should be no pending security updates. This check uses aptitudes which needs to be installed. 6 | 7 | pkgs=$(aptitude search '?and(~U,~Asecurity)' -F "%p=%V" --disable-columns) 8 | if [ "$pkgs" != "" ]; then 9 | result_warning "Security upgrades pending:" $pkgs 10 | else 11 | result_ok 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/security-pending-updates4.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Pending Updates 5 | # description: There should be no pending security updates. This check uses debsecan and checks for "high urgency" listings. Note: debsecan does not report package versions. 6 | # solution-cmd: apt-get install $(debsecan --only-fixed 2>&1 | grep "high urgency" | awk '{print $2}') 7 | 8 | # Note: explicitely pass --config and SOURCE= to workaround Debian #842428 9 | if [ -f /usr/bin/debsecan ]; then 10 | /usr/bin/timeout -k 5 -s 9 4 /usr/bin/debsecan --only-fixed --config <(/bin/echo SOURCE="https://security-tracker.debian.org/tracker/debsecan/release/1/") --suite "$(lsb_release -cs)" 2>&1 |\ 11 | grep "high urgency" |\ 12 | grep -v "obsolete" |\ 13 | while read cve package tags; do 14 | tags=$(/bin/echo "$tags" | sed "s/fixed, //;s/(//;s/)//") 15 | result_vulnerability "$cve" "$package" "$tags" 16 | done 17 | else 18 | result_warning "debscan is not installed" 19 | fi 20 | 21 | -------------------------------------------------------------------------------- /lib/scanners/security-remote-fs-mounts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Remote FS Mounts 5 | # description: All remote FS mounts need to use nodev and nosuid 6 | # tags: CCE-27090-0 CCE-26972-0 7 | 8 | # FIXME: Complete list of remote FS 9 | missing_nodev=$(mount | egrep 'type (nfs|gluster)' | egrep -v 'nodev|nfsd') 10 | missing_nosuid=$(mount | egrep 'type (nfs|gluster)' | egrep -v 'nosuid|nfsd') 11 | 12 | if [ "$missing_nodev" != "" ]; then 13 | result_failed "Remote FS mount without nodev option: $missing_nodev" 14 | fi 15 | if [ "$missing_nosuid" != "" ]; then 16 | result_failed "Remote FS mount without nosuid option: $missing_nosuid" 17 | fi 18 | -------------------------------------------------------------------------------- /lib/scanners/security-repo-enabled.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Security Repo 5 | # description: Ensure the security.debian.org repo is enabled 6 | 7 | if [ -d /etc/apt ]; then 8 | if egrep -q '^deb[[:space:]]*(http://security.debian.org|http://archive.debian.org.*security).*main' /etc/apt/sources.list /etc/apt/sources.list.d/* 2>/dev/null; then 9 | result_ok 10 | else 11 | result_failed "Debian security updates repo not found in /etc/apt/source.list*!" 12 | fi 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/security-securetty.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: securetty 5 | # description: No root login via virtual consoles and serial ports 6 | # tags: CCE-26855-7 CCE-27047-0 7 | 8 | insecure=$(egrep '^(ttyS[0-9]|vc/[0-9])[[:space:]]*$' /etc/securetty) 9 | if [ "$insecure" != "" ]; then 10 | result_failed "/etc/securetty allows root login via" $insecure 11 | else 12 | result_ok 13 | fi 14 | 15 | -------------------------------------------------------------------------------- /lib/scanners/security-selinux-enabled.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: SELinux enabled 5 | # description: Uses 'sestatus' to check if SELinux is enabled. Checks grub.cfg for not having selinux=0. 6 | # tags: CCE-26956-3 CCE-26969-6 7 | 8 | if ! sestatus 2>/dev/null | grep -q enabled; then 9 | result_failed "sestatus does not report 'enabled'" 10 | fi 11 | 12 | if grep -q "^[[:space:]]*[^#]selinux=0" /boot/grub/grub.cfg; then 13 | result_failed "SELinux disabled in grub.cfg" 14 | fi 15 | 16 | -------------------------------------------------------------------------------- /lib/scanners/security-sysrq-disabled.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: No SysRq 5 | # description: /proc/sys/kernel/sysrq is to be 0 6 | # solution-cmd: echo 'kernel.sysrq = 0' >/etc/sysctl.d/50-kernel.sysrq.conf && sysctl -p 7 | 8 | if [[ $(/sbin/sysctl -n kernel.sysrq 2>/dev/null) != "0" ]]; then 9 | result_failed "sysctl kernel.sysrq != 0" 10 | fi 11 | 12 | -------------------------------------------------------------------------------- /lib/scanners/security-tomcat-admin-password.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security 4 | # name: Tomcat Admin 5 | # description: Check for insane tomcat admin default password 'admin' as there as just to many places online suggesting this. 6 | # source: https://geekflare.com/apache-tomcat-hardening-and-security-guide/ 7 | 8 | if rgrep -q "password=.admin." /etc/tomcat*/tomcat-users.xml /var/lib/tomcat*/conf/tomcat-users.xml 2>/dev/null; then 9 | result_failed "Evil tomcat password detected!" 10 | else 11 | result_ok 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/security-unpatched-cve.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Security-CVEs 4 | # name: Unpatched CVEs 5 | # description: There should be no urgent CVEs unpatched for which there are Debian updates available. Requires debsecan. Also provides a count based inventory. 6 | # solution-cmd: apt-get install $(debsecan --only-fixed 2>&1 | grep "high urgency" | awk '{print $2}') 7 | 8 | if [ -f /usr/bin/debsecan ]; then 9 | # Note: explicitely pass --config and SOURCE= to workaround Debian #842428 10 | cves=$(/usr/bin/timeout -k 5 -s 9 4 /usr/bin/debsecan --only-fixed --config <(/bin/echo SOURCE="https://security-tracker.debian.org/tracker/debsecan/release/1/") --suite "$(lsb_release -cs)" 2>/dev/null |\ 11 | grep -v "obsolete" |\ 12 | awk '/high urgency/{print $1 ":" $2}' | sort -u) 13 | if [ "$cves" != "" ]; then 14 | result_failed "Unpatched CVE: " $cves 15 | else 16 | result_ok 17 | fi 18 | 19 | count=$(/bin/echo $cves | grep -v '^ *$' | wc -w) 20 | if [ "$count" -gt 100 ]; then 21 | count=$(( count / 100 * 100 )) 22 | elif [ "$count" -gt 10 ]; then 23 | count=$(( count / 10 * 10 )) 24 | fi 25 | result_inventory "Unpatched Count" $count 26 | else 27 | result_warning "debscan is not installed" 28 | fi 29 | -------------------------------------------------------------------------------- /lib/scanners/ssh-hashknownhosts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: SSH 4 | # name: HashKnownHosts 5 | # description: Checks /etc/ssh/ssh_config for HashKnownHosts yes 6 | 7 | if grep -q "HashKnownHosts yes" /etc/ssh/ssh_config 2>/dev/null; then 8 | result_ok 9 | else 10 | result_failed 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/ssh-key-inventory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: SSH 4 | # name: Inventory 5 | # description: Inventory only scanner fuzzy guessing SSH key equivalencies from authorized_keys comment field. Recognizes all non-revoked keys with @ in the comment field. Fuzzy matches host names to known FQDNs. 6 | 7 | fqdn=$(hostname -f) 8 | users=$(getent passwd | awk -F: '{if(($3 >= 1000) && ($3 < 65534)) { print $1 }}') 9 | for u in $users; do 10 | homedir=$(getent passwd "$u" | cut -d: -f 6) 11 | afile="${homedir}/.ssh/authorized_keys" 12 | if [ -f "${afile}" ]; then 13 | while read user host; do 14 | # Track once for inbound host 15 | result_network_edge "Key Equivalency" "$u" "$fqdn" high "$user@$host" high in 1 16 | 17 | # Track once for originating host 18 | result_network_edge "Key Equivalency" "$host" "$user@$host" high "$fqdn" high out 1 19 | done < <( 20 | grep -v "^@revoked" "$afile" | sed "s/.* //" | grep ".@..." | sed "s/@/ /" 21 | ) 22 | fi 23 | done 24 | -------------------------------------------------------------------------------- /lib/scanners/ssh-key-permissions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: SSH 4 | # name: Key Permissions 5 | # description: Checks for correct key permissions. This avoids having unusable/unprotected keys. 6 | 7 | error=0 8 | users=$(getent passwd | awk -F: '{if(($3 >= 1000) && ($3 < 65534)) { print $1 }}') 9 | for u in $users; do 10 | homedir=$(getent passwd $u | cut -d: -f 6) 11 | if [ -d "${homedir}/.ssh" ]; then 12 | if find "${homedir}" -name .ssh -perm "/g+w,o+w" -printf >/dev/null 2>&1; then 13 | result_failed "${homedir}/.ssh too open" 14 | error=1 15 | fi 16 | insecure_keys=$( 17 | find "${homedir}/.ssh" -regex ".*id_[a-z]*" -perm "/g+rwx,o+rwx" 18 | find "${homedir}/.ssh" -regex ".*id_[a-z]*.pub" -perm "/g+w,o+w" 19 | ) 20 | if [ "$insecure_keys" != "" ]; then 21 | result_failed "Insecure key file permissions:" $insecure_keys 22 | error=1 23 | fi 24 | fi 25 | done 26 | 27 | if [ $error -eq 0 ]; then 28 | result_ok 29 | fi 30 | -------------------------------------------------------------------------------- /lib/scanners/ssh-legacy-disabled.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: SSH 4 | # name: Legacy Options 5 | # description: Checks /etc/ssh/sshd_config for disabled legacy features: IgnoreRhosts yes, HostbasedAuthentication no, RhostsRSAAuthentication no, Protocol 2 (no 1). Use for older SSH versions only as these options are default e.g. in Jessie. 6 | # tags: CCE-27124-7 CCE-27091-8 CCE-26887-0 7 | 8 | if ! grep -q "IgnoreRhosts yes" /etc/ssh/sshd_config 2>/dev/null; then 9 | result_failed "IgnoreRhosts is not set to yes" 10 | fi 11 | 12 | if ! grep -q "HostbasedAuthentication no" /etc/ssh/sshd_config 2>/dev/null; then 13 | result_failed "HostbasedAuthentication is not set to no" 14 | fi 15 | 16 | if ! grep -q "PermitEmptyPasswords no" /etc/ssh/sshd_config 2>/dev/null; then 17 | result_failed "PermitEmptyPasswords is not set to no" 18 | fi 19 | 20 | if ! grep -q "RhostsRSAAuthentication no" /etc/ssh/sshd_config 2>/dev/null; then 21 | result_failed "RhostsRSAAuthentication is not set to no" 22 | fi 23 | 24 | if ! grep -q "Protocol 2" /etc/ssh/sshd_config 2>/dev/null; then 25 | result_failed "Protocol is not set to 2" 26 | fi 27 | -------------------------------------------------------------------------------- /lib/scanners/ssh-no-keyboard.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: SSH 4 | # name: No keyboard auth 5 | # description: /etc/ssh/sshd_config must not have enabled keyboard-interactive authentication 6 | 7 | if grep -q "^[[:space:]]*[^#]keyboard-interactive" /etc/ssh/sshd_config 2>/dev/null; then 8 | result_ok 9 | else 10 | result_failed "Found keyboard-interactive in /etc/ssh/sshd_config" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/ssh-no-root.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: SSH 4 | # name: PermitRootLogin no 5 | # description: Checks /etc/ssh/sshd_config for PermitRootLogin no 6 | # solution-cmd: sed -i 's/PermitRootLogin.*yes/PermitRootLogin no/' /etc/ssh/sshd_config 7 | 8 | if grep -q "PermitRootLogin no" /etc/ssh/sshd_config 2>/dev/null; then 9 | result_ok 10 | else 11 | result_failed 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/ssh-no-tcp-forward.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: SSH 4 | # name: AllowTcpForwarding no 5 | # description: Checks /etc/ssh/sshd_config for AllowTcpForwarding no 6 | 7 | if grep -q "AllowTcpForwarding no" /etc/ssh/sshd_config 2>/dev/null; then 8 | result_ok 9 | else 10 | result_failed 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/ssh-no-x11-forward.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: SSH 4 | # name: X11Forwarding no 5 | # description: Checks /etc/ssh/sshd_config for X11Forwarding yes 6 | # solution-cmd: sed -i 's/X11Forwarding.*yes/X11Forwarding no/' /etc/ssh/sshd_config 7 | 8 | if grep -q "X11Forwarding yes" /etc/ssh/sshd_config 2>/dev/null; then 9 | result_failed 10 | else 11 | result_ok 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/ssh-privilege-separation.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: SSH 4 | # name: UsePrivilegeSeparation 5 | # description: Checks /etc/ssh/sshd_config for UsePrivilegeSeparation no 6 | # solution-cmd: sed -i 's/UsePrivilegeSeparation.*no/UsePrivilegeSeparation yes/' /etc/ssh/sshd_config 7 | 8 | if grep -q "UsePrivilegeSeparation[[:space:]]no" /etc/ssh/sshd_config 2>/dev/null; then 9 | result_failed "UsePrivilegeSeparation is not yes" 10 | else 11 | result_ok 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/ssh-sftp-disabled.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: SSH 4 | # name: SFTP disabled 5 | # description: SFTP subsystem must not be enabled in /etc/ssh/sshd_config 6 | 7 | if grep -q "Subsystem[[:space:]]*sftp[[:space:]]*.*sftp-server" /etc/ssh/sshd_config 2>/dev/null; then 8 | result_failed "SFTP subsystem is enabled, but should not be!" 9 | fi 10 | -------------------------------------------------------------------------------- /lib/scanners/ssh-strict-mode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: SSH 4 | # name: Strict Modes enabled 5 | # description: Checks /etc/ssh/sshd_config for StrictModes no 6 | # solution-cmd: sed -i 's/StrictModes.*no/StrictModes yes/' /etc/ssh/sshd_config 7 | 8 | if grep -q "StrictModes no" /etc/ssh/sshd_config 2>/dev/null; then 9 | result_failed 10 | else 11 | result_ok 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/system-208day-kernel-freeze.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: 208 day freeze 5 | # description: Ensure we are not running an outdated buggy kernel. Enforce safe uptime <190 days 6 | # solution: Reboot the server before uptime reaches 208 days. 7 | # source: https://www.novell.com/support/kb/doc.php?id=7009834 8 | # source: http://www.claudiokuenzler.com/blog/247/linux-virtual-server-crash-update_cpu_power-kernel-uptime-bug#.VfvWTGWzQy8 9 | 10 | kernel_version=$(/bin/uname -r) 11 | if [[ $kernel_version =~ ^2.6.32- ]]; then 12 | patch_level=$(expr match "$kernel_version" '^2.6.32-\([0-9]\+\)') 13 | if [[ $patch_level -lt 41 ]]; then 14 | uptime_days=$(/usr/bin/uptime | sed 's/.* \([0-9][0-9]*\) days.*/\1/') 15 | if [ "$uptime_days" -lt 208 -a "$uptime_days" -gt 190 ]; then 16 | result_failed "Uptime is >190. Please reboot!" 17 | else 18 | result_warning "Buggy kernel active ($kernel_version). Upgrade to 2.6.32-41 or later!" 19 | fi 20 | fi 21 | fi 22 | 23 | -------------------------------------------------------------------------------- /lib/scanners/system-disk-space.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: Disk Space 5 | # description: Simple policy that ensure disks won't run full within the next 3 days using a 14 day interval of samples. Uses average growth in this interval to predict days to disk full. Warns if disk will be full in 7 days, critical when disk full in 3 days. 6 | 7 | FS_SPACE_HISTORY=${FS_SPACE_HISTORY-/tmp/.polscan_fs_space_history} 8 | FS_SPACE_HISTORY_INTERVAL=${FS_SPACE_HISTORY_INTERVAL-14} 9 | FS_SPACE_MIN_VALUE_COUNT=3 10 | FS_SPACE_WARNING_DAYS=7 11 | FS_SPACE_CRITICAL_DAYS=3 12 | 13 | today=$(date +%Y%m%d) 14 | old=$(date -d "$FS_SPACE_HISTORY_INTERVAL days ago" +%Y%m%d) 15 | 16 | # Read old values and add current ones 17 | values=$( 18 | ( 19 | while read mount day available; do 20 | if [ "$day" -ge $old -a "$day" -ne "$today" ]; then 21 | /bin/echo "$mount $day $available" 22 | fi 23 | done < <(tail -$(( $FS_SPACE_HISTORY_INTERVAL - 1 )) "$FS_SPACE_HISTORY") 24 | 25 | while read device fs blocks used available percent mount rest; do 26 | /bin/echo "$mount $today $available" 27 | done < <( 28 | /bin/df -lTm | /bin/egrep -v "tmpfs|Filesystem" 29 | ) 30 | ) | sort -u 31 | ) 32 | 33 | # Save new values 34 | /bin/echo "$values" >${FS_SPACE_HISTORY} 2>/dev/null 35 | 36 | # Calculate diffs 37 | prevMount= 38 | echo "$values 39 | " |\ 40 | while read mount day available rest; do 41 | if [ "$mount" == "$prevMount" -a "$day" != "$prevDay" ]; then 42 | diff=$(( $prevValue - $available )) 43 | # We are only interested in daily space increases 44 | if [ $diff -gt 0 ]; then 45 | diffsum=$(( $diffsum + $diff)) 46 | fi 47 | diffcount=$(( $diffcount + 1 )) 48 | else 49 | # Analyze 50 | if [ "$prevMount" != "" ]; then 51 | if [ "${diffcount-0}" -ge $FS_SPACE_MIN_VALUE_COUNT ]; then 52 | if [ ${diffsum-0} -gt 0 ]; then 53 | days_left=$(( $prevValue * $diffcount / $diffsum )) 54 | message="$prevMount is probably full in $days_left days (grows $(( $diffsum / $diffcount)) MiB/day)" 55 | if [ "$days_left" -le $FS_SPACE_CRITICAL_DAYS ]; then 56 | result_failed "$message" 57 | elif [ "$days_left" -le $FS_SPACE_WARNING_DAYS ]; then 58 | result_warning "$message" 59 | else 60 | result_ok "$message" 61 | fi 62 | else 63 | result_ok "$prevMount usage has no growth since $diffcount days" 64 | fi 65 | else 66 | result_ok "$prevMount usage unknown, not enough samples..." 67 | fi 68 | fi 69 | # Reset 70 | diffsum=0 71 | diffcount=0 72 | fi 73 | prevValue=$available 74 | prevMount=$mount 75 | prevDay=$day 76 | done 77 | -------------------------------------------------------------------------------- /lib/scanners/system-home-partition.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: /home Partition 5 | # description: /home is to be a separate partition 6 | # tags: CCE-26639-5 7 | 8 | if mount | grep -q " on /home "; then 9 | result_ok 10 | else 11 | result_failed "/home is not a mounted path" 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/system-hung-tasks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: No Hung Tasks 5 | # description: There shall be no 'blocked for more than xxx seconds' in dmesg 6 | # solution: Solution often depends on the application blocked, rarely on the HW. Clear dmesg buffer to acknowledge. 7 | 8 | logged=$(/bin/dmesg | grep 'blocked for more than [0-9]* seconds' | tail -10) 9 | if [ "$logged" == "" ]; then 10 | result_ok 11 | else 12 | result_warning "$logged" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/system-immutable-files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: Immutable Files 5 | # description: There must be no immutable files as they would block automation tools. 6 | 7 | files=$(lsattr -R / 2>/dev/null | grep -- "^[^/ ]*i" | cut -d " " -f 2) 8 | if [ "$files" == "" ]; then 9 | result_ok 10 | else 11 | result_failed "Immutable files found: $files" 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/system-inventory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: Inventory 5 | # description: Inventory only scanner determining kernel version with uname -r, locale, timezone, CPU+RAM GB count, OS release, RAID vendor, Uptime, NMI Watchdog Setting 6 | 7 | result_inventory "Kernel Version" "$(uname -r)" 8 | result_inventory "Default Locale" "$LANG" 9 | result_inventory "Timezone" "$(cat /etc/timezone 2>/dev/null)" 10 | result_inventory "Remote Mounts" "$(/bin/mount | awk '/:\//{print $1}' | xargs)" 11 | 12 | cpucount=$(grep -c processor /proc/cpuinfo) 13 | memory=$(grep MemTotal: /proc/meminfo | awk '{printf("%d\n", $2/1024/1024 + 0.5)}') 14 | 15 | result_inventory "CPU RAM" "${cpucount}x${memory}" 16 | 17 | result_inventory "OS Release" "$(/usr/bin/lsb_release -si)_$(/usr/bin/lsb_release -sr)" 18 | result_inventory "OS Major Release" "$(/usr/bin/lsb_release -si)_$(/usr/bin/lsb_release -sr | sed 's/\..*//')" 19 | 20 | # For RAID Vendor we rely on the superb Nagios check from Debian package 21 | # nagios-plugins-contrib which prints in format ": :[/dev/null | grep OK: | cut -d : -f 2)" 23 | 24 | # FIXME; switch to comma separated lists instead of space separated, 25 | # so we do not need to replace whitespaces here! 26 | result_inventory "Server Type" "$(/usr/sbin/dmidecode -s system-product-name | grep -v '^#' | head -1 | sed "s/ /_/g")" 27 | 28 | result_inventory "CPU Type" "$(/usr/sbin/dmidecode -s processor-version | grep -v "^#" | head -1 | sed "s/ /_/g")" 29 | 30 | # Slotted Uptime (7d,14d,31d,3m,6m,1y). This can help spotting issues per HW type/age/server role. 31 | days=$(( $(cut -d . -f 1 /proc/uptime) / 86400)) 32 | if [ "$days" != "" ]; then 33 | prev=0 34 | result= 35 | for d in 7 14 31 121 182 365; do 36 | if [ "$days" -lt $d ]; then 37 | result=$prev 38 | break; 39 | fi 40 | prev=$d 41 | done 42 | 43 | if [ "$result" != "" ]; then 44 | result_inventory "Uptime" $result 45 | else 46 | result_inventory "Uptime" 365 47 | fi 48 | fi 49 | 50 | result_inventory "NMI Watchdog" "$(cat /proc/sys/kernel/nmi_watchdog)" 51 | result_inventory "Clock Source" "$(cat /sys/devices/system/clocksource/clocksource0/current_clocksource 2>/dev/null)" 52 | result_inventory "Block IO Scheduler" $(grep -vh "^none" /sys/block/*/queue/scheduler 2>/dev/null | sed 's/.*\[\(.*\)\].*/\1/' | sort -u) 53 | -------------------------------------------------------------------------------- /lib/scanners/system-mce-logged.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: No MCE logged 5 | # description: There shall be no 'Machine check events' in /var/log/dmesg as they can indicate real hardware issues 6 | # solution: There is no real solution. But 'apt-get install mcelog && mcelog' can help to diagnose it. Clear dmesg buffer to acknowledge. 7 | # source: http://serverfault.com/questions/430005/machine-check-events-logged 8 | 9 | logged=$(/bin/dmesg | grep 'Machine check events logged' | tail -10) 10 | if [ "$logged" == "" ]; then 11 | result_ok 12 | else 13 | result_failed "dmesg has 'Machine check events logged' entries" 14 | fi 15 | -------------------------------------------------------------------------------- /lib/scanners/system-mounts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: Persistent Mounts 5 | # description: All mount points need to be persistent. 6 | 7 | declare -a fstab 8 | while read mountpoint; do 9 | fstab[m_${mountpoint//[^a-zA-Z0-9]/_}]=1 10 | done < <( 11 | grep -v '^#' /etc/fstab | awk '{print $2}' 12 | ) 13 | 14 | mountpoints=$(mount | egrep -v '(proc|type rpc|type tmpfs|fuse|on /dev|on /sys|on /run|on /cgroup)' | awk '{if($2 == "on") {print $3}}') 15 | non_persistent= 16 | 17 | for m in $mountpoints 18 | do 19 | if [[ ${fstab[m_${m//[^a-zA-Z0-9]/_}]} != 1 ]]; then 20 | non_persistent="$non_persistent $m" 21 | fi 22 | done 23 | 24 | if [ "$non_persistent" == "" ]; then 25 | result_ok 26 | else 27 | result_failed "Non-persistent mount points found: $non_persistent" 28 | fi 29 | -------------------------------------------------------------------------------- /lib/scanners/system-no-local-root-mail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: No local root mail 5 | # description: There should be no local root mail. Policy checks wether /var/mail/root is less than 1kByte 6 | # solution: Configure a mail alias for root to forward mails 7 | 8 | if [[ $(/usr/bin/find /var/mail -size +1k -name root >/dev/null 2>&1) != "" ]]; then 9 | result_failed "/var/mail/root contains >1kB mails" 10 | else 11 | result_ok 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /lib/scanners/system-no-oom-panic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: No Panic on OOM 5 | # description: /proc/sys/vm/panic_on_oom is to be 0. /proc/sys/vm/overcommit_memory is to be 2. This is usually NOT a good idea because an OOM situation causes undefined behaviour. 6 | # solution-cmd: echo 'vm.panic_on_oom = 0' >/etc/sysctl.d/50-vm.panic_on_oom.conf; echo 'vm.overcommit_memory = 2' >/etc/sysctl.d/50-vm.overcommit_memory.conf; sysctl -p 7 | # source: http://www.oracle.com/technetwork/articles/servers-storage-dev/oom-killer-1911807.html 8 | # source: http://www.debuntu.org/how-to-reboot-on-oom/ 9 | 10 | if [[ $(/sbin/sysctl -n vm.panic_on_oom 2>/dev/null) != "0" ]]; then 11 | result_failed "sysctl vm.panic_on_oom != 0" 12 | fi 13 | if [[ $(/sbin/sysctl -n vm.overcommit_memory 2>/dev/null) != "2" ]]; then 14 | result_failed "sysctl vm.overcommit_memory != 2" 15 | fi 16 | 17 | -------------------------------------------------------------------------------- /lib/scanners/system-ntpd-slew.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: NTPd with -x 5 | # description: Checks ntpd is running with -x option. This is useful to survive leap second effects. 6 | 7 | if pgrep -f "ntpd.*-x" >/dev/null; then 8 | result_ok 9 | else 10 | result_failed "ntpd not running with -x option" 11 | fi 12 | -------------------------------------------------------------------------------- /lib/scanners/system-oom-logged.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: OOM logged 5 | # description: There shall be no 'Out of memory: kill process' in /var/log/dmesg as they usually indicate insufficient memory or memory leaks. 6 | 7 | logged=$(/bin/dmesg | grep 'Out of memory: kill process' | tail -10) 8 | if [ "$logged" == "" ]; then 9 | result_ok 10 | else 11 | result_failed "dmesg has 'Out of memory: kill process' entries: $logged" 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/system-oom-panic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: Panic on OOM 5 | # description: /proc/sys/vm/panic_on_oom is to be 1. /proc/sys/kernel.panic is to be > 0. This prevents undefined service behaviour on OOM 6 | # solution-cmd: echo 'vm.panic_on_oom = 1' >/etc/sysctl.d/50-vm.panic_on_oom.conf; sysctl -p; echo 'kernel.panic = 1' >/etc/sysctl.d/50-kernel.panic.conf; sysctl -p 7 | 8 | if [[ $(/sbin/sysctl -n vm.panic_on_oom 2>/dev/null) != "1" ]]; then 9 | result_failed "sysctl vm.panic_on_oom != 1" 10 | fi 11 | if [[ $(/sbin/sysctl -n kernel.panic 2>/dev/null) != "0" ]]; then 12 | result_failed "sysctl kernel.panic != 0" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/system-raid-state.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: RAID State 5 | # description: There should be no RAID errors. Uses Nagios plugin check_raid from Debian package nagios-plugins-contrib to check RAID errors. 6 | 7 | if [ -x /usr/lib/nagios/plugins/check_raid ]; then 8 | output=$(/usr/lib/nagios/plugins/check_raid) 9 | case $? in 10 | 1) result_warning "RAID State" "$output" ;; 11 | 2) result_failed "RAID State" "$output" ;; 12 | 0) result_ok "RAID State" "$output";; 13 | esac 14 | fi 15 | -------------------------------------------------------------------------------- /lib/scanners/system-readonly-fs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: Readonly FS 5 | # description: There should be no read-only local filesystems 6 | # source: http://serverfault.com/questions/193971/determine-if-filesystem-or-partition-is-mounted-ro-or-rw-via-bash-script 7 | 8 | mounts=$(egrep "^/.*( ro,|,ro )" /proc/mounts | grep -v iso9660 | cut -d " " -f 2) 9 | if [ "$mounts" == "" ]; then 10 | result_ok 11 | else 12 | result_warning "Read-only mount points found: $mounts" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/system-segfaults.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: No Segfaults 5 | # description: There shall be no 'segfault at ' messages in dmesg 6 | # solution: Solution always depends on the application. Clear dmesg buffer to acknowledge. 7 | 8 | logged=$(/bin/dmesg | grep ': segfault at ' | tail -10) 9 | if [ "$logged" == "" ]; then 10 | result_ok 11 | else 12 | result_warning "$logged" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/system-sysctl-settings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: sysctl Settings 5 | # description: sysctl settings should be applied and persistent. That means all settings in /etc/sysctl.conf /etc/sysctl.d/*.conf must be active. This is important because network settings are not safely applied in Debian Squeeze by the procps script (running to early in the boot order). 6 | # solution-cmd: /sbin/sysctl -p 7 | # source: http://serverfault.com/a/494899 8 | # source: https://bugs.launchpad.net/ubuntu/+source/procps/+bug/50093 9 | # source: http://wiki.debian.org/BridgeNetworkConnections 10 | 11 | all=$(/sbin/sysctl -a 2>/dev/null) 12 | results=$( 13 | while read -r m; do 14 | printf "%s\n" "$all" | grep -q "$m" 15 | done < <( 16 | /bin/egrep -vh "^ *#|^ *$" /etc/sysctl.conf /etc/sysctl.d/*.conf 2>/dev/null | sed "s/ *= */ = /" 2>/dev/null 17 | ) | grep -v '^$' 18 | ) 19 | 20 | if [ "$results" == "" ]; then 21 | result_ok 22 | else 23 | result_failed "The following sysctl settings are not active: $results" 24 | fi 25 | -------------------------------------------------------------------------------- /lib/scanners/system-tmp-overflow.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: Overflow tmp 5 | # description: There should be no overflow mounted /tmp. This happens when you have no /tmp partition and / runs full. In this case the kernel mounts an 1MB /tmp partition which is unusable in normal operation. 6 | # solution-cmd: umount overflow 7 | # source: http://jarrodoverson.com/blog/overflow-filesystem-in-linux/ 8 | # source: http://stackoverflow.com/questions/17536139/tmp-mounted-with-only-1mb-space-100-used-as-filesystem-overflow 9 | 10 | if grep -q "^overflow.*/tmp" /proc/mounts; then 11 | result_failed "/tmp is overflow mounted!" 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/system-tmp-partition.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: /tmp Partition 5 | # description: /tmp is to be a separate partition 6 | # tags: CCE-26435-8 7 | 8 | if mount | grep -q " on /tmp "; then 9 | result_ok 10 | else 11 | result_failed "/tmp is not a mounted path" 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/system-unattended-upgrades.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: Unattended Upgrades 5 | # description: The Debian unattended-upgrades packages is installed and activated 6 | 7 | if ! dpkg -l unattended-upgrades 2>/dev/null | grep -q '^ii'; then 8 | result_failed "Package unattended-upgrades is not installed" 9 | else 10 | # Check for the config lines activating auto update of package list 11 | # and the upgrade itself 12 | patterns=' 13 | ^APT::Periodic::Update-Package-Lists "[^0] 14 | ^APT::Periodic::Unattended-Upgrade "[^0] 15 | ' 16 | for pattern in $patterns 17 | do 18 | if ! rgrep -q "$pattern" /etc/apt/apt.conf.d/; then 19 | result_failed "Pattern >>>$pattern<<< not found /etc/apt/apt.conf.d/" 20 | fi 21 | done 22 | fi 23 | -------------------------------------------------------------------------------- /lib/scanners/system-usb-drives.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: USB Drives 5 | # description: On server systems USB disk devices are not normal. So you might want to know about all of them. 6 | 7 | disks=$(ls /dev/disk/by-id/usb* 2>/dev/null) 8 | if [ "$disks" == "" ]; then 9 | result_ok 10 | else 11 | result_warning "USB disk devices found: $disks" 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/system-usb-keyboard.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: USB Keyboard 5 | # description: On server systems USB keyboards are not normal. So you might want to know when on is attached 6 | 7 | # Example: Oct 29 11:28:23 srv1 kernel: [59270631.727430] generic-usb 0003:0624:0248.000B: input,hidraw0: USB HID v1.00 Keyboard [Avocent USB Composite Device-0] on usb-0000:00:12.0-2/input0 8 | output=$(/bin/dmesg 2>/dev/null| grep -i "generic-usb:.*USB.*HID.*Keyboard") 9 | if [ "$output" == "" ]; then 10 | result_ok 11 | else 12 | result_warning "USB keyboard found: $output" 13 | fi 14 | -------------------------------------------------------------------------------- /lib/scanners/system-var-partition.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: /var Partition 5 | # description: /var is to be a separate partition 6 | # tags: CCE-26639-5 7 | 8 | if mount | grep -q " on /var "; then 9 | result_ok 10 | else 11 | result_failed "/var is not a mounted path" 12 | fi 13 | -------------------------------------------------------------------------------- /lib/scanners/systemd-no-failed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Systemd 4 | # name: No failed services 5 | # description: Ensure there are no failed services or units in maintenance 6 | 7 | output=$(systemctl list-units 2>/dev/null) 8 | if [ "$output" != "" ]; then 9 | failed=$(/bin/echo "$output" | egrep " loaded (maintenance|failed) " | cut -d " " -f2) 10 | if [ "$failed" != "" ]; then 11 | result_warning "There are failed services: " $failed 12 | else 13 | result_ok 14 | fi 15 | fi 16 | -------------------------------------------------------------------------------- /lib/scanners/systemd-no-masked.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Systemd 4 | # name: No masked services 5 | # description: Ensure there are no masked services 6 | 7 | output=$(systemctl list-units 2>/dev/null) 8 | if [ "$output" != "" ]; then 9 | masked=$(/bin/echo "$output" | grep " masked " | cut -d " " -f2) 10 | if [ "$masked" != "" ]; then 11 | result_warning "There are masked services: " $masked 12 | else 13 | result_ok 14 | fi 15 | fi 16 | -------------------------------------------------------------------------------- /lib/scanners/updates-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Updates 4 | # name: Distro Release 5 | # description: Checks if distribution is still ok 6 | 7 | vendor=$(lsb_release -si) 8 | release=$(lsb_release -sr) 9 | release=${release/\.*/} 10 | 11 | case $vendor in 12 | Debian) 13 | if [ "${release-0}" -lt 7 ]; then 14 | result_warning "$vendor $release needs to be updated" 15 | fi 16 | ;; 17 | esac 18 | -------------------------------------------------------------------------------- /lib/scanners/virtualization-inventory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Virtualization 4 | # name: Inventory 5 | # description: Inventory only scanner virtualization instance count (containers, VMs) and type (KVM, LXC, docker...) 6 | 7 | vtype= 8 | while [ 1 ]; do 9 | # Check KVM/libvirt 10 | if virsh -version >/dev/null 2>&1; then 11 | # Do not use virsh list --name to support older versions 12 | instances=$(virsh list | egrep -c -v "^$|^---|State") 13 | vtype=KVM 14 | break 15 | fi 16 | 17 | # Check docker 18 | if docker version >/dev/null 2>&1; then 19 | instances=$(docker info 2>/dev/null | grep Images | sed 's/.*\://') 20 | vtype=Docker 21 | break 22 | fi 23 | 24 | # Check LXC 25 | out=$(lxc-ls >/dev/null 2>&1) 26 | if [ "$out" != "" ]; then 27 | instances=$(echo "$out" | wc -l) 28 | vtype=LXC 29 | break 30 | fi 31 | 32 | break 33 | done 34 | 35 | if [ "$vtype" != "" ]; then 36 | result_inventory "Type" "$vtype" 37 | result_inventory "Count" "$instances" 38 | fi 39 | -------------------------------------------------------------------------------- /lib/standalone/kubernetes-helm2-status.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Kubernetes 4 | # name: Helm Status 5 | # description: Checks for broken Helm releases. 6 | # solution: Perform 'helm rollback' on broken release 7 | 8 | source $(dirname $0)/../scanner-functions.inc 9 | 10 | k8s_helm_releases() { 11 | # As polscan doesn't support cluster results yet, we report 12 | # all findings against the first master node 13 | HOST=$(kubectl get nodes --selector='node-role.kubernetes.io/master' -o json | jq -r '.items[].metadata.name' | head -1) 14 | 15 | HELM_RELEASES=$(timeout -k 20 -s 9 15 helm ls) 16 | 17 | # Check for output header 18 | if [[ ! "$HELM_RELEASES" =~ REVISION ]]; then 19 | echo "$HOST Kubernetes FAILED |||Helm Status||| Failed to 'helm ls' ($HELM_RELEASES)" 20 | return 21 | fi 22 | 23 | failed=$(echo "$HELM_RELEASES" | grep FAILED | cut -f 1) 24 | if [ "$failed" != "" ]; then 25 | echo "$HOST Kubernetes FAILED |||Helm Status||| failed releases: $failed" 26 | else 27 | echo "$HOST Kubernetes OK |||Helm Status||| All fine." 28 | fi 29 | } 30 | 31 | 32 | if helm version -c 2>/dev/null | grep -q "SemVer.*v2"; then 33 | foreach_kube_context k8s_helm_releases 34 | fi 35 | -------------------------------------------------------------------------------- /lib/standalone/kubernetes-kube-bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Kubernetes 4 | # name: kube-bench 5 | # description: Check all namespaces for kube-bench crons kube-bench-worker and kube-bench-master, to see if they are run daily and if it reports any problems 6 | # reference: https://github.com/aquasecurity/kube-bench#running-in-a-kubernetes-cluster 7 | 8 | source $(dirname $0)/../scanner-functions.inc 9 | 10 | k8s_kube_bench() { 11 | # As polscan doesn't support cluster results yet, we report 12 | # all findings against the first master node 13 | HOST=$(kubectl get nodes --selector='node-role.kubernetes.io/master' -o json | jq -r '.items[].metadata.name' | head -1) 14 | 15 | # kube-bench documentation says to run two crons, one for workers, one 16 | # for master nodes. We check the result pod if it ran and its log output 17 | for type in master node; do 18 | # How to get pods older than (adapted to crons) 19 | # https://stackoverflow.com/questions/48934491/kubernetes-how-to-delete-pods-based-on-time-age-creation 20 | CRON_NS=$(timeout -k 10 -s 9 5 kubectl get pods -A -o go-template --template '{{range .items}}{{.metadata.name}} {{.metadata.namespace}} {{.metadata.creationTimestamp}}{{"\n"}}{{end}}' | awk '$3 >= "'$(date -d '24 hours ago' -Ins --utc | sed 's/+0000/Z/')'" { print $1,$2 }' | grep "kube-bench-$type" | head -1) 21 | CRON=${CRON_NS/ */} 22 | NS=${CRON_NS/* /} 23 | 24 | if [[ $CRON_NS = "" ]]; then 25 | echo "$HOST Kubernetes FAILED |||kube-bench||| No kube-bench-$type cron executed since yesterday!" 26 | else 27 | # Get cron output, filter all result lines that should start with [ 28 | # and drop all INFO and PASS lines, report the rest 29 | output=$(timeout -k 10 -s 9 5 kubectl logs "$CRON" -n "$NS" | grep "^\[" | egrep -v "^\[(PASS|INFO)\]") 30 | echo "$output" | while read -r severity line; do 31 | polscan_severity="FAILED" 32 | if [ "$severity" = "[WARN]" ]; then 33 | polscan_severity="WARNING" 34 | fi 35 | echo "$HOST Kubernetes $polscan_severity |||kube-bench||| $severity $line" 36 | done 37 | 38 | if [ "$output" = "" ]; then 39 | echo "$HOST Kubernetes OK |||kube-bench||| All fine no problems reported." 40 | fi 41 | fi 42 | done 43 | } 44 | 45 | foreach_kube_context k8s_kube_bench 46 | -------------------------------------------------------------------------------- /lib/standalone/kubernetes-kube2iam-allowed-roles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # group: Kubernetes 3 | # name: kube2iam 4 | # description: Check all namespaces for usage of kube2iam. Checks if kube2iam using namespace have a filter for allowed IAM roles. 5 | # solution: Create a "iam.amazonaws.com/allowed-roles" annotation 6 | # reference: https://github.com/jtblin/kube2iam#namespace-restrictions 7 | 8 | source $(dirname $0)/../scanner-functions.inc 9 | 10 | k8s_kube2iam_filter() { 11 | # As polscan doesn't support cluster results yet, we report 12 | # all findings against the first master node 13 | HOST=$(kubectl get nodes --selector='node-role.kubernetes.io/master' -o json | jq -r '.items[].metadata.name' | head -1) 14 | 15 | # Get list of namespace holding pods with kube2iam annotations 16 | K2IAM_NS=$(json "$(kubectl get pods -A -o json)" '.items[] | select(.metadata.annotations["iam.amazonaws.com/role"] != null) | .metadata.namespace' | uniq) 17 | 18 | # Get list of all namespaces with a kube2iam filter annotation 19 | K2IAM_FILTERED_NS=$(json "$(kubectl get ns -A -o json)" '.items[] | select(.metadata.annotations["iam.amazonaws.com/allowed-roles"] != null) | .metadata.name') 20 | 21 | # Diff both lists... 22 | missing=$(diff -u <(echo "$K2IAM_NS") <(echo "$KIAM_FILTERED_NS") | grep "^-") 23 | if [ "$missing" != "" ]; then 24 | echo "$HOST Kubernetes OK |||kube2iam||| Namespaces with missing kube2iam role filter annotation: $missing" 25 | else 26 | echo "$HOST Kubernetes OK |||kube2iam||| Found $(echo "$K2IAM_NS" | wc -l) namespaces using kube2iam and no problems" 27 | fi 28 | } 29 | 30 | foreach_kube_context k8s_kube2iam_filter 31 | -------------------------------------------------------------------------------- /lib/standalone/kubernetes-node-inventory.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Kubernetes 4 | # name: Inventory 5 | # description: Inventory for kubernetes nodes 6 | 7 | source $(dirname $0)/../scanner-functions.inc 8 | 9 | k8s_node_size() { 10 | JSON=$(timeout -k 10 -s 9 5 kubectl get nodes -o json) 11 | NODES=$(json "$JSON" '.items[].metadata.name') 12 | for n in $NODES; do 13 | /bin/echo "Processing $n" >&2 14 | cpucount=$(json "$JSON" '.items[] | select(.metadata.name == "'$n'") | .status.capacity.cpu') 15 | memory=$(json "$JSON" '.items[] | select(.metadata.name == "'$n'") | .status.capacity.memory') 16 | ( 17 | echo "|||CPU RAM|||" "${cpucount}x${memory}" 18 | echo "|||OS Release|||" "$(json "$JSON" '.items[] | select(.metadata.name == "'$n'") | .status.nodeInfo.osImage' | sed "s/ /_/g")" 19 | echo "|||kubelet Version|||" "$(json "$JSON" '.items[] | select(.metadata.name == "'$n'") | .status.nodeInfo.kubeletVersion')" 20 | echo "|||Container Runtime|||" "$(json "$JSON" '.items[] | select(.metadata.name == "'$n'") | .status.nodeInfo.containerRuntimeVersion')" 21 | echo "|||Kernel Version|||" "$(json "$JSON" '.items[] | select(.metadata.name == "'$n'") | .status.nodeInfo.kernelVersion')" 22 | echo "|||Region|||" "$(json "$JSON" '.items[] | select(.metadata.name == "'$n'") | .metadata.labels["failure-domain.beta.kubernetes.io/region"]')" 23 | echo "|||Zone|||" "$(json "$JSON" '.items[] | select(.metadata.name == "'$n'") | .metadata.labels["failure-domain.beta.kubernetes.io/zone"]')" 24 | echo "|||Instance Type|||" "$(json "$JSON" '.items[] | select(.metadata.name == "'$n'") | .metadata.labels["beta.kubernetes.io/instance-type"]')" 25 | ) | sed "s/^/$n Kubernetes INVENTORY /" 26 | done 27 | } 28 | 29 | foreach_kube_context k8s_node_size 30 | -------------------------------------------------------------------------------- /lib/standalone/mcollective-facter2-inventory-disk-devices.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: Disk Devices 5 | # description: Inventory only scanner determining disk devices via facter 2. Probably won't work with facter 3+ 6 | 7 | mco inventory --script <(/bin/echo " 8 | inventory do 9 | format '%s System INVENTORY |||Disk Devices||| %s' 10 | fields { [ identity, facts['disks']] } 11 | end 12 | ") 2>/dev/null | grep -v '\|\|\| $' 13 | -------------------------------------------------------------------------------- /lib/standalone/mcollective-facter2-inventory-nic-names.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: System 4 | # name: NIC Names 5 | # description: Inventory only scanner determining NIC names via facter 2. Probably won't work with facter 3+ 6 | 7 | mco inventory --script <(/bin/echo " 8 | inventory do 9 | format '%s System INVENTORY |||NIC Names||| %s' 10 | fields { [ identity, facts['interfaces']] } 11 | end 12 | ") 2>/dev/null | sed "s/,/ /g;s/ lo//" | grep -v '\|\|\| $' 13 | -------------------------------------------------------------------------------- /lib/standalone/network-rbl-listings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: RBL Listings 5 | # description: Ensures that no known external IP is DNS blacklisted. Requires the 'Network' inventory scanner for a list of external IPs. 6 | 7 | NETWORK_DEFAULT_RBL_LIST=" 8 | cbl.abuseat.org 9 | bl.spamcop.net 10 | ix.dnsbl.manitu.net 11 | zen.spamhaus.org 12 | b.barracudacentral.org 13 | " 14 | NETWORK_RBL_LOOKUP_RATE=${NETWORK_RBL_LOOKUP_RATE-25} 15 | NETWORK_RBL_LIST=${RBL_LIST-$NETWORK_DEFAULT_RBL_LIST} 16 | 17 | ips=$(rgrep 'External IPs' "$RESULT_DIR/" | sed "s/.*|||//" | xargs | sort -u) 18 | 19 | count=0 20 | failed=0 21 | for i in $ips; do 22 | ri=$(echo ${i/\/*} | awk -F. '{for(i=NF;i>0;--i)printf "%s.",$i}') 23 | results= 24 | for r in $NETWORK_RBL_LIST; do 25 | count=$(( count + 1 )) 26 | if [ $count -gt "$NETWORK_RBL_LOOKUP_RATE" ]; then 27 | count=0 28 | sleep 1 29 | fi 30 | if [[ $(dig +short "${ri}${r}") != "" ]]; then 31 | results="${results}$r " 32 | fi 33 | done 34 | 35 | if [ "$results" != "" ]; then 36 | echo "Global Network FAILED |||RBL Listings||| DNS black listing of $i: $results" 37 | failed=1 38 | fi 39 | done 40 | 41 | if [ $failed -eq 0 ]; then 42 | echo "Global Network OK |||RBL Listings||| All external IPs are fine with RBLs: $NETWORK_RBL_LIST" 43 | fi 44 | -------------------------------------------------------------------------------- /lib/standalone/network-reverse-lookup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # group: Network 4 | # name: Host Reverse Lookup 5 | # description: Ensures that all hosts have reverse lookup properly configured 6 | 7 | for h in $HOST_LIST; do 8 | h=${h/*@/} 9 | ip=$(dig +short $h) 10 | if [ "$ip" != "" ]; then 11 | reverse=$(dig +short -x $ip) 12 | if ! echo "${reverse}" | grep -q "^${h}\."; then 13 | echo "$h Network FAILED |||Host Reverse Lookup||| Reverse lookup failed. Unexpected result '$reverse'." 14 | else 15 | echo "$h Network OK |||Host Reverse Lookup||| Reverse lookup is ok" 16 | fi 17 | fi 18 | done 19 | -------------------------------------------------------------------------------- /lib/target-providers/kubernetes-contexts: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # name: kubernetes-contexts 4 | # type: kubectxt 5 | # description: Provide a list of kubernetes contexts that we scan 6 | 7 | # Dirty kubectl context listing 8 | ( 9 | kubectl config view | grep -A5 "^- context:" |grep " *name:" | sed "s/ *name: //" 2>/dev/null 10 | ) | sed "s/^/K8S /" 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polscan-backend", 3 | "main": "main.js", 4 | "description": "Polscan backend to perform live queries against monitoring APIs and remote connection stats", 5 | "license": "GPL-3.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/lwindolf/polscan.git" 9 | }, 10 | "dependencies": { 11 | "chart.js": "^3.2.1", 12 | "d3": "^6.7.0", 13 | "d3-scale": "^3.3.0", 14 | "dagre": "^0.8.5", 15 | "dagre-d3": "^0.6.4", 16 | "express": ">=4.5.0", 17 | "handlebars": "^4.7.7", 18 | "jquery": "^3.6.0", 19 | "jquery-sparkline": "^2.4.0", 20 | "jquery-ui": "^1.10.5", 21 | "pixi.js": "^6.0.4", 22 | "promise": "~8.0.0", 23 | "split.js": "^1.6.4", 24 | "stateful-process-command-proxy": "~1.0.1", 25 | "tablesorter": "^2.31.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/host-group-providers/Domain.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | 4 | # "Domain" is a host name extractor, so we test some extractions 5 | @test "domain example.com" { 6 | echo "example.com" >$RESULT_DIR/.hosts 7 | output=$($BASE/lib/host-group-providers/Domain) 8 | [ "$output" == "Domain::com example.com" ] 9 | } 10 | 11 | @test "subdomain www.example.com" { 12 | echo "www.example.com" >$RESULT_DIR/.hosts 13 | output=$($BASE/lib/host-group-providers/Domain) 14 | [ "$output" == "Domain::example.com www.example.com" ] 15 | } 16 | 17 | @test "multiple hosts" { 18 | echo "www1.example.com www2.example.com" >$RESULT_DIR/.hosts 19 | output=$($BASE/lib/host-group-providers/Domain) 20 | [ "$output" == "Domain::example.com www1.example.com 21 | Domain::example.com www2.example.com" ] 22 | } 23 | 24 | @test "invalid hosts" { 25 | echo "www1 11" >$RESULT_DIR/.hosts 26 | output=$($BASE/lib/host-group-providers/Domain) 27 | [ "$output" == "" ] 28 | } 29 | 30 | -------------------------------------------------------------------------------- /tests/host-group-providers/Subdomain-Prefix.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | 4 | # "Subdomain-Prefix" is a host name extractor, so we test some extractions 5 | @test "domain example.com" { 6 | echo "example.com" >$RESULT_DIR/.hosts 7 | output=$($BASE/lib/host-group-providers/Subdomain-Prefix) 8 | [ "$output" == "Subdomain-Prefix::com example.com" ] 9 | } 10 | 11 | @test "subdomain www.example.com" { 12 | echo "www.example.com" >$RESULT_DIR/.hosts 13 | output=$($BASE/lib/host-group-providers/Subdomain-Prefix) 14 | [ "$output" == "Subdomain-Prefix::example www.example.com" ] 15 | } 16 | 17 | @test "multiple hosts" { 18 | echo "www1.example.com www2.example.com" >$RESULT_DIR/.hosts 19 | output=$($BASE/lib/host-group-providers/Subdomain-Prefix) 20 | [ "$output" == "Subdomain-Prefix::example www1.example.com 21 | Subdomain-Prefix::example www2.example.com" ] 22 | } 23 | 24 | @test "invalid hosts" { 25 | echo "www1 11" >$RESULT_DIR/.hosts 26 | output=$($BASE/lib/host-group-providers/Subdomain-Prefix) 27 | [ "$output" == "" ] 28 | } 29 | 30 | -------------------------------------------------------------------------------- /tests/host-group-providers/auto_detect.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | 4 | # We test the auto_detect provider by checking wether he returns 5 | # results from Domain and Subdomain-Prefix by passing some hosts via 6 | # $RESULT_DIR/.hosts 7 | 8 | @test "multiple hosts from Domain and Subdomain-Prefix" { 9 | echo "www1.example.com www2.example.com" >$RESULT_DIR/.hosts 10 | output=$($BASE/lib/host-group-providers/auto_detect) 11 | [[ "$output" =~ Domain::example.com.www1.example.com ]] && \ 12 | [[ "$output" =~ Domain::example.com.www2.example.com ]] && \ 13 | [[ "$output" =~ Subdomain-Prefix::example.www1.example.com ]] && \ 14 | [[ "$output" =~ Subdomain-Prefix::example.www2.example.com ]] 15 | } 16 | -------------------------------------------------------------------------------- /tests/run.sh: -------------------------------------------------------------------------------- 1 | export BASE="../" 2 | export RESULT_DIR=$(mktemp -d) 3 | err=0 4 | for t in */*.sh; do 5 | printf "\n%s\n" "$t" 6 | if ! bats "$t"; then 7 | err=1 8 | fi 9 | done 10 | rm -rf ${RESULT_DIR} 11 | exit $err 12 | -------------------------------------------------------------------------------- /www/api.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | /* Helper function to run backend API calls */ 3 | 4 | function getAPI(name, callback, errorcb, params) { 5 | if(undefined !== params) { 6 | name += "?" + params.map(function(obj) { 7 | var k = Object.keys(obj)[0]; 8 | return k + "=" + obj[k]; 9 | }).join("&"); 10 | } 11 | 12 | return $.getJSON("/api/" + name, {}) 13 | .done(function(data) { 14 | callback(data); 15 | }) 16 | .fail(function(j, t, e) { 17 | if(errorcb) 18 | errorcb(e); 19 | else 20 | error('Fetching API results for "'+name+'" failed!?

Exception: ' + e); 21 | }).promise(); 22 | } 23 | -------------------------------------------------------------------------------- /www/fonts/Muli-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/fonts/Muli-Bold.ttf -------------------------------------------------------------------------------- /www/fonts/Muli-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/fonts/Muli-Regular.ttf -------------------------------------------------------------------------------- /www/fonts/Muli-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/fonts/Muli-SemiBold.ttf -------------------------------------------------------------------------------- /www/group_list.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | 3 | /* Helper function to present a list of finding groups to choose from 4 | * 5 | * @param id: element to attach list to 6 | * @param target: view name for click target 7 | */ 8 | 9 | function group_list(id, target) { 10 | $(id).append('
'); 11 | getData("overview", function(data) { 12 | $.each(data.overview.sort(function(a,b) { 13 | if(!a.group || !b.group) 14 | return 0; 15 | if(a.FAILED !== b.FAILED) 16 | return b.FAILED - a.FAILED; 17 | return b.WARNING - a.WARNING; 18 | }), function(i, d) { 19 | if(!d.group) 20 | return; 21 | $('#group_list').append(render('group_list_item', d)); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /www/img/cog_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/cog_icon.png -------------------------------------------------------------------------------- /www/img/failed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/img/grips-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/grips-horizontal.png -------------------------------------------------------------------------------- /www/img/grips-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/grips-vertical.png -------------------------------------------------------------------------------- /www/img/home_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/home_icon.png -------------------------------------------------------------------------------- /www/img/hostmap_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/hostmap_icon.png -------------------------------------------------------------------------------- /www/img/itable_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/itable_icon.png -------------------------------------------------------------------------------- /www/img/lightning_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/lightning_icon.png -------------------------------------------------------------------------------- /www/img/net_comp_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/net_comp_icon.png -------------------------------------------------------------------------------- /www/img/netedge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/netedge.png -------------------------------------------------------------------------------- /www/img/netmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/netmap.png -------------------------------------------------------------------------------- /www/img/netmap_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/netmap_icon.png -------------------------------------------------------------------------------- /www/img/netrad_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/netrad_icon.png -------------------------------------------------------------------------------- /www/img/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/network.png -------------------------------------------------------------------------------- /www/img/ok.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/img/server_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/server_icon.png -------------------------------------------------------------------------------- /www/img/servicemap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/servicemap.png -------------------------------------------------------------------------------- /www/img/table_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/table_icon.png -------------------------------------------------------------------------------- /www/img/treemap_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/treemap_icon.png -------------------------------------------------------------------------------- /www/img/vtable_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/vtable_icon.png -------------------------------------------------------------------------------- /www/img/warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/img/zoom_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwindolf/polscan/8174428af6b37950bf65681b7bb66eaf399abbfd/www/img/zoom_icon.png -------------------------------------------------------------------------------- /www/pie_chart.js: -------------------------------------------------------------------------------- 1 | function addPieChart(id, title, size, pColor, data) { 2 | return new d3pie(id, { 3 | "header": { 4 | "title": { 5 | "text": title, 6 | "color": "#fff", 7 | }, 8 | }, 9 | "size": { 10 | "canvasWidth": size*0.8, 11 | "canvasHeight": size, 12 | "pieOuterRadius": "90%" 13 | }, 14 | "data": { 15 | "sortOrder": "value-desc", 16 | "content": data, 17 | }, 18 | "tooltips": { 19 | "enabled": true, 20 | "type": "placeholder", 21 | "string": "{label}: \n{value} ({percentage}%)" 22 | }, 23 | "labels": { 24 | "outer": { 25 | "format": "none" 26 | }, 27 | "inner": { 28 | "format": "label-percentage2", 29 | "hideWhenLessThanPercentage": 8 30 | }, 31 | "mainLabel": { 32 | "fontSize": 16, 33 | "color": "black" 34 | }, 35 | "percentage": { 36 | "color": pColor, 37 | "decimalPlaces": 0 38 | }, 39 | "value": { 40 | "color": "#adadad", 41 | "fontSize": 10 42 | } 43 | }, 44 | 'callbacks': { 45 | 'onClickSegment': function(segment) { 46 | setLocationHash({ fG: segment.data.label, view: 'results'}); 47 | } 48 | } 49 | }); 50 | } 51 | 52 | -------------------------------------------------------------------------------- /www/probeapi.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | /* Probe API singleton, allowing one host being probed at a time. 3 | Manages auto-updates and probe dependency tree */ 4 | 5 | function ProbeAPI() { 6 | if(arguments.callee._singletonInstance) { 7 | return arguments.callee._singletonInstance; 8 | } 9 | 10 | arguments.callee._singletonInstance = this; 11 | 12 | var a = this; 13 | $.ajax({ 14 | dataType: "json", 15 | async: false, 16 | url: "/api/probes", 17 | success: function(data) { 18 | a.probes = data; 19 | } 20 | // FIXME: error handling 21 | }); 22 | 23 | // Perform a given probe and call callback cb for result processing 24 | this.probe = function(name, cb, errorCb) { 25 | var a = this; 26 | 27 | a.probes[name].updating = true; 28 | a.probes[name].timestamp = Date.now(); 29 | 30 | // on updates we need to use the previously stored callback 31 | if(undefined === cb) { 32 | cb = a.probes[name].cb; 33 | errorCb = a.probes[name].errorCb; 34 | } else { 35 | a.probes[name].cb = cb; 36 | a.probes[name].errorCb = errorCb; 37 | } 38 | 39 | getAPI('probe/'+name+'/'+a.host, function(res) { 40 | // FIXME: check actual response code here! 41 | 42 | a.probes[name].updating = false; 43 | a.probes[name].timestamp = Date.now(); 44 | 45 | // Always trigger follow probes, serialization is done in backend 46 | for(var p in res.next) { 47 | a.probe(res.next[p], cb); 48 | } 49 | 50 | if(undefined !== cb) 51 | cb(name, a.host, res); 52 | }, function(e) { 53 | // FIXME: use a promise instead 54 | a.probes[name].updating = false; 55 | a.probes[name].timestamp = Date.now(); 56 | errorCb(e); 57 | }); 58 | }; 59 | 60 | // Triggers the initial probes, all others will be handled in the 61 | // update method 62 | this.startProbes = function(cb, errorCb) { 63 | var a = this; 64 | Object.keys(a.probes).forEach(function(p) { 65 | if(a.probes[p].initial) 66 | a.probe(p, cb, errorCb); 67 | }); 68 | }; 69 | 70 | // Start probing a given host, handles initial probe list fetch 71 | // Ensures to stop previous host probes. 72 | this.start = function(host, cb, errorCb) { 73 | this.host = host; 74 | this.stop(); 75 | this.startProbes(cb, errorCb); 76 | this.update(); 77 | }; 78 | 79 | // Stop all probing 80 | this.stop = function() { 81 | clearTimeout(this.updateTimer); 82 | this.probeStates = {}; 83 | }; 84 | 85 | this.update = function() { 86 | var a = this; 87 | var now = Date.now(); 88 | this.updateTimer = setTimeout(this.update.bind(this), 5000); 89 | $.each(this.probes, function(name, p) { 90 | if(p.updating === false && p.refresh*1000 < now - p.timestamp) 91 | a.probe(name) 92 | }); 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /www/renderer_settings.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | 3 | // Returns array of known inventories 4 | function getInventoryTypes() { 5 | var results = []; 6 | var data = getCachedData("overview"); 7 | $.each(data.overview, function(i, item) { 8 | if(item.inventory) 9 | results.push(item.inventory); 10 | }); 11 | return results; 12 | } 13 | 14 | // Add all inventory type names to a 27 | function loadNetedgeTypes(id, selected) { 28 | var options = []; 29 | getData("overview", function(data) { 30 | $.each(data.overview, function(i, item) { 31 | if(item.netedge) { 32 | options.push(''); 33 | } 34 | }); 35 | $('#'+id).append(options.sort().join('')); 36 | $('#'+id+" option[value='"+selected+"']").attr('selected', true); 37 | }); 38 | } 39 | 40 | // Add all findings group names to a '); 94 | if(o.nt) 95 | fbox.append('Type '); 96 | if(o.findings) 97 | fbox.append('Findings '); 98 | 99 | if(o.groupbyid && params.r === 'table') { 100 | fbox.append('Group Hosts By '); 101 | if(params.gI && params.gI != '') 102 | $('#groupById').append(''); 103 | } 104 | if(o.groupbyhg && -1 !== ['hostmap', 'treemap', 'netgraph'].indexOf(params.r)) 105 | fbox.append('Group Hosts by '); 106 | 107 | if(o.host) { 108 | fbox.append('
Host ' + 109 | ' '); 110 | getData("hosts", function(data) { 111 | $.each(data.results, function(host) { 112 | $('#availableHosts').append('
"; 36 | return result; 37 | } 38 | if(probeResult['render']['type'] === 'lines') { 39 | return "
"+probeResult.stdout
 40 | 				.split(/\n/)
 41 | 				.map(function(str) {
 42 | 					return probeResultApplySeverity(str, probeResult['render']['severity']);
 43 | 				})
 44 | 			.join('\n')+"
"; 45 | } 46 | } catch(e) { 47 | console.log(e); 48 | return "Rendering error!"; 49 | } 50 | } 51 | 52 | /* Render a probe result into a target table and add a header */ 53 | function probeRenderAsRow(id, probe, res) { 54 | var label = "#probe_label_" +toId(probe); 55 | var result = "#probe_result_"+toId(probe); 56 | var firstTime = false; 57 | var severity = ''; 58 | var rendered = ""+(res["name"]?res["name"]:probe)+"
"+probeRenderResult(res)+""; 59 | 60 | if(-1 !== rendered.indexOf('WARNING')) 61 | severity = 'WARNING'; 62 | if(-1 !== rendered.indexOf('FAILED')) 63 | severity = 'FAILED'; 64 | 65 | if(0 === res.stdout.length) { 66 | $(label).remove(); 67 | $(result).remove(); 68 | return; 69 | } 70 | 71 | if(!$("#"+id+" .probes").length) { 72 | $('#'+id+' tbody').prepend( 73 | "Live Probes: "+ 74 | // Invisible insertion marker rows for proper insertion order 75 | ""+ 76 | ""+ 77 | "" 78 | ); 79 | } 80 | if(!$(result).length) { 81 | $('#'+id+' .probes_'+severity).after(""); 82 | firstTime = true; 83 | } 84 | 85 | // Add a result row 86 | $(result).html(rendered); 87 | 88 | // Add label to header 89 | if(!$(label).length) { 90 | $('#'+id+' tr.probes th').append(""+probe+""); 91 | 92 | // Initially hide probe results without severity or of type server 93 | if(severity === '' && res['type'] != 'service') { 94 | $(result).hide(); 95 | $(label).addClass('hidden'); 96 | } else { 97 | $(label).addClass('shown'); 98 | } 99 | $(label).click(function(e) { 100 | if(!e.shiftKey) { 101 | $('.probe_label').addClass('hidden').removeClass('shown'); 102 | $('tr.probe_result').hide(); 103 | } 104 | console.log("Showing "+result); 105 | $(label).removeClass('hidden'); 106 | $(label).addClass('shown'); 107 | $(result).show(); 108 | }); 109 | } 110 | 111 | // Show probe results without severity or of type server 112 | if(firstTime && severity !== '') { 113 | $(label).removeClass('hidden'); 114 | $(label).addClass('shown'); 115 | } 116 | 117 | // Ensure tables overflow correctly 118 | $('#'+id+' table.probes').parent().width($('#'+id).parent().width()); 119 | 120 | // FIXME: Sort result table alphabetically and by severity 121 | } 122 | -------------------------------------------------------------------------------- /www/renderers/ptable.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | /* A view presenting all available polscan scanners with 3 | all details and extra statistics for all enabled scanners. */ 4 | 5 | renderers.ptable = function tableRenderer(parentDiv) { }; 6 | 7 | renderers.ptable.prototype.render = function(id, data, params) { 8 | 9 | $(id).html( 10 | "" + 11 | "" + 12 | "" + 13 | "" + 14 | "" + 15 | "" + 16 | "" + 17 | "
EnabledGroupPolicyDetailsTags
" 18 | ); 19 | 20 | $.each(data.results, function(i, item) { 21 | if(item.policy) { 22 | var solution = ""; 23 | var references = ""; 24 | 25 | if(item.solution_cmd && item.solution_cmd !== "") 26 | solution += " Quick Fix
" + item.solution_cmd + "
"; 27 | if(item.solution && item.solution !== "") 28 | solution += " Solution

" + item.solution + "

"; 29 | if(item.references) { 30 | $.each(item.references, function(i, link) { 31 | if(link === "") 32 | return; 33 | references += ""+link+"
"; 34 | }); 35 | } 36 | 37 | $(id+" table.resultTable tbody").append( 38 | "" + 39 | (item.enabled == 1 ? "✓" : "") + "" + 40 | item.parent + "" + 41 | item.policy + "" + 42 | item.description + solution + 43 | (references !== ''?"References

"+references+"

":'') + 44 | "" + 45 | (item.tags?item.tags.join(' '):'') + 46 | "" 47 | ); 48 | } 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /www/renderers/vtable.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | // Renderer for displaying vulnerability details in a sortable table 3 | // Loads row asynchronously to allow for larger tables 4 | 5 | renderers.vtable = function tableRenderer(parentDiv) { 6 | this.tableLoadTimeout = undefined; 7 | }; 8 | 9 | renderers.vtable.prototype.sortTable = function(id, sortOrder) { 10 | $('#loadmessage i').html('Sorting results...'); 11 | console.log("Table setup done."); 12 | this.tableLoadTimeout = setTimeout(function() { 13 | try { 14 | $(id).tablesorter(sortOrder); 15 | } catch(e) { 16 | } 17 | console.log("Table sorting done."); 18 | $('#loadmessage').hide(); 19 | }, 100); 20 | }; 21 | 22 | renderers.vtable.prototype.addResultRows = function(name, rows, offset, count, sortOrder) { 23 | var renderer = this; 24 | var r = ""; 25 | for(var i = offset; i < offset+count; i++) { 26 | if(rows[i]) 27 | r += "" + rows[i] + ""; 28 | } 29 | $("#resultTable tbody").append(r); 30 | if(offset + count < rows.length) { 31 | resultTableLoadTimeout = setTimeout(function() { 32 | $('#loadmessage i').html('Loading results ('+Math.ceil(100*offset/rows.length)+'%)...'); 33 | renderer.addResultRows(name, rows, offset+count, count, sortOrder); 34 | }, 50); 35 | } else { 36 | // Enable table sorting 37 | if(sortOrder != null) 38 | this.sortTable("#resultTable", sortOrder); 39 | else 40 | $('#loadmessage').hide(); 41 | 42 | // Enable clicking 43 | var view = this; 44 | $("#resultTable .hosts a").click(function() { 45 | var key = $(this).attr('id').replace("vuln_",""); 46 | $('#copyHostList').remove(); 47 | $('#filter').append(''); 48 | return false; 49 | }); 50 | } 51 | }; 52 | 53 | renderers.vtable.prototype.render = function(id, data, params) { 54 | 55 | clearTimeout(this.tableLoadTimeout); 56 | $('#loadmessage').show(); 57 | $('.resultTable').empty(); 58 | $('.resultTable').remove(); 59 | $("") 60 | .html("") 61 | .appendTo(id); 62 | 63 | console.log("Grouping hosts by vulnerability"); 64 | $('#loadmessage i').html("Grouping by CVE..."); 65 | var view = this; 66 | var cves = {}; 67 | var packages = {}; 68 | var hosts = {}; 69 | var values = new Array(1000); 70 | view.hosts = {}; 71 | $.each(data.results, function( i, item ) { 72 | var key = item.cve+"___"+item.pkg; 73 | if(values[key] === undefined) 74 | values[key] = item; 75 | if(view.hosts[key] === undefined) 76 | view.hosts[key] = []; 77 | view.hosts[key].push(item.host); 78 | packages[item.pkg] = 1; 79 | cves[item.cve] = 1; 80 | hosts[item.host] = 1; 81 | }); 82 | console.log("Parsing done."); 83 | 84 | var rows = new Array(250); 85 | for(var key in values) { 86 | rows.push('' + 87 | '' + 88 | '' + 89 | '' + 90 | ''); 91 | } 92 | this.addResultRows(id, rows.sort().reverse(), 0, 250, {sortList: [[0,1]]}); 93 | }; 94 | -------------------------------------------------------------------------------- /www/templates.js: -------------------------------------------------------------------------------- 1 | // Rendering HTML using jquery/handlebars.js 2 | 3 | // http://stackoverflow.com/questions/8366733/external-template-in-underscore 4 | // http://javascriptissexy.com/handlebars-js-tutorial-learn-everything-about-handlebars-js-javascript-templating/ 5 | function render(tmpl_name, tmpl_data) { 6 | if ( !render.tmpl_cache ) { 7 | render.tmpl_cache = {}; 8 | } 9 | if ( ! render.tmpl_cache[tmpl_name] ) { 10 | var tmpl_dir = '/templates'; 11 | var tmpl_url = tmpl_dir + '/' + tmpl_name + '.html'; 12 | var tmpl_string; 13 | $.ajax({ 14 | url: tmpl_url, 15 | method: 'GET', 16 | async: false, 17 | success: function(data) { 18 | tmpl_string = data; 19 | } 20 | }); 21 | render.tmpl_cache[tmpl_name] = Handlebars.compile(tmpl_string); 22 | } 23 | return render.tmpl_cache[tmpl_name](tmpl_data); 24 | } 25 | -------------------------------------------------------------------------------- /www/templates/dashboard_top_changes.html: -------------------------------------------------------------------------------- 1 | {{#if changes.length}} 2 |
VulnerabilityPackageSeverityHost CountHosts
'+ values[key].cve +'' + values[key].pkg + '' + values[key].tags + '' + view.hosts[key].length + 'Show List
3 | 4 | {{#each changes}} 5 | 6 | 11 | 12 | {{/each}} 13 | 14 |
7 | {{name}} 8 | 9 | {{count}} 10 |
15 | {{/if}} 16 | -------------------------------------------------------------------------------- /www/templates/group_list_item.html: -------------------------------------------------------------------------------- 1 | 2 | {{ group }} 3 | {{#if FAILED}}{{ FAILED }}{{/if}} 4 | {{#if WARNING}}{{ WARNING }} 6 | -------------------------------------------------------------------------------- /www/templates/inventory_list_item.html: -------------------------------------------------------------------------------- 1 | 2 | {{ inventory }} 3 | 4 | -------------------------------------------------------------------------------- /www/templates/puppetdb.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{#each stats}} 10 | 11 | 17 | 18 | {{/each}} 19 | 20 |
StatusCount
12 |
13 | {{label}} 14 |
15 | {{value}} 16 |
Total Hosts{{total}}
21 | -------------------------------------------------------------------------------- /www/tooltip.js: -------------------------------------------------------------------------------- 1 | function installTooltip(container, cb, data) { 2 | var tooltipShowCb = cb; 3 | var changeTooltipPosition = function(event) { 4 | var tooltipX = event.pageX + 8; 5 | var tooltipY = event.pageY + 8; 6 | 7 | if(tooltipX > $(window).width() * 2 / 3 - 16) 8 | tooltipX = $(window).width() * 2 / 3 - 16; 9 | 10 | $('div.tooltip').css({ 11 | top: tooltipY, 12 | left: tooltipX, 13 | width: $(window).width() / 3 14 | }); 15 | }; 16 | 17 | var hideTooltip = function() { 18 | $('div.tooltip').remove(); 19 | }; 20 | var showTooltip = function(event) { 21 | var content = cb(this, event, data); 22 | $('div.tooltip').remove(); 23 | $('
'+content+'
').appendTo('#results'); 24 | changeTooltipPosition(event); 25 | }; 26 | 27 | $(container).bind({ 28 | mousemove : changeTooltipPosition, 29 | mouseenter : showTooltip, 30 | mouseleave: hideTooltip 31 | }); 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /www/view_netmenu.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | /* A view that allow to select the network view you want :-) */ 3 | 4 | views.NetmenuView = function NetmenuView(parentDiv) { 5 | this.parentDiv = parentDiv; 6 | this.filterOptions = {}; 7 | }; 8 | 9 | views.NetmenuView.prototype.update = function(params) { 10 | $("head").append(` 11 | 42 | `); 43 | 44 | $(this.parentDiv).html(` 45 |
46 |

Available Network Views

47 |
48 | 49 |
Network Map
50 |
Radial diagram of hosts grouped by subdomains and TCP connections. Most useful with hierarchical FQDNs.
51 |
52 |
53 | 54 |
Network Edge
55 |
A matrix of all active TCP connections to external IPv4 networks.
56 |
57 |
58 | 59 |
Connection Browser
60 |
Browse TCP connections per host and navigate along in-/outbound service connections.
61 |
62 |
63 | 64 |
Service Map
65 |
A directed graph of all active TCP services interconnections and in-/outbound external clients.
66 |
67 | 68 |
`); 69 | }; 70 | -------------------------------------------------------------------------------- /www/view_puppetdb_facts.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | /* Show PuppetDB fact details / statistics */ 3 | 4 | views.Puppetdb_factsView = function Puppetdb_factsView(parentDiv) { 5 | this.parentDiv = parentDiv; 6 | this.filterOptions = {}; 7 | }; 8 | 9 | views.Puppetdb_factsView.prototype.getFactDetails = function(params) { 10 | var view = this; 11 | 12 | getAPI("puppetdb/node_facts", function(data) { 13 | var j = 0; 14 | console.log(JSON.stringify(data)); 15 | $(""+ 16 | "

Latest Facts for "+params.h+"

Click a row for report details.

"+ 17 | data.map(function(i) { 18 | return ""+ 19 | ""+ 20 | ""; 21 | }).join("") + 22 | "
Fact NameFact Value
"+i.path.join("::")+""+i.value+"
").appendTo('#row2'); 23 | }, undefined, [{ hostname: params.h }]); 24 | } 25 | 26 | views.Puppetdb_factsView.prototype.update = function(params) { 27 | clean(); 28 | $('#filter').hide(); 29 | $(this.parentDiv).css('margin-left', '12px'); 30 | this.getFactDetails(params); 31 | } 32 | -------------------------------------------------------------------------------- /www/view_servicemap.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | /* A view visualizing service relations as a directed graph. 3 | with external IPs being the tree root */ 4 | 5 | views.ServicemapView = function ServicemapView(parentDiv) { 6 | this.parentDiv = parentDiv; 7 | this.filterOptions = { 8 | filterby: true, 9 | search: true 10 | }; 11 | this.edges = []; 12 | this.services = {}; 13 | this.uniqueEdges = {}; 14 | }; 15 | 16 | views.ServicemapView.prototype.updateGraph = function() { 17 | var width = $('#netmap').width(); 18 | var height = $('#netmap').height(); 19 | var svg = d3.select("#netmap").append("svg") 20 | .attr("width", width) 21 | .attr("height", height); 22 | 23 | 24 | var nodeArea = svg.append('g').classed('node-area', true); 25 | 26 | var g = new dagreD3.graphlib.Graph() 27 | .setGraph({ "rankdir": "LR", "ranksep": 75, "nodesep": 12, "marginx": 20, "marginy": 20, "align": "DL" }) 28 | .setDefaultEdgeLabel(function() { return {}; }); 29 | 30 | $.each(this.services, function(i, n) { 31 | var props = { "label": n.service, "labelType": "html", "class": n.class }; 32 | $.each($.unique(n.ips), function(i, ip) { 33 | if(i < 8) { 34 | if (ip.match(/^(10\.|172\.|192\.)/)) 35 | props.label += "
"+resolveIp(ip); 36 | else 37 | props.label += '
'+ip+" "; 38 | } 39 | 40 | if(i == 8) 41 | props.label += "
("+(n.ips.length - 8)+" more ...)"; 42 | }); 43 | g.setNode(i, props); 44 | }); 45 | 46 | $.each(this.edges, function(i, l) { 47 | if(l.source === undefined || l.target === undefined) 48 | return; 49 | var props = { lineInterpolate: 'basis' }; 50 | if("high" !== l.port) 51 | props.label = l.port; 52 | g.setEdge(l.source, l.target, props); 53 | }); 54 | 55 | var render = new dagreD3.render(); 56 | render(nodeArea, g); 57 | 58 | var xCenterOffset = (svg.attr("width") - g.graph().width) / 2; 59 | svg.attr("height", g.graph().height + 40); 60 | } 61 | 62 | views.ServicemapView.prototype.addUniqueEdge = function(e, service, resolvedService) { 63 | var escaped = service.replace(/ /g, ''); 64 | var escapedT = resolvedService.replace(/ /g, ''); 65 | var ke = e.dir + "_" + service + "_" + e.rn + "_" + resolvedService + "_" + e.rtn; 66 | if(!this.uniqueEdges.hasOwnProperty(ke)) { 67 | this.uniqueEdges[ke] = 1; 68 | if(e.dir === 'out') 69 | this.edges.push({ direction: e.dir, source: escaped, target: escapedT, port: e.rtn }); 70 | else 71 | this.edges.push({ direction: e.dir, target: escaped, source: escapedT, port: e.ltn }); 72 | } 73 | } 74 | 75 | views.ServicemapView.prototype.addUniqueService = function(program, port, c, ip) { 76 | // FIXME: really safe hash keys! 77 | var escaped = program.replace(/ /g, ''); 78 | if(!this.services.hasOwnProperty(escaped)) 79 | this.services[escaped] = { "service": program, "port": port, "class": c, "ips": [] }; 80 | 81 | if(ip) 82 | this.services[escaped]['ips'].push(ip); 83 | } 84 | 85 | views.ServicemapView.prototype.addHosts = function(filteredHosts) { 86 | var view = this; 87 | this.edges = []; 88 | this.services = {}; 89 | this.uniqueEdges = {}; 90 | 91 | // get connections for this host 92 | getData("netedge TCP connection", function(data) { 93 | var nodeportToProgram = {}; 94 | var portToProgram = new Array(); 95 | $.each(data.results, function(i, item) { 96 | if(undefined === filteredHosts || 97 | -1 !== filteredHosts.indexOf(item.host)) { 98 | var id, port = item.ltn, program = item.scope; 99 | if(program === "-") 100 | return; 101 | if(item.dir === 'in') 102 | return; 103 | if(!nodeportToProgram.hasOwnProperty(item.ln+"_"+item.ltn)) { 104 | nodeportToProgram[item.ln+"_"+item.ltn] = program; 105 | } 106 | } 107 | }); 108 | 109 | $.each(data.results, function(i, item) { 110 | if(undefined === filteredHosts || 111 | -1 !== filteredHosts.indexOf(item.host)) { 112 | var port = item.ltn, program = item.scope; 113 | 114 | // Resolve program for close-wait, time-wait listings 115 | if(program !== "-" && !(port in portToProgram)) 116 | portToProgram[port] = program; 117 | if(program === "-" && (port in portToProgram)) 118 | program = portToProgram[port]; 119 | 120 | // Reduce connections to service<->service connections 121 | if(program === "-") 122 | return; // displaying unknown procs is just useless 123 | 124 | view.addUniqueService(program, port, 'local'); 125 | 126 | var resolvedService = nodeportToProgram[item.rn+"_"+item.rtn]; 127 | if(resolvedService) { 128 | view.addUniqueEdge(item, program, resolvedService); 129 | } else { 130 | // FIXME: private net match 131 | if(item.rn.match(/^(10\.|172\.|192\.)/)) { 132 | view.addUniqueService('Internal '+item.dir, undefined, 'other', item.rn); 133 | view.addUniqueEdge(item, program, 'Internal '+item.dir); 134 | } else { 135 | view.addUniqueService('External '+item.dir, undefined, 'other', item.rn); 136 | view.addUniqueEdge(item, program, 'External '+item.dir); 137 | } 138 | } 139 | } 140 | }); 141 | 142 | view.updateGraph(); 143 | if(isLive()) 144 | overlayMonitoring(undefined, undefined, false); 145 | }); 146 | } 147 | 148 | views.ServicemapView.prototype.update = function(params) { 149 | var filteredHosts = get_hosts_filtered(params, true); 150 | 151 | clean(); 152 | $('#results').append('
'); 153 | this.addHosts(filteredHosts); 154 | }; 155 | -------------------------------------------------------------------------------- /www/view_vulnerabilities.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | // View for displaying vulnerabilities in a sortable table 3 | 4 | views.VulnerabilitiesView = function VulnerabilitiesView(parentDiv) { 5 | this.parentDiv = parentDiv; 6 | this.filterOptions = { 7 | filterby: true, 8 | search: true 9 | }; 10 | }; 11 | 12 | var resultTableLoadTimeout; 13 | 14 | function sortTable(id, sortOrder) { 15 | $('#loadmessage i').html('Sorting results...'); 16 | console.log("Table setup done."); 17 | resultTableLoadTimeout = setTimeout(function() { 18 | try { 19 | $(id).tablesorter(sortOrder); 20 | } catch(e) { 21 | console.log(e); 22 | } 23 | console.log("Table sorting done."); 24 | $('#loadmessage').hide(); 25 | }, 100); 26 | } 27 | 28 | function addVulnResultRows(rows, offset, count, sortOrder) { 29 | var r = ""; 30 | for(var i = offset; i < offset+count; i++) { 31 | if(rows[i]) 32 | r += "" + rows[i] + ""; 33 | } 34 | $("#resultTable tbody").append(r); 35 | if(offset + count < rows.length) { 36 | resultTableLoadTimeout = setTimeout(function() { 37 | $('#loadmessage i').html('Loading results ('+Math.ceil(100*offset/rows.length)+'%)...'); 38 | addVulnResultRows(rows, offset+count, count, sortOrder); 39 | }, 50); 40 | } else { 41 | // Enable table sorting 42 | if(sortOrder != null) 43 | sortTable("#resultTable", sortOrder); 44 | else 45 | $('#loadmessage').hide(); 46 | 47 | // Enable clicking 48 | var view = this; 49 | $("#resultTable .hosts a").click(function() { 50 | var key = $(this).attr('id').replace("vuln_",""); 51 | $('#copyHostList').remove(); 52 | $('#filter').append(''); 53 | return false; 54 | }); 55 | } 56 | } 57 | 58 | function vulnMatches(item) { 59 | if(params.sT && 60 | !((undefined !== item.host && item.host.indexOf(this.params.sT) != -1) || 61 | (undefined !== item.cve && item.cve.indexOf(this.params.sT) != -1) || 62 | (undefined !== item.pkg && item.pkg.indexOf(this.params.sT) != -1))) 63 | return false; 64 | if(undefined !== filteredHosts && 65 | -1 == this.filteredHosts.indexOf(item.host)) 66 | return false; 67 | return true; 68 | } 69 | 70 | function createVulnGroupTable(id, results) { 71 | 72 | clearTimeout(resultTableLoadTimeout); 73 | $('#loadmessage').show(); 74 | $('.resultTable').empty(); 75 | $('.resultTable').remove(); 76 | $("") 77 | .html("") 78 | .appendTo(id); 79 | 80 | console.log("Grouping hosts by vulnerability"); 81 | $('#loadmessage i').html("Grouping by CVE..."); 82 | var view = this; 83 | var cves = {}; 84 | var packages = {}; 85 | var hosts = {} 86 | var values = new Array(1000); 87 | view.hosts = {}; 88 | $.each(results.filter(vulnMatches, view), function( i, item ) { 89 | var key = item.cve+"___"+item.pkg; 90 | if(values[key] === undefined) 91 | values[key] = item; 92 | if(view.hosts[key] === undefined) 93 | view.hosts[key] = new Array(); 94 | view.hosts[key].push(item.host); 95 | packages[item.pkg] = 1; 96 | cves[item.cve] = 1; 97 | hosts[item.host] = 1; 98 | }); 99 | console.log("Parsing done."); 100 | 101 | var rows = new Array(250); 102 | for(var key in values) { 103 | rows.push('' + 104 | '' + 105 | '' + 106 | '' + 107 | ''); 108 | } 109 | viewInfoAddBlock('Hosts', Object.keys(hosts).length); 110 | viewInfoAddBlock('Vulnerabilities', Object.keys(view.hosts).length); 111 | viewInfoAddBlock('Packages', Object.keys(packages).length); 112 | viewInfoAddBlock('CVEs', Object.keys(cves).length); 113 | $('#tableRow').width('100%'); 114 | addVulnResultRows(rows.sort().reverse(), 0, 250, {sortList: [[0,1]]}); 115 | } 116 | 117 | views.VulnerabilitiesView.prototype.update = function(params) { 118 | var id = this.parentDiv; 119 | 120 | console.log("Fetching results start (search="+params.sT+")"); 121 | clean(); 122 | 123 | $('#loadmessage').show(); 124 | $('#loadmessage i').html("Fetching data..."); 125 | getData("vulnerabilities", function(data) { 126 | this.params = params; 127 | this.filteredHosts = get_hosts_filtered(params, false); 128 | 129 | viewInfoReset('Vulnerabilities'); 130 | 131 | $(id).append("
"); 132 | 133 | createVulnGroupTable('#tableRow', data.results); 134 | }); 135 | }; 136 | -------------------------------------------------------------------------------- /www/views/Dashboard.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | // The Dashboard... 3 | 4 | function Dashboard() { 5 | this.name = 'Dashboard'; 6 | this.renderers = ['dashboard']; 7 | this.defaultRenderer = 'dashboard'; 8 | this.filterOptions = {}; 9 | } 10 | 11 | Dashboard.prototype = Object.create(PolscanView.prototype); 12 | 13 | Dashboard.prototype.update = function(params) { 14 | var view = this; 15 | 16 | $("#filter").hide(); 17 | 18 | getData("overview", function(data) { 19 | view.addInfoBlock('Hosts', data.hostCount); 20 | view.addInfoBlock('Failed', data.FAILED); 21 | view.addInfoBlock('Warnings', data.WARNING); 22 | view.render(view.parentDiv, data, params); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /www/views/Inventory.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | /* A view visualizing distribution of inventory over host groups. 3 | Represents inventory type items as color coded bars inside 4 | host boxes. Suitable for mapping out inventory where each 5 | host has less than 5 findings */ 6 | 7 | function Inventory() { 8 | this.name = 'Inventory'; 9 | this.renderers = ['itable', 'hostmap', 'treemap']; 10 | this.defaultRenderer = 'hostmap'; 11 | this.filterOptions = { 12 | inventory: true, 13 | groupbyhg: true, 14 | filterby: true, 15 | search: true, 16 | copyHosts: true 17 | }; 18 | this.legend = { 19 | colorIndex : {}, 20 | multiSelect : false // maybe this should be true for heatmaps 21 | }; 22 | } 23 | 24 | Inventory.prototype = Object.create(PolscanView.prototype); 25 | 26 | // Result filter method 27 | Inventory.prototype.resultsFilter = function(item) { 28 | if(this.filteredHosts !== undefined && 29 | -1 == this.filteredHosts.indexOf(item.host)) 30 | return false; 31 | return true; 32 | }; 33 | 34 | /* Smart legend sorting by trying to extract leading float 35 | numbers (e.g. versions) from legend string. Falls back 36 | to string sort if extraction fails. */ 37 | Inventory.prototype.legendSort = function(a, b) { 38 | var aNr = a.match(/^(\d+(\.\d+)?)/); 39 | var bNr = b.match(/^(\d+(\.\d+)?)/); 40 | 41 | if(aNr == null || bNr == null) 42 | return a>b; 43 | 44 | return aNr[1] - bNr[1]; 45 | }; 46 | 47 | Inventory.prototype.addLegend = function(results) { 48 | var view = this; 49 | var legendCount = {}; 50 | var legend = []; 51 | var pcolor = []; 52 | 53 | $.each(results, function (i, f) { 54 | var values = f.values.split(/ /).filter(function(i) { 55 | return i != ''; 56 | }); 57 | 58 | // Update legend 59 | $.each(values, function(i, c) { 60 | if(undefined === legendCount[c]) 61 | legendCount[c] = 0; 62 | legendCount[c]++; 63 | 64 | if(-1 !== legend.indexOf(c)) 65 | return; 66 | view.legend.colorIndex[c] = legend.length; 67 | legend.push(c); 68 | }); 69 | }); 70 | 71 | // Determine which palette to use (shaded for numeric values) 72 | // and high contrast for non-numeric values 73 | var numeric = 1; 74 | for(var l in legend) { 75 | if(!legend[l].match(/^[0-9]+$/)) 76 | numeric = 0; 77 | } 78 | 79 | if(numeric) 80 | view.legend.colors = []; 81 | 82 | // Create colors for numeric legend by title 83 | // and for non-numeric legends by index 84 | var sortedLegend = legend.sort(view.legendSort); 85 | var lastElem = sortedLegend[sortedLegend.length-1]; 86 | for(l in sortedLegend) { 87 | var name = legend[l]; 88 | var colorIndex; 89 | if(numeric) { 90 | // Create ad-hoc gradient 91 | if(0 != name) 92 | view.legend.colors[colorIndex] = "rgb("+Math.ceil(153-(153*name/lastElem))+", "+Math.ceil(255-(255*name/lastElem))+", 102)"; 93 | else 94 | view.legend.colors[colorIndex] = "white"; 95 | colorIndex = view.legend.colorIndex[name]; 96 | } else { 97 | colorIndex = view.legend.colorIndex[name]; 98 | } 99 | view.addLegendItem(name, name, legendCount[name], colorIndex); 100 | } 101 | 102 | $("#legend .legendItem").on("click", view, view.selectLegendItem); 103 | }; 104 | 105 | Inventory.prototype.update = function(params) { 106 | var view = this; 107 | 108 | if(!params.iT) { 109 | params.iT = getInventoryTypes()[0]; 110 | setLocationHash(params); 111 | return; 112 | } 113 | if(!params.gT) { 114 | params.gT = "Domain"; 115 | setLocationHash(params); 116 | return; 117 | } 118 | 119 | getData("inventory "+params.iT, function(data) { 120 | view.filteredHosts = get_hosts_filtered(params, true); 121 | 122 | $(view.parentDiv).append('
Legend
'); 123 | $(view.parentDiv).append("
"); 124 | Split(['#render', '#legend'], { 125 | sizes: [75, 25], 126 | minSize: [200, 200] 127 | }); 128 | 129 | var results = data.results.filter(view.resultsFilter, view); 130 | view.addLegend(results); 131 | view.render('#render', { 132 | results : results, 133 | legend : view.legend 134 | }, params); 135 | view.addInfoBlock('Hosts', view.filteredHosts.length); 136 | }); 137 | }; 138 | -------------------------------------------------------------------------------- /www/views/Network.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | // View for displaying network topologies based on edge types 3 | 4 | function Network() { 5 | this.name = 'Network'; 6 | this.renderers = ['netrad', 'netmap', 'netgraph']; 7 | this.defaultRenderer = 'netrad'; 8 | this.firstHost = undefined; 9 | 10 | var params = getParams(); 11 | if('netmap' === params.r) { 12 | this.filterOptions = { 13 | host: true, 14 | nt: true 15 | }; 16 | } 17 | if('netrad' === params.r) { 18 | this.filterOptions = { 19 | filterby: true, 20 | search: true, 21 | nt: true 22 | }; 23 | } 24 | if('netgraph' === params.r) { 25 | this.filterOptions = { 26 | filterby: true, 27 | groupbyhg: true, 28 | search: true, 29 | nt: true 30 | }; 31 | } 32 | } 33 | 34 | Network.prototype = Object.create(PolscanView.prototype); 35 | 36 | // Result filter method 37 | Network.prototype.resultsFilter = function(item) { 38 | // Validate input 39 | if(item.ln === '' || item.ltn === '' || item.rn === '' || item.rtn === '') 40 | return false; 41 | 42 | if(undefined === this.firstHost) 43 | this.firstHost = item.host; 44 | 45 | if(this.params.sT && !(item.host.indexOf(this.params.sT) != -1)) 46 | return false; 47 | 48 | if(this.filteredHosts !== undefined && 49 | -1 == this.filteredHosts.indexOf(item.host)) 50 | return false; 51 | return true; 52 | }; 53 | 54 | Network.prototype.update = function(params) { 55 | var view = this; 56 | 57 | if(!("nt" in params) || (params.nt === "")) { 58 | changeLocationHash({ 59 | nt: params.nt?params.nt:'TCP connection' 60 | }); 61 | return; 62 | } 63 | if(params.r === 'netmap' && undefined === params.h) { 64 | setLocationHash({h:view.firstHost, nt: params.nt}); 65 | return; 66 | } 67 | 68 | getData("netedge "+params.nt, function(data) { 69 | view.params = params; 70 | view.filteredHosts = get_hosts_filtered(params, false); 71 | 72 | var results = data.results.filter(view.resultsFilter, view); 73 | 74 | $(view.parentDiv).append("
"+ 75 | "
"); 76 | 77 | $('#render').addClass("split split-horizontal"); 78 | $('#legend').addClass("split split-horizontal"); 79 | Split(['#legend', '#render'], { 80 | sizes: [20, 80], 81 | minSize: [200, 200] 82 | }); 83 | 84 | view.render('#render', { results: results }, view.params); 85 | if(params.r !== 'netmap') 86 | view.addInfoBlock('Hosts', view.filteredHosts.length); 87 | // Maybe add connections info block 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /www/views/Policies.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | /* A view presenting all available polscan scanners with 3 | all details and extra statistics for all enabled scanners. */ 4 | 5 | function Policies() { 6 | this.name = 'Policies'; 7 | this.renderers = ['ptable']; 8 | this.defaultRenderer = 'ptable'; 9 | this.filterOptions = {}; 10 | } 11 | 12 | Policies.prototype = Object.create(PolscanView.prototype); 13 | 14 | Policies.prototype.update = function(params) { 15 | var view = this; 16 | 17 | $("#filter").hide(); 18 | 19 | getData("policies", function(data) { 20 | var totalCount = 0; 21 | var enabledCount = 0; 22 | 23 | $.each(data.results, function(i,item) { 24 | if(item.policy) { 25 | totalCount++; 26 | if(item.enabled) 27 | enabledCount++; 28 | } 29 | }); 30 | 31 | view.addInfoBlock('Policies', totalCount); 32 | view.addInfoBlock('Enabled', enabledCount); 33 | view.render(view.parentDiv, data, params); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /www/views/ScanResults.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | // View for displaying finding details by group 3 | 4 | function ScanResults() { 5 | this.name = 'Scan Results'; 6 | this.renderers = ['table', 'hostmap', 'treemap']; 7 | this.defaultRenderer = 'table'; 8 | this.filterOptions = { 9 | findings: true, 10 | groupbyhg: true, 11 | groupbyid: true, 12 | filterby: true, 13 | search: true, 14 | copyHosts: true 15 | }; 16 | this.legend = { 17 | colors : [ '#F77', '#FF7', '#7F7' ], 18 | colorIndex : { 'FAILED':0, 'WARNING':1, 'OK':2 }, 19 | multiSelect : true 20 | }; 21 | } 22 | 23 | ScanResults.prototype = Object.create(PolscanView.prototype); 24 | 25 | // Result filter method 26 | ScanResults.prototype.resultsFilter = function(item) { 27 | if(this.params.gI !== undefined && item.message.indexOf(this.params.gI) == -1) 28 | return false; 29 | if(this.params.sT !== undefined && item.severity == 'OK') 30 | return false; 31 | 32 | if(this.params.sT && 33 | !(( item.host.indexOf(this.params.sT) != -1) || 34 | ( item.policy.indexOf(this.params.sT) != -1) || 35 | (item.message.indexOf(this.params.sT) != -1))) 36 | return false; 37 | 38 | if(this.filteredHosts !== undefined && 39 | -1 == this.filteredHosts.indexOf(item.host)) 40 | return false; 41 | return true; 42 | }; 43 | 44 | ScanResults.prototype.addLegend = function(results) { 45 | var view = this; 46 | var topFindings = {}; 47 | 48 | $.each(results, function(i, item) { 49 | if('OK' === item.severity) 50 | return; 51 | if('FAILED' == item.severity) 52 | view.failed++; 53 | if('WARNING' == item.severity) 54 | view.warning++; 55 | 56 | // Legend counting 57 | var topKey = item.severity+":::"+(item.group?item.group:view.params.fG)+':::'+item.policy; 58 | if(undefined === topFindings[topKey]) 59 | topFindings[topKey] = 0; 60 | topFindings[topKey]++; 61 | }); 62 | 63 | // Create colors for numeric legend by title 64 | // and for non-numeric legends by index 65 | var numeric = 0; 66 | var lastElem = topFindings[topFindings.length-1]; 67 | Object.keys(topFindings).sort(function(a,b) { 68 | if((-1 !== a.indexOf('FAILED') && -1 !== b.indexOf('FAILED')) || 69 | (-1 !== a.indexOf('WARNING') && -1 !== b.indexOf('WARNING'))) 70 | return topFindings[b] - topFindings[a]; 71 | if(-1 !== a.indexOf('FAILED')) 72 | return -1; 73 | return 1; 74 | }).forEach(function(name) { 75 | var count = topFindings[name]; 76 | var tmp = name.split(/:::/); 77 | view.addLegendItem(name, tmp[1]+" - "+tmp[2], count, view.legend.colorIndex[tmp[0]]); 78 | }); 79 | $("#legend .legendItem").on("click", view, view.selectLegendItem); 80 | }; 81 | 82 | ScanResults.prototype.update = function(params) { 83 | var view = this; 84 | 85 | if(!params.fG) { 86 | params.fG = 'new'; 87 | setLocationHash(params); 88 | return; 89 | } 90 | 91 | getData(params.fG, function(data) { 92 | view.failed = 0; 93 | view.warning = 0; 94 | view.hostCount = 0; 95 | view.params = params; 96 | view.filteredHosts = get_hosts_filtered(params, false); 97 | 98 | $(view.parentDiv).append("
Legend
"+ 99 | "
"); 100 | 101 | $('#render').addClass("split split-horizontal"); 102 | $('#legend').addClass("split split-horizontal"); 103 | Split(['#legend', '#render'], { 104 | sizes: [25, 75], 105 | minSize: [234, 0] // 234px matches view name block width 106 | }); 107 | 108 | var results = data.results.filter(view.resultsFilter, view); 109 | view.addLegend(results); 110 | view.render('#tableRow', { results: results, legend: view.legend }, view.params); 111 | view.addInfoBlock('Hosts', view.filteredHosts.length); 112 | view.addInfoBlock('Failed', view.failed); 113 | view.addInfoBlock('Warnings', view.warning); 114 | 115 | // FIXME: view.addHistogram 116 | //createHistogram('#histogramRow', params.fG, params.sT); 117 | }); 118 | }; 119 | -------------------------------------------------------------------------------- /www/views/Vulnerabilities.js: -------------------------------------------------------------------------------- 1 | // vim: set ts=4 sw=4: 2 | // View for displaying vulnerabilities in a sortable table 3 | 4 | function Vulnerabilities() { 5 | this.name = 'Vulnerabilities'; 6 | this.renderers = ['vtable', 'treemap']; 7 | this.defaultRenderer = 'vtable'; 8 | this.filterOptions = { 9 | filterby: true, 10 | search: true 11 | }; 12 | this.legend = { 13 | colors : [ '#F77' ], 14 | colorIndex : { 'FAILED':0 }, 15 | }; 16 | } 17 | 18 | Vulnerabilities.prototype = Object.create(PolscanView.prototype); 19 | 20 | Vulnerabilities.prototype.vulnFilter = function(item) { 21 | if(this.params.sT && 22 | !((undefined !== item.host && item.host.indexOf(this.params.sT) != -1) || 23 | (undefined !== item.cve && item.cve.indexOf(this.params.sT) != -1) || 24 | (undefined !== item.pkg && item.pkg.indexOf(this.params.sT) != -1))) 25 | return false; 26 | if(undefined !== this.filteredHosts && 27 | -1 == this.filteredHosts.indexOf(item.host)) 28 | return false; 29 | return true; 30 | }; 31 | 32 | Vulnerabilities.prototype.update = function(params) { 33 | var view = this; 34 | view.params = params; 35 | 36 | if(!params.gT) 37 | params.gT = "Domain"; // This usually does exist 38 | 39 | getData("vulnerabilities", function(data) { 40 | view.filteredHosts = get_hosts_filtered(params, false); 41 | var cves = {}; 42 | var packages = {}; 43 | var hosts = {}; 44 | var values = new Array(1000); 45 | view.hosts = {}; 46 | 47 | var results = data.results.filter(view.vulnFilter, view); 48 | $.each(results, function(i, item) { 49 | var key = item.cve+"___"+item.pkg; 50 | if(values[key] === undefined) 51 | values[key] = item; 52 | if(view.hosts[key] === undefined) 53 | view.hosts[key] = []; 54 | view.hosts[key].push(item.host); 55 | packages[item.pkg] = 1; 56 | cves[item.cve] = 1; 57 | hosts[item.host] = 1; 58 | }); 59 | 60 | view.addInfoBlock('Hosts', Object.keys(hosts).length); 61 | view.addInfoBlock('Vulnerabilities', Object.keys(view.hosts).length); 62 | view.addInfoBlock('Packages', Object.keys(packages).length); 63 | view.addInfoBlock('CVEs', Object.keys(cves).length); 64 | 65 | $(view.parentDiv).append("
"); 66 | view.render('#tableRow', { results: data.results, legend: view.legend }, view.params); 67 | }); 68 | }; 69 | --------------------------------------------------------------------------------
VulnerabilityPackageSeverityHost CountHosts
'+ values[key].cve +'' + values[key].pkg + '' + values[key].tags + '' + view.hosts[key].length + 'Show List