28. December 2018 · Comments Off on Search for object matches in an ASA config. · Categories: AWK, Firewall, Linux, Linux Scripts, Networking · Tags: , , , , , ,

Having to parse ASA configs for migration purposes provides a never-ending source of reasons to write scripts. The following AWK script will munge an ASA config searching for any specified address or object name and will output any objects that reference it. This script is something I use in conjunction with the ASA_acls.sh script to find security rules relating to an address. As far as I know this is the closest offline tool simmilar to the “Where Used” feature in ASDM for finding addresses.

ASA_obj.awk: awk script, ASCII text executable

#!/usr/bin/awk -f
## Search for object matches in an ASA config.
## 2018 (v.01) - Script from www.davideaves.com
 
### BEGIN ###
 
BEGIN {
  dig_range="y"
  dig_subnet="n"
 
  # Script arguments: ASA configuration + Search objects
  if ( ARGV[1] == "" ) {
    print "ERROR: No Input ASA config provided!" > "/dev/stderr"
    exit 1
  } else if ( ARGV[2] == "" ) {
    print "ERROR: No address or object to search for!" > "/dev/stderr"
    exit 1
  } else {
    # Saving everything after ARGV[1] in search_array.
    for (i = 2; i < ARGC; i++) {
      search_array[ARGV[i]] = ARGV[i]
      delete ARGV[i]
  } }
}
 
### FUNCTIONS ###
 
# Convert IP to Interger.
function ip_to_int(input) {
  split(input, oc, ".")
  ip_int=(oc[1]*(256^3))+(oc[2]*(256^2))+(oc[3]*(256))+(oc[4])
  return ip_int
}
 
# test if a string is an ipv4 address
function is_v4(address) {
  split(address, octet, ".")
  if ( octet[1] <= 255 && octet[2] <= 255 && octet[3] <= 255 && octet[4] <= 255 )
  return address
}
 
# convert number to bits
function bits(N){
  c = 0
  for(i=0; i<8; ++i) if( and(2**i, N) ) ++c
  return c
}
 
# convert ipv4 to prefix
function to_prefix(mask) {
  split(mask, octet, ".")
  return bits(octet[1]) + bits(octet[2]) + bits(octet[3]) + bits(octet[4])
}
 
### SCRIPT ###
 
//{ gsub(/\r/, "") # Strip CTRL+M
 
  ### LINE IS NAME ###
  if ( $1 ~ /^name$/ ) {
 
    name=$3; host=$2; type=$1
    for(col = 5; col <= NF; col++) { previous=previous" "$col }
    description=substr(previous,2)
    previous=""
 
    # Add to search_array
    for (search in search_array) if ( host == search ) search_array[name]
  }
 
  ### LINE IS OBJECT ### 
  else if ( $1 ~ /^object/ ) {
 
    tab="Y"
    name=$3
    type=$2
    if ( type == "service" ) service=$4
    previous=""
 
  } else if ( tab == "Y" && substr($0,1,1) == " " ) {
 
    # object is single host.
    if ( $1 == "host" ) {
      host=$NF
      for (search in search_array) if ( host == search ) search_array[name]
    }
 
    # object is a subnet
    else if ( $1 == "subnet" && dig_subnet == "y" ) {
      for (search in search_array) if ( is_v4(search) ) {
 
        NETWORK=ip_to_int($2)
        PREFIX=to_prefix($3)
        BROADCAST=(NETWORK + (2 ^ (32 - PREFIX) - 1))
 
        if ( ip_to_int(search) >= int(NETWORK) && ip_to_int(search) <= int(BROADCAST) ) {
          search_array[name]
      } }
    }
 
    # object is a range
    else if ( $1 == "range" && dig_range == "y" ) {
      for (search in search_array) if ( is_v4(search) ) {
        if ( ip_to_int(search) >= ip_to_int($2) && ip_to_int(search) <= ip_to_int($3) ) {
          search_array[name]
      } }
    }
 
    # object is group of other objects
    else if ( $2 ~ /(host|object)/ ) {
      for (search in search_array) if ( $NF == search ) search_array[name]
    }
 
    # object contains nat statement
    else if ( $1 == "nat" ) {
      for (search in search_array) if ( $NF == search ) search_array[name]
    }
 
    ### Debug everything else within an object
    #else { print "DEBUG:",$0 }
 
  }
  else { tab="" }
 
}
 
### END ###
 
END{
  if ( isarray(search_array) ) {
    print "asa_objects:"
    for (search in search_array) print "  -",search
  }
}
19. December 2018 · Comments Off on Collect all sensor information from the FMC. · Categories: Cisco, Firewall, Linux Scripts, Networking, Uncategorized · Tags: , , , , , ,

Eventually I plan on refactoring all my firepower scripts into Ansible Playbooks. But in the meanwhile the following is a quick script that will collect all sensor information from a Firepower Management Center and save that information to a CSV file. The output is pretty handy for migrations and general data collection.

#!/bin/bash
## Collect all sensor devicerecords from a FMC.
## Requires: python:PyYAML,shyaml
## 2018 (v.01) - Script from www.davideaves.com
 
username="fmcusername"
password="fmcpassword"
 
FMC="192.0.2.13 192.0.2.14 192.0.2.15 192.0.2.16 192.0.2.17 192.0.2.18 192.0.2.21 192.0.2.22 192.0.2.23"
 
### Convert JSON to YAML.
j2y() {
 python -c 'import sys, yaml, json; yaml.safe_dump(json.load(sys.stdin), sys.stdout, default_flow_style=False)' 2> /dev/null
}
 
### Convert YAML to JSON.
y2j() {
 python -c 'import sys, yaml, json; y=yaml.load(sys.stdin.read()); print json.dumps(y)' 2> /dev/null
}
 
echo "FMC,healthStatus,hostName,model,name," > "$(basename ${0%.*}).csv"
 
# Itterate through all FMC devices
for firepower in ${FMC}
 do eval "$(curl -skX POST https://${firepower}/api/fmc_platform/v1/auth/generatetoken \
        -H "Authorization: Basic $(printf "${username}:${password}" | base64)" -D - |\
        awk '/(auth|DOMAIN|global)/{gsub(/[\r|:]/,""); gsub(/-/,"_",$1); print $1"=\""$2"\""}')"
 
    ### Get expanded of list devices
    curl -skX GET "https://${firepower}/api/fmc_config/v1/domain/${DOMAIN_UUID}/devices/devicerecords?offset=0&limit=1000&expanded=true" -H "X-auth-access-token: ${X_auth_access_token}" |\
     j2y | awk 'BEGIN{ X=0; }/^(-|  [a-z])/{if($1 == "-") {X+=1; printf "'''${firepower}''',"} else if($1 == "healthStatus:" || $1 == "hostName:" || $1 == "model:" || $1 == "name:") {printf $NF","} else if($1 == "type:") {printf "\n"}}'
 
done >> "$(basename ${0%.*}).csv"
19. December 2018 · Comments Off on Ansible playbook to provision Netscaler VIPs. · Categories: Ansible, Linux, Linux Admin, Load Balancing, NetScaler, Networking · Tags: , , ,

The following playbook will create a fully functional VIP; including the supporting monitor, service-group (pool) and servers (nodes) on a netscaler loadbalancer. Additionally, the same playbook has the ability to fully deprovision a VIP and all its supporting artifacts. To do all this I use the native Netscaler Ansible modules. When it comes to using the netscaler_servicegroup module, since the number of servers are not always consistent; I create that task with a Jinja2 template, where its imported back into the play.

netscaler_provision.yaml: a /usr/bin/ansible-playbook -f 10 script text executable, ASCII text

#!/usr/bin/ansible-playbook -f 10
## Ansible playbook to provision Netscaler VIPs.
# Requires: nitrosdk-python
# 2018 (v.01) - Playbook from www.davideaves.com
---
- name: Netscaler VIP provision
  hosts: netscaler
  connection: local
  gather_facts: False

  vars:

    ansible_connection: "local"
    ansible_python_interpreter: "/usr/bin/env python"

    state: 'present'

    lbvip:
      name: testvip
      address: 203.0.113.1
      server:
        - name: 'server-1'
          address: '192.0.2.1'
          description: 'Ansible Test Server 1'
          disabled: 'true'
        - name: 'server-2'
          address: '192.0.2.2'
          description: 'Ansible Test Server 2'
          disabled: 'true'
        - name: 'server-3'
          address: '192.0.2.3'
          description: 'Ansible Test Server 3'
          disabled: 'true'
        - name: 'server-4'
          address: '192.0.2.4'
          description: 'Ansible Test Server 4'
          disabled: 'true'
        - name: 'server-5'
          address: '192.0.2.5'
          description: 'Ansible Test Server 5'
          disabled: 'true'
        - name: 'server-6'
          address: '192.0.2.6'
          description: 'Ansible Test Server 6'
          disabled: 'true'
        - name: 'server-7'
          address: '192.0.2.7'
          description: 'Ansible Test Server 7'
          disabled: 'true'
        - name: 'server-8'
          address: '192.0.2.8'
          description: 'Ansible Test Server 8'
          disabled: 'true'
      vserver:
        - port: '80'
          description: 'Generic service running on 80'
          type: 'HTTP'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '443'
          description: 'Generic service running on 443'
          type: 'SSL_BRIDGE'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '8080'
          description: 'Generic service running on 8080'
          type: 'HTTP'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '8081'
          description: 'Generic service running on 8081'
          type: 'HTTP'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'
        - port: '8443'
          description: 'Generic service running on 8443'
          type: 'SSL_BRIDGE'
          method: 'LEASTCONNECTION'
          persistence: 'SOURCEIP'

  tasks:

    - name: Build lbvip and all related componets.
      block:
      - local_action:
          module: netscaler_server
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "{{ item.name }}"
          ipaddress: "{{ item.address }}"
          comment: "{{ item.description | default('Ansible Created') }}"
          disabled: "{{ item.disabled | default('false') }}"
        with_items: "{{ lbvip.server }}"
      - local_action:
          module: netscaler_lb_monitor
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          monitorname: "tcp_{{ lbvip.name }}_{{ item.port }}"
          type: TCP
          destport: "{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
        no_log: false
      - local_action:
          module: copy
          content: "{{ lookup('template', 'templates/netscaler_servicegroup.j2') }}"
          dest: "/tmp/svg_{{ lbvip.name }}_{{ item.port }}.yaml"
          mode: "0644"
        with_items: "{{ lbvip.vserver }}"
        changed_when: false
      - include_tasks: "/tmp/svg_{{ lbvip.name }}_{{ item.port }}.yaml"
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: file
          state: absent
          path: "/tmp/svg_{{ lbvip.name }}_{{ item.port }}.yaml"
        with_items: "{{ lbvip.vserver }}"
        changed_when: false
      - local_action:
          module: netscaler_lb_vserver
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "vs_{{ lbvip.name }}_{{ item.port }}"
          servicetype: "{{ item.type }}"
          ipv46: "{{ lbvip.address }}"
          port: "{{ item.port }}"
          lbmethod: "{{ item.method | default('LEASTCONNECTION') }}"
          persistencetype: "{{ item.persistence | default('SOURCEIP') }}"
          servicegroupbindings:
            - servicegroupname: "svg_{{ lbvip.name }}_{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
      when: state == "present"

    - name: Destroy lbvip and all related componets.
      block:
      - local_action:
          module: netscaler_lb_vserver
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "vs_{{ lbvip.name }}_{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: netscaler_servicegroup
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          servicegroupname: "svg_{{ lbvip.name }}_{{ item.port }}"
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: netscaler_lb_monitor
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          monitorname: "tcp_{{ lbvip.name }}_{{ item.port }}"
          type: TCP
        with_items: "{{ lbvip.vserver }}"
      - local_action:
          module: netscaler_server
          nsip: "{{ inventory_hostname }}"
          nitro_user: "{{ nitro_user | default('nsroot') }}"
          nitro_pass: "{{ nitro_pass | default('nsroot') }}"
          nitro_protocol: "https"
          validate_certs: no
          state: "{{ state }}"
          name: "{{ item.name }}"
        with_items: "{{ lbvip.server }}"
      when: state == "absent"

The following is the Jinja2 template that creates the netscaler_servicegroup task. An important thing to note is my use of the RAW block. When the task is created and stored in /tmp it does not contain any account credentials, instead I preserve the variable in the raw to prevent leaking sensitive information to anyone who may be snooping around on the server while the playbook is running.

templates/netscaler_servicegroup.j2: ASCII text, with CRLF line terminators

---
- local_action:
    module: netscaler_servicegroup
    nsip: {% raw %}"{{ inventory_hostname }}"
{% endraw %}
    nitro_user: {% raw %}"{{ nitro_user }}"
{% endraw %}
    nitro_pass: {% raw %}"{{ nitro_pass }}"
{% endraw %}
    nitro_protocol: "https"
    validate_certs: no

    state: "{{ state | default('present') }}"

    servicegroupname: "svg_{{ lbvip.name }}_{{ item.port }}"
    comment: "{{ item.description | default('Ansible Created') }}"
    servicetype: "{{ item.type }}"
    servicemembers:
{% for i in lbvip.server %}
      - servername: "{{ i.name }}"
        port: "{{ item.port }}"
{% endfor %}
    monitorbindings:
      - monitorname: "tcp_{{ lbvip.name }}_{{ item.port }}"
04. December 2018 · Comments Off on Using Ansible to perform a Netscaler backup · Categories: Ansible, Load Balancing, NetScaler · Tags: , , , ,

The following Ansible playbook is a rewrite of a script from a long time ago to perform backups of a Netscaler. As far as I know, there are no native Ansible or Vendor modules to perform a system backup. Within the playbook I am simply performing a raw call using the URI module against the Nitro API and fetching the backup file.

The following Vendor links contain good/related reference information:

netscaler_systembackup.yaml: a /usr/bin/ansible-playbook -f 10 script text executable, ASCII text

#!/usr/bin/ansible-playbook -f 10
## Ansible playbook to perform a full backup of Netscaler systems
## 2018 (v.01) - Playbook from www.davideaves.com
---
- name: Netscaler full backup
  hosts: netscalers
  connection: local
  gather_facts: False

  vars:

    ansible_connection: "local"
    ansible_python_interpreter: "/usr/bin/env python"

    backup_location: "/srv/nsbackup"

    ns_sys_backup: "/var/ns_sys_backup"

  tasks:

    - name: Check backup file status
      local_action:
        module: stat
        path: "{{ backup_location }}/{{ inventory_hostname }}_{{ lookup('pipe', 'date +%Y%m%d') }}_nsbackup.tgz"
      register: stat_result

    - name: Check backup directory location
      local_action:
        module: file
        path: "{{ backup_location }}"
        state: directory
        mode: 0775
        recurse: yes
      run_once: True
      when: stat_result.stat.exists == False

    - name: Full backup of Netscaler configuration.
      block:

      - name: Create Netscaler system backup
        local_action:
          module: uri
          url: "https://{{ inventory_hostname }}/nitro/v1/config/systembackup?action=create"
          method: POST
          validate_certs: no
          return_content: yes
          headers:
            X-NITRO-USER: "{{ nitro_user | default('nsroot') }}"
            X-NITRO-PASS: "{{ nitro_pass | default('nsroot') }}"
          body_format: json
          body: 
            systembackup:
              filename: "{{ inventory_hostname | hash('md5') }}"
              level: full
              comment: Ansible Generated Backup

      - name: Fetch Netscaler system backup
        local_action:
          module: uri
          url: "https://{{ inventory_hostname }}/nitro/v1/config/systemfile?args=filename:{{ inventory_hostname | hash('md5') }}.tgz,filelocation:{{ ns_sys_backup | replace('/','%2F') }}"
          method: GET
          status_code: 200
          validate_certs: no
          return_content: yes
          headers:
            X-NITRO-USER: "{{ nitro_user | default('nsroot') }}"
            X-NITRO-PASS: "{{ nitro_pass | default('nsroot') }}"
        register: result

      - name: Save Netscaler system backup to backup directory
        local_action: "shell echo '{{ result.json.systemfile[0].filecontent }}' | base64 -d > '{{ backup_location }}/{{ inventory_hostname }}_{{ lookup('pipe', 'date +%Y%m%d') }}_nsbackup.tgz'"

      - name: Chmod saved backup file permissions
        local_action:
          module: file
          path: "{{ backup_location }}/{{ inventory_hostname }}_{{ lookup('pipe', 'date +%Y%m%d') }}_nsbackup.tgz"
          mode: 0644

      always:

      - name: Delete system backup from Netscaler
        local_action:
          module: uri
          url: "https://{{ inventory_hostname }}/nitro/v1/config/systembackup/{{ inventory_hostname | hash('md5') }}.tgz"
          method: DELETE
          validate_certs: no
          return_content: yes
          headers:
            X-NITRO-USER: "{{ nitro_user | default('nsroot') }}"
            X-NITRO-PASS: "{{ nitro_pass | default('nsroot') }}"

      - name: Locate backup files older than 90 days
        local_action:
          module: find
          paths: "{{ backup_location }}"
          age: "1d"
        run_once: true
        register: files_matched

      - name: Purge old backup files
        local_action:
          module: file
          path: "{{ item.path }}"
          state: absent
        run_once: true
        with_items: "{{ files_matched.files }}"

      when: stat_result.stat.exists == False