Skip to content

Commit

Permalink
Autodetect NOS (fixes #13)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kircheneer committed Apr 13, 2021
1 parent a96b972 commit 3d6f7fe
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 33 deletions.
8 changes: 5 additions & 3 deletions netlint/checks/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import functools
import typing

from netlint.utils import NOS


class CheckResult(typing.NamedTuple):
"""Result of a single check."""
Expand All @@ -26,15 +28,15 @@ class Checker:
"""Class to handle check execution."""

# Map NOSes to applicable checks
checks: typing.Dict[str, typing.List[CheckFunctionTuple]] = {}
checks: typing.Dict[NOS, typing.List[CheckFunctionTuple]] = {}

def __init__(self) -> None:
# Map check name to check result (NOS-agnostic)
self.check_results: typing.Dict[str, typing.Optional[CheckResult]] = {}

@classmethod
def register(
cls, apply_to: typing.List[str], name: str
cls, apply_to: typing.List[NOS], name: str
) -> typing.Callable[
[typing.Callable[[typing.List[str]], typing.Optional[CheckResult]]],
typing.Callable[[typing.List[str]], typing.Optional[CheckResult]],
Expand Down Expand Up @@ -80,7 +82,7 @@ def wrapper(config: typing.List[str]) -> typing.Optional[CheckResult]:

return decorator

def run_checks(self, configuration: typing.List[str], nos: str) -> bool:
def run_checks(self, configuration: typing.List[str], nos: NOS) -> bool:
"""
Run all the registered checks on the configuration.
Expand Down
10 changes: 6 additions & 4 deletions netlint/checks/cisco_ios/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
"check_plaintext_passwords",
"check_console_password",
"check_ip_http_server",
"check_password_hash_strength",
]

from netlint.checks.cisco_ios.utils import get_password_hash_algorithm
from netlint.utils import NOS


@Checker.register(apply_to=["cisco_ios"], name="IOS101")
@Checker.register(apply_to=[NOS.CISCO_IOS], name="IOS101")
def check_plaintext_passwords(config: typing.List[str]) -> typing.Optional[CheckResult]:
"""Check if there are any plaintext passwords in the configuration."""
parsed_config = CiscoConfParse(config)
Expand All @@ -39,7 +41,7 @@ def check_plaintext_passwords(config: typing.List[str]) -> typing.Optional[Check
return None


@Checker.register(apply_to=["cisco_ios"], name="IOS102")
@Checker.register(apply_to=[NOS.CISCO_IOS], name="IOS102")
def check_ip_http_server(config: typing.List[str]) -> typing.Optional[CheckResult]:
"""Check if the http server is enabled."""
parsed_config = CiscoConfParse(config)
Expand All @@ -50,7 +52,7 @@ def check_ip_http_server(config: typing.List[str]) -> typing.Optional[CheckResul
return None


@Checker.register(apply_to=["cisco_ios"], name="IOS103")
@Checker.register(apply_to=[NOS.CISCO_IOS], name="IOS103")
def check_console_password(config: typing.List[str]) -> typing.Optional[CheckResult]:
"""Check for authentication on the console line."""
parsed_config = CiscoConfParse(config)
Expand All @@ -72,7 +74,7 @@ def check_console_password(config: typing.List[str]) -> typing.Optional[CheckRes
return None


@Checker.register(apply_to=["cisco_ios"], name="IOS104")
@Checker.register(apply_to=[NOS.CISCO_IOS], name="IOS104")
def check_password_hash_strength(
config: typing.List[str],
) -> typing.Optional[CheckResult]:
Expand Down
9 changes: 5 additions & 4 deletions netlint/checks/cisco_nxos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
]

from netlint.checks.constants import bogus_as_numbers
from netlint.utils import NOS


@Checker.register(apply_to=["cisco_nxos"], name="NXOS101")
@Checker.register(apply_to=[NOS.CISCO_NXOS], name="NXOS101")
def check_telnet_enabled(config: typing.List[str]) -> typing.Optional[CheckResult]:
"""Check if the telnet feature is explicitly enabled."""
parsed_config = CiscoConfParse(config)
Expand All @@ -26,7 +27,7 @@ def check_telnet_enabled(config: typing.List[str]) -> typing.Optional[CheckResul
return None


@Checker.register(apply_to=["cisco_nxos"], name="NXOS102")
@Checker.register(apply_to=[NOS.CISCO_NXOS], name="NXOS102")
def check_routing_protocol_enabled_and_used(
config: typing.List[str],
) -> typing.Optional[CheckResult]:
Expand All @@ -46,7 +47,7 @@ def check_routing_protocol_enabled_and_used(
return None


@Checker.register(apply_to=["cisco_nxos"], name="NXOS103")
@Checker.register(apply_to=[NOS.CISCO_NXOS], name="NXOS103")
def check_password_strength(config: typing.List[str]) -> typing.Optional[CheckResult]:
"""Check if the password strength check has been disabled."""
parsed_config = CiscoConfParse(config)
Expand All @@ -57,7 +58,7 @@ def check_password_strength(config: typing.List[str]) -> typing.Optional[CheckRe
return None


@Checker.register(apply_to=["cisco_nxos"], name="NXOS104")
@Checker.register(apply_to=[NOS.CISCO_NXOS], name="NXOS104")
def check_bogus_as(config: typing.List[str]) -> typing.Optional[CheckResult]:
"""Check if any bogus autonomous system is used in the configuration."""
parsed_config = CiscoConfParse(config)
Expand Down
4 changes: 3 additions & 1 deletion netlint/checks/various/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

__all__ = ["check_default_snmp_communities"]

from netlint.utils import NOS

@Checker.register(apply_to=["cisco_ios", "cisco_nxos"], name="VAR101")

@Checker.register(apply_to=[NOS.CISCO_IOS, NOS.CISCO_NXOS], name="VAR101")
def check_default_snmp_communities(
config: typing.List[str],
) -> typing.Optional[CheckResult]:
Expand Down
54 changes: 34 additions & 20 deletions netlint/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from netlint.checks.checker import Checker
from netlint.types import JSONOutputDict
from netlint.utils import smart_open, style
from netlint.utils import smart_open, style, detect_nos, NOS

CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
DEFAULT_CONFIG = "pyproject.toml"
Expand Down Expand Up @@ -87,11 +87,6 @@ def configure(
type=click.Choice(["json", "normal"]),
help="The format of the output data (--format json implies --quiet).",
)
@click.option(
"--nos",
type=click.Choice(["cisco_ios", "cisco_nxos"]),
help="The NOS the configuration(s) is/are for.",
)
@click.option(
"--select",
type=str,
Expand Down Expand Up @@ -139,7 +134,6 @@ def cli(
prefix: str,
output: typing.Optional[str],
format_: str,
nos: str,
select: typing.Optional[str],
exclude: typing.Optional[str],
quiet: bool,
Expand All @@ -160,24 +154,42 @@ def cli(

checker_instance = Checker()

configurations: typing.Dict[str, typing.List[str]] = {"default": []}

input_path = Path(path)
nos_mapping = {"default": None}
if input_path.is_file():
with open(input_path) as f:
configurations["default"] = f.readlines()
nos_mapping["default"] = detect_nos(configurations["default"])
elif input_path.is_dir():
path_items = input_path.glob(glob)
for item in path_items:
dict_key = str(item)
with open(item) as f:
configurations[dict_key] = f.readlines()
nos_mapping[dict_key] = detect_nos(configurations[dict_key])

if select:
selected_checks = []
for check in checker_instance.checks[nos]:
for check in checker_instance.checks[nos_mapping["default"]]:
if check.name in select.split(","):
selected_checks.append(check)
checker_instance.checks[nos] = selected_checks
checker_instance.checks[nos_mapping["default"]] = selected_checks
elif exclude:
excluded_checks = []
for check in checker_instance.checks[nos]:
if check.name in exclude.split(","):
excluded_checks.append(check)
# Iterate over each unique NOS
for nos in set(nos_mapping.values()):
for check in checker_instance.checks[nos]:
if check.name in exclude.split(","):
excluded_checks.append(check)
for check in excluded_checks:
checker_instance.checks[nos].remove(check)

input_path = Path(path)
checker_instance.checks[nos_mapping["default"]].remove(check)

if input_path.is_file():
processed_config = check_config(checker_instance, input_path, nos)
processed_config = check_config(
checker_instance, configurations["default"], nos_mapping["default"]
)
if processed_config:
has_errors = True
with smart_open(output) as f:
Expand All @@ -189,7 +201,9 @@ def cli(
path_items = input_path.glob(glob)
processed_configs: typing.Dict[str, JSONOutputDict] = {}
for item in path_items:
processed_configs[str(item)] = check_config(checker_instance, item, nos)
processed_configs[str(item)] = check_config(
checker_instance, configurations[str(item)], nos_mapping[str(item)]
)
if processed_configs:
has_errors = True
with smart_open(output) as f:
Expand Down Expand Up @@ -219,12 +233,12 @@ def cli(
ctx.exit(0)


def check_config(checker_instance: Checker, path: Path, nos: str) -> JSONOutputDict:
def check_config(
checker_instance: Checker, configuration: typing.List[str], nos: NOS
) -> JSONOutputDict:
"""Run checks on config at a given path."""
return_value: JSONOutputDict = {}

with open(path, "r") as f:
configuration = f.readlines()
checker_instance.run_checks(configuration, nos)

for check, result in checker_instance.check_results.items():
Expand Down
19 changes: 19 additions & 0 deletions netlint/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import contextlib
import sys
import typing
from enum import Enum

import click

Expand All @@ -28,3 +29,21 @@ def style(message: str, quiet: bool, **kwargs: typing.Any) -> str:
return message
else:
return click.style(message, **kwargs)


class NOS(Enum):
"""Overview of different available NOSes."""

CISCO_IOS = "cisco_ios"
CISCO_NXOS = "cisco_nxos"


def detect_nos(configuration: typing.List[str]) -> NOS:
"""Automatically detect the NOS in the configuration.
Very rudimentary as of now, will get more complex support for more NOSes is added.
"""
for line in configuration:
if "feature" in line:
return NOS.CISCO_NXOS
return NOS.CISCO_IOS
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def test_lint_basic(quiet: bool, format_: str):

cisco_ios_faulty_conf = TESTS_DIR / "cisco_ios" / "configurations" / "faulty.conf"

commands = ["--nos", "cisco_ios", str(cisco_ios_faulty_conf)]
commands = [str(cisco_ios_faulty_conf)]

if quiet:
commands.insert(0, "--quiet")
Expand Down

0 comments on commit 3d6f7fe

Please sign in to comment.