I built a standalone property source for this. Checking if “ip http” server is in the config is not enough (what if “no ip http server” is present?). Also, it’s a good idea to highlight the ones that have public ip addresses. Also, there’s a mitigation for it that you can check for.
Here’s my PS code. I apply it to “system.sysinfo =~ "IOSXE" && ssh.user && ssh.pass && isCisco()”:
import com.santaba.agent.groovyapi.expect.Expect
import com.santaba.common.groovyapi.expect.expectj.ExpectJ
import java.util.regex.Pattern
import com.jcraft.jsch.JSch
deviceType = hostProps.get("system.categories")
if (deviceType.contains("Cisco")){
def device = null
try {
device = new CiscoIOSDevice(hostProps)
} catch (all) {
println("http.server.error=Fail to initiate device object: ${all.getMessage()}")
throw new RuntimeException("Failure to initiate the device.\nMessage: ${all.getMessage()}")
return 1
}
try {
runningConfig = device.LM_getCollectionOutput("running-config | include ip http se")
}
catch (all) {
println("http.server.error=Fail to fetch running config: ${all.getMessage()}")
throw new RuntimeException("Failure to fetch the running config.\nMessage: ${all.getMessage()}")
return 2
}
ipaddresses = hostProps.get("system.ips", "")
device.Cisco_LogoutDevice()
runningConfig.eachLine{
if (it.contains("ip http server")){
if (it.tokenize(" ")[0] == "no"){println("http.server=false")}
else {println("http.server=true")}
}
if (it.contains("ip http secure-server")){
if (it.tokenize(" ")[0] == "no"){println("http.server.secure=false")}
else {println("http.server.secure=true")}
}
if (it.contains("ip http active-session-modules none")){
if (it.tokenize(" ")[0] == "no"){println("http.server.active_session_modules=false")}
else {println("http.server.active_session_modules=true")}
}
if (it.contains("ip http secure-active-session-modules none")){
if (it.tokenize(" ")[0] == "no"){println("http.server.secure_active_session_modules=false")}
else {println("http.server.secure_active_session_modules=true")}
}
}
privateips = []
publicips = []
ipaddresses.tokenize(",").each{
try{
publicips.add((it =~ /\b(?!(10(\.\d{1,3}){3}|192\.168(\.\d{1,3}){2}|172\.(1[6-9]|2[0-9]|3[0-1])(\.\d{1,3}){2}))(\d{1,3}(\.\d{1,3}){3})\b/)[0][0])
} catch (x){
try {
privateips.add((it =~ /\b(([0-9]{1,3}\.){3}[0-9]{1,3}\b)/)[0][0])
} catch (y){}
}
}
println("http.server.private.ips=${privateips.join(',')}")
println("http.server.public.ips=${publicips.join(',')}")
return 0
} else {
println("http.server.error=Not a supported device type")
}
/**
* Cisco IOS command execution
*/
class CiscoIOSDevice {
private hostProps = null
private host = null
private user = [:]
private pass = [:]
private creds = [:]
private enable_pass = []// If the user has specified an enable password, this will get set further down.
private enable_level = null
private priv_exec_mode = false // Notes what EXEC mode we are in. Initially we assume USER EXEC.
private escalated_privs = false // If we need to escalate our prives from $ to #, we should know.
private parser_view = false
private enum Cisco_ENUMUserPrivilegeState {
USER, PRIV
}
private user_privilege_mode = Cisco_ENUMUserPrivilegeState.USER
private Expect cli
private prompt = ""
private mode = ">"
private full_prompt = ""
// The following variables are set to show what type of IOS device we have, based off the sysinfo property.
private sysinfo = null
private isCisco_ASA = false
private isCisco_WAAS = false
private isCisco_c6800 = false
private isCisco_Cat_3750 = false
private isCisco_Cat = false
private isCisco_PIX = false
private isCisco_Standard = false
private enum CiscoModel {
STANDARD, ASA, WAAS, CATALYST, CATALYST_3750, PIX, C6800
}
private CiscoModel cisco_model
private raw_show_output = null
private show_entries = null
private use_ssh = true // We'll assume ssh unless otherwise specified.
private telnet_port = "23"
private connected = false // Describes the current device ssh connectivity state.
private script_timeout = 60 // seconds
private max_conn_attempts = 3 // Attempt the ssh connection this many times after failure.
private conn_attempt_pause = 5 // Seconds to pause between ssh connection attempts.
private error_message = null
// Get's populated with the last critical error. Used by the init method when throwing an exception
CiscoIOSDevice(hostProps) {
if (!init(hostProps)) {
if (error_message) {
throw new RuntimeException("Initialization process failed! Reason: \"${error_message}\".\n\n")
}
else {
throw new RuntimeException("Initialization process failed!\n\n")
}
}
}
/**
* Called by the Class Constructor, this is the main 'loop' that calls the necessary methods to make a successful connection
* to the device. It is also responsible for multi-attempt logic.
* @param hostProps The hostProps object retrieved from the CollectorDB, and used to extract necessary device properties
* @return TRUE if successful. FALSE otherwise.
*/
private boolean init(hostProps) {
this.hostProps = hostProps
get_lm_props()
Cisco_DetermineIOSDeviceType()
def conn_attempt_num = 1
connected = false
while (!connected && conn_attempt_num < (max_conn_attempts + 1)) {
// Attempt to connect to the device. If we fail, and the failure occurs after connectivity is established,
// we want to disconnect from the device. This is typically means we received a "Permission denied" error.
if (!Cisco_ConnectToDevice()) {
if (connected) {
Cisco_LogoutDevice()
}
error_message = "Could not establish ssh session with host"
}
// Check if we are within a parser view.
if (connected) {
Cisco_ShowParserView()
}
//if ( connected && !escalate_device_privs() ) {
if (connected && !Cisco_EscalateDevicePrivileges()) {
if (connected) {
Cisco_LogoutDevice()
}
error_message = "Could not properly escalate privileges on the device. Ensure that 'ssh.enable.pass' property has been set for the device if necessary"
}
if (connected && !Cisco_SetTerminal()) {
if (connected) {
Cisco_LogoutDevice()
}
}
if (!connected) {
conn_attempt_num++
sleep(conn_attempt_pause * 1000)
}
}
return connected && escalated_privs
}
private get_lm_props() {
// Grab all the hostProp entries
def propKeys = this.hostProps.keySet()
// Iterate over the hostProp entries, determining what values we have set
propKeys.each { key ->
switch (key.toLowerCase()) {
case ~/^config\.forceview$/:
if(this.hostProps.get(key).toLowerCase() == "true") {
this.parser_view = true;
}
break
case ~/^system\.hostname$/:
this.host = this.hostProps.get(key)
break
case ~/ssh\.user$/:
this.creds.put this.hostProps.get(key).toString(), this.hostProps.get("ssh.pass")
if (!this.enable_pass.contains(this.hostProps.get("ssh.pass"))) {
this.enable_pass.add(this.hostProps.get("ssh.pass"))
}
break
case ~/config\.user$/:
this.creds.put this.hostProps.get(key).toString(), this.hostProps.get("config.pass")
if (!this.enable_pass.contains((this.hostProps.get("config.pass")))) {
this.enable_pass.add(this.hostProps.get("config.pass"))
}
break
case ~/ssh\.enable\.pass$/:
if (!this.enable_pass.contains((this.hostProps.get("ssh.enable.pass")))) {
this.enable_pass = ["${this.hostProps.get("ssh.enable.pass")}", *this.enable_pass]
}
break
case ~/config\.enable\.pass$/:
if (!this.enable_pass.contains((this.hostProps.get("config.enable.pass")))) {
this.enable_pass = ["${this.hostProps.get("config.enable.pass")}", *enable_pass]
}
break
case ~/^system\.sysinfo$/:
this.sysinfo = this.hostProps.get(key)
break
case ~/^(config|ssh)\.enable\.level$/:
this.enable_level = this.hostProps.get(key)
break
case ~/^configsource\.use\.telnet$/:
if (this.hostProps.get(key) == "1" || this.hostProps.get(key) == "true" || this.hostProps.get(key) == "yes") {
this.use_ssh = false
}
break
case ~/^configsource\.telnet\.port/:
this.telnet_port = this.hostProps.get(key)
break
case ~/^configsource\.script\.timeout/:
this.script_timeout = (this.hostProps.get(key).toInteger() > 0 && this.hostProps.get(key).toInteger() <= 300) ? this.hostProps.get(key).toInteger() : 60
break
}
}
/*
* In this section of code, we test the values of host, user, and pass -- ensuring
* they did indeed get set. If not, the script will certainly fail, so catch it
* now and provide a useful error message.
*/
if (!this.host) {
System.err << "(debug::fatal) Could not retrieve the system.hostname value. Exiting."
return false
}
if (this.creds.isEmpty()) {
System.err << "(debug::fatal) Could not retrieve the ssh.user/ssh.pass device property. This is required for authentication. Exiting."
return false
}
return true
}
/**
* Takes the SYSINFO value from hostProps and attempts to determine what sort of underlying IOS platform we are working with.
* Some devices behave differently and thus require different logic.
* @return
*/
private Cisco_DetermineIOSDeviceType() {
switch (this.sysinfo.trim()) {
case { it.startsWith("Cisco Adaptive Security Appliance") }:
this.isCisco_ASA = true
this.cisco_model = CiscoModel.ASA
break
case { it.startsWith("Cisco Wide Area Application Services") }:
this.isCisco_WAAS = true
this.cisco_model = CiscoModel.WAAS
break
case { it.startsWith("Cisco IOS Software, c68") }:
this.isCisco_c6800 = true
this.cisco_model = CiscoModel.C6800
break
case { it.contains("C3750") }:
this.isCisco_Cat_3750 = true
this.cisco_model = CiscoModel.CATALYST_3750
break
case ~/^(?:Cisco )?IOS Software(?:\s\[.*\])?,?\s*Catalyst.*, .*Version\s+(.*)/:
this.isCisco_Cat = true
this.isCisco_Standard = true
break
case { it.startsWith("Cisco IOS") }:
this.isCisco_Standard = true
break
case { it.contains("Cisco PIX") }:
this.isCisco_PIX = true
break
default:
this.isCisco_Standard = true
break
}
}
/**
* This method is responsible for establishing the connection to a device.
* @return TRUE if successful. FALSE otherwise
*/
private boolean Cisco_ConnectToDevice() {
def err_state = true
if (this.use_ssh) {
// open an ssh connection and wait for the prompt
this.creds.each { lm_username, lm_password ->
try {
if (err_state) {
this.cli = Expect.open(this.host, lm_username, lm_password, this.script_timeout)
err_state = false
}
}
catch (Exception all) {
return false
}
}
if (this.cli == null) {
return false
}
}
else {
this.creds.each { lm_username, lm_password ->
// telnet session
try {
if (err_state) {
this.cli = Expect.open(this.host, this.telnet_port.toInteger(), this.script_timeout)
err_state = false
this.cli.expect("Username:")
this.cli.send(lm_username + "\n")
this.cli.expect("Password:")
this.cli.send(lm_password + "\n")
}
}
catch (Exception all) {
return false
}
}
}
// Let's ensure that the cli object was created OR that we have performed the previous methods without exiting an error state.
// This ensures that the script doesn't continue on if connectivity was not established, returning control back to the retry loop.
if (this.cli == null || err_state) {
return false
}
// Connection successfully established
connected = true
/* INITIAL MATCH ----------------------------------------------------------------------------------------------------------------------------
In order to accommodate ascii art banners and such, we need essentially look for the last line sent to us after connection.
Everything before that tends to be noise.
*/
def raw_prompt_line = ""
try {
// The following expect method should match on most any cisco ios device prompt. Later we can take this value and parse it for the prompt
this.cli.expect('[a-zA-Z0-9\\-\\_\\.\\/]+[>#][\\s+]?\$')
this.cli.send("\n")
this.cli.expect('[a-zA-Z0-9\\-\\_\\.\\/]+[>#][\\s+]?\$')
// TODO: move this over to precompiled regex matchers
raw_prompt_line = ("${this.cli.matched()}" =~ /([a-zA-Z0-9\-_\.\/]+[>#])[\s+]?$/)[0][1]
}
catch (Exception all) {
return false
}
/* -- PROMPT DETECTION ---------------------------------------------------------------------------------------------------------------------
We have progressed this far, we should be able to deconstruct the prompt line into 'prompt' and 'mode'
*/
try {
this.prompt = raw_prompt_line[0..-2]
this.mode = raw_prompt_line[-1]
this.full_prompt = this.prompt + this.mode
}
catch (Exception all) {
return false
}
// Check to see what the previous expect command matched. This will us which user mode we have been dropped into.
if (this.mode == "#") {
this.priv_exec_mode = true
}
return true
}
/**
* This method will attempt to escalate our user privileges to PRIV
* @return boolean Will return TRUE if successful; FALSE otherwise.
*/
private boolean Cisco_EscalateDevicePrivileges() {
if (this.user_privilege_mode == Cisco_ENUMUserPrivilegeState.PRIV) {
this.escalated_privs = true; return true
}
if (this.priv_exec_mode) {
escalated_privs = true; return true
}
def exit_loop = false
def password_position = 0
while (!exit_loop) {
def response
if (password_position == 0) {
this.cli.send("enable\n")
}
try {
this.cli.expect("[Pp]assword:", "${prompt}\\s*#", full_prompt)
response = this.cli.matched()
}
catch (Exception e) { }
if (response =~ /${this.prompt}\s*#/) {
this.mode = "#"
this.full_prompt = Pattern.quote(response)
this.priv_exec_mode = true
this.user_privilege_mode = Cisco_ENUMUserPrivilegeState.PRIV
this.escalated_privs = true
exit_loop = true
}
else {
this.cli.send(enable_pass[password_position] + "\n")
}
if (password_position == enable_pass.size()) {
exit_loop = true
}
else {
password_position++
}
}
return (this.user_privilege_mode == Cisco_ENUMUserPrivilegeState.PRIV)
}
private Cisco_SetTerminal() {
try {
if (this.isCisco_WAAS || this.isCisco_Standard || this.isCisco_c6800 || this.isCisco_Cat_3750) {
this.cli.send("terminal length 0\n")
this.cli.expect(this.full_prompt)
this.cli.send("terminal width 0\n")
this.cli.expect("${this.full_prompt}\\s*\$")
}
else {
this.cli.send("terminal pager 0\n")
this.cli.expect("${this.full_prompt}\\s*\$")
}
}
catch (Exception all) {
return false
}
return true
}
private Cisco_ShowParserView() {
try {
this.cli.send("show parser view\n")
this.cli.expect(this.full_prompt, "No view is active")
this.cli.before()
}
catch (Exception all) {
return false
}
if (this.cli.before().toString().contains("Current view is")) {
this.parser_view = true
}
}
def Cisco_GetShowCommands() {
this.cli.send("show ?")
try {
if (this.isCisco_ASA || this.isCisco_PIX) {
this.cli.expect("${this.full_prompt} show")
}
else if (this.isCisco_WAAS || this.isCisco_c6800) {
this.cli.expect(this.full_prompt)
}
else {
this.cli.expect("${this.full_prompt}show")
}
}
catch (Exception all) {
return false
}
try {
this.raw_show_output = this.cli.before()
}
catch (Exception all) {
return false
}
return this.raw_show_output
}
def Cisco_ParseShowCommands() {
this.raw_show_output.eachLine() { line ->
if (!show_entries) {
show_entries = [:]
}
switch (line.trim()) {
case { it.isEmpty() }:
break
case { it.startsWith("show ?") }:
break
case { it.startsWith(this.full_prompt) }:
break
default:
try {
def cmd = line.trim().tokenize()[0]
def desc = line.trim().tokenize()[1..-1].join(" ")
this.show_entries << ["${cmd}": "${desc.trim()}"]
}
catch (all) {
// some sort of odd output. ignore.
break
}
}
}
}
def LM_getActiveDiscoveryOutput(desired_show_commands) {
// Need to ensure that we are actually connected to the device before proceeding
if (!connected) {
return false
}
def output = ""
if (!this.raw_show_output) {
this.Cisco_GetShowCommands()
}
if (!this.show_entries) {
this.Cisco_ParseShowCommands()
}
this.show_entries.each { key, value ->
if (desired_show_commands.contains(key.toString())) {
output += "${key}##${key}##${value}\n"
}
}
return output
}
def LM_getCollectionOutput(show_operator) {
// Need to ensure that we are actually connected to the device before proceeding
if (!connected) {
return false
}
def output = ""
def response = null
// In the event that we are within a parser view, we need to add 'view full' to a 'running-config' request.
if (parser_view && show_operator == "running-config") {
show_operator += " view full"
}
def conn_attempt_num = 1
while ((conn_attempt_num < (this.max_conn_attempts + 1)) && response == null) {
conn_attempt_num++
// Send the requested show command to the device
if (this.isCisco_c6800 || this.isCisco_Cat_3750 || this.isCisco_PIX || this.isCisco_ASA || this.isCisco_Cat) {
if (this.escalated_privs) {
this.cli.send("show ${show_operator}\nexit\nexit\n")
}
else {
this.cli.send("show ${show_operator}\nexit\n")
}
}
else {
this.cli.send("show ${show_operator}\n")
}
// Attempt to match
try {
if (isCisco_c6800 || isCisco_Cat_3750 || this.isCisco_PIX || this.isCisco_ASA) {
this.cli.expectClose()
response = this.cli.stdout()
}
else {
// Match prompt on a line by itself with or without surrounding whitespace.
// Obviate case where config returns with embedded prompt and terminates
// the config prematurely.
this.cli.expect(/^\s*${this.full_prompt}\s*$/)
response = this.cli.before()
}
}
catch (Exception all) { }
}
response = response.trim()
try {
def found_current_config_entry = true
if (this.isCisco_c6800 || this.isCisco_Cat_3750 || this.isCisco_PIX || this.isCisco_ASA) {
found_current_config_entry = false
}
response.eachLine() { line ->
switch (line) {
case ~/^(.show|show) ${show_operator}/:
if (!found_current_config_entry) {
found_current_config_entry = true
}
break
case ~/^: Saved/:
if (!found_current_config_entry) {
found_current_config_entry = true
}
output += "${line}\n"
break
case ~/^${full_prompt}\s?show ${show_operator}.*/:
if (!found_current_config_entry) {
found_current_config_entry = true
}
break
case ~/^Building configuration.*/:
break
case ~/.+\d+ (day|days|hour|hours|year|years|week|weeks|minute|minutes).*/:
break
case ~/(^[Ss]witch\s|^)[Uu]ptime.*/:
break
case ~/^${full_prompt}.*/:
break
case ~/^(Current configuration|Using \d+ out of \d+ bytes).*/:
found_current_config_entry = true
break
case ~/^ntp clock-period.*/:
break
case ~/^Logoff/:
break
case ~/^Load for five secs.*/:
break
case ~/^Time source is.*/:
break
case ~/^\w+\s+\w+\s+\d+\s\d+:\d+:\d+\.\d+\s+\w+/:
break
default:
if (found_current_config_entry) {
output += "${line}\n"
}
break
}
}
}
catch (Exception all) { }
return output
}
def Cisco_LogoutDevice() {
// If we were never connected, just return true. Most likely poor logic that allowed us to call this method even though we never established a connection.
if (!connected) {
return true
}
if (this.isCisco_c6800 || this.isCisco_Cat_3750 || this.isCisco_PIX || this.isCisco_ASA || this.isCisco_Cat) {
return true
} // The c6800 and Catalyst 3750 series routines will have already exited the device.
// logout from the device
if (this.escalated_privs) {
this.cli.send("\nexit\nexit\n")
this.cli.expect("\$")
}
else {
this.cli.send("\nexit\n");
this.cli.expect("#", "${this.full_prompt}exit")
}
connected = false
// close the ssh connection handle then print the config
try {
this.cli.expectClose()
}
catch (Exception all) {
return false
}
}
}