diff options
Diffstat (limited to 'suricata/update/commands')
-rw-r--r-- | suricata/update/commands/__init__.py | 23 | ||||
-rw-r--r-- | suricata/update/commands/addsource.py | 72 | ||||
-rw-r--r-- | suricata/update/commands/checkversions.py | 83 | ||||
-rw-r--r-- | suricata/update/commands/disablesource.py | 40 | ||||
-rw-r--r-- | suricata/update/commands/enablesource.py | 162 | ||||
-rw-r--r-- | suricata/update/commands/listsources.py | 116 | ||||
-rw-r--r-- | suricata/update/commands/removesource.py | 49 | ||||
-rw-r--r-- | suricata/update/commands/updatesources.py | 105 |
8 files changed, 650 insertions, 0 deletions
diff --git a/suricata/update/commands/__init__.py b/suricata/update/commands/__init__.py new file mode 100644 index 0000000..e75c80a --- /dev/null +++ b/suricata/update/commands/__init__.py @@ -0,0 +1,23 @@ +# Copyright (C) 2017 Open Information Security Foundation +# +# You can copy, redistribute or modify this Program under the terms of +# the GNU General Public License version 2 as published by the Free +# Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# version 2 along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +from suricata.update.commands import addsource +from suricata.update.commands import listsources +from suricata.update.commands import updatesources +from suricata.update.commands import enablesource +from suricata.update.commands import disablesource +from suricata.update.commands import removesource +from suricata.update.commands import checkversions diff --git a/suricata/update/commands/addsource.py b/suricata/update/commands/addsource.py new file mode 100644 index 0000000..a87095c --- /dev/null +++ b/suricata/update/commands/addsource.py @@ -0,0 +1,72 @@ +# Copyright (C) 2017 Open Information Security Foundation +# +# You can copy, redistribute or modify this Program under the terms of +# the GNU General Public License version 2 as published by the Free +# Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# version 2 along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +from __future__ import print_function + +import logging + +from suricata.update import config +from suricata.update import sources + +try: + input = raw_input +except: + pass + +logger = logging.getLogger() + + +def register(parser): + parser.add_argument("name", metavar="<name>", nargs="?", + help="Name of source") + parser.add_argument("url", metavar="<url>", nargs="?", help="Source URL") + parser.add_argument("--http-header", metavar="<http-header>", + help="Additional HTTP header to add to requests") + parser.add_argument("--no-checksum", action="store_false", + help="Skips downloading the checksum URL") + parser.set_defaults(func=add_source) + + +def add_source(): + args = config.args() + + if args.name: + name = args.name + else: + while True: + name = input("Name of source: ").strip() + if name: + break + + if sources.source_name_exists(name): + logger.error("A source with name %s already exists.", name) + return 1 + + if args.url: + url = args.url + else: + while True: + url = input("URL: ").strip() + if url: + break + + checksum = args.no_checksum + + header = args.http_header if args.http_header else None + + source_config = sources.SourceConfiguration( + name, header=header, url=url, checksum=checksum) + sources.save_source_config(source_config) diff --git a/suricata/update/commands/checkversions.py b/suricata/update/commands/checkversions.py new file mode 100644 index 0000000..3492317 --- /dev/null +++ b/suricata/update/commands/checkversions.py @@ -0,0 +1,83 @@ +# Copyright (C) 2019 Open Information Security Foundation +# +# You can copy, redistribute or modify this Program under the terms of +# the GNU General Public License version 2 as published by the Free +# Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# version 2 along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +import os.path +import logging +from suricata.update import sources, engine + +logger = logging.getLogger() + + +def is_gt(v1, v2): + if v1.full == v2.full: + return False + + if v1.major < v2.major: + return False + elif v1.major > v2.major: + return True + + if v1.minor < v2.minor: + return False + elif v1.minor > v2.minor: + return True + + if v1.patch < v2.patch: + return False + + return True + + +def register(parser): + parser.set_defaults(func=check_version) + + +def check_version(suricata_version): + if "dev" in suricata_version.full: + logger.warning("Development version of Suricata found: %s. " + "Skipping version check.", suricata_version.full) + return + + index_filename = sources.get_index_filename() + if not os.path.exists(index_filename): + logger.warning("No index exists, will use bundled index.") + logger.warning("Please run suricata-update update-sources.") + index = sources.Index(index_filename) + version = index.get_versions() + recommended = engine.parse_version(version["suricata"]["recommended"]) + if not recommended: + logger.error("Recommended version was not parsed properly") + sys.exit(1) + # In case index is out of date + if is_gt(suricata_version, recommended): + return + # Evaluate if the installed version is present in index + upgrade_version = version["suricata"].get(suricata_version.short) + if not upgrade_version: + logger.warning("Suricata version %s has reached EOL. Please upgrade to %s.", + suricata_version.full, recommended.full) + return + if suricata_version.full == upgrade_version: + logger.info("Suricata version %s is up to date", suricata_version.full) + elif upgrade_version == recommended.full: + logger.warning( + "Suricata version %s is outdated. Please upgrade to %s.", + suricata_version.full, recommended.full) + else: + logger.warning( + "Suricata version %s is outdated. Please upgrade to %s or %s.", + suricata_version.full, upgrade_version, recommended.full) + diff --git a/suricata/update/commands/disablesource.py b/suricata/update/commands/disablesource.py new file mode 100644 index 0000000..6a64a7b --- /dev/null +++ b/suricata/update/commands/disablesource.py @@ -0,0 +1,40 @@ +# Copyright (C) 2017 Open Information Security Foundation +# +# You can copy, redistribute or modify this Program under the terms of +# the GNU General Public License version 2 as published by the Free +# Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# version 2 along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +from __future__ import print_function + +import os +import logging + +from suricata.update import config +from suricata.update import sources + +logger = logging.getLogger() + +def register(parser): + parser.add_argument("name") + parser.set_defaults(func=disable_source) + +def disable_source(): + name = config.args().name + filename = sources.get_enabled_source_filename(name) + if not os.path.exists(filename): + logger.debug("Filename %s does not exist.", filename) + logger.warning("Source %s is not enabled.", name) + return 0 + logger.debug("Renaming %s to %s.disabled.", filename, filename) + os.rename(filename, "%s.disabled" % (filename)) + logger.info("Source %s has been disabled", name) diff --git a/suricata/update/commands/enablesource.py b/suricata/update/commands/enablesource.py new file mode 100644 index 0000000..53bb68a --- /dev/null +++ b/suricata/update/commands/enablesource.py @@ -0,0 +1,162 @@ +# Copyright (C) 2017 Open Information Security Foundation +# +# You can copy, redistribute or modify this Program under the terms of +# the GNU General Public License version 2 as published by the Free +# Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# version 2 along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +from __future__ import print_function + +import os +import logging + +import yaml + +from suricata.update import config +from suricata.update import sources + +try: + input = raw_input +except: + pass + +logger = logging.getLogger() + +default_source = "et/open" + +def register(parser): + parser.add_argument("name") + parser.add_argument("params", nargs="*", metavar="param=val") + parser.set_defaults(func=enable_source) + +def enable_source(): + name = config.args().name + update_params = False + + # Check if source is already enabled. + enabled_source_filename = sources.get_enabled_source_filename(name) + if os.path.exists(enabled_source_filename): + logger.warning("The source %s is already enabled.", name) + update_params = True + + # First check if this source was previous disabled and then just + # re-enable it. + disabled_source_filename = sources.get_disabled_source_filename(name) + if os.path.exists(disabled_source_filename): + logger.info("Re-enabling previously disabled source for %s.", name) + os.rename(disabled_source_filename, enabled_source_filename) + update_params = True + + if not os.path.exists(sources.get_index_filename()): + logger.warning("Source index does not exist, will use bundled one.") + logger.warning("Please run suricata-update update-sources.") + + source_index = sources.load_source_index(config) + + if not name in source_index.get_sources() and not name in sources.get_sources_from_dir(): + logger.error("Unknown source: %s", name) + return 1 + + # Parse key=val options. + opts = {} + for param in config.args().params: + key, val = param.split("=", 1) + opts[key] = val + + params = {} + if update_params: + source = yaml.safe_load(open(sources.get_enabled_source_filename(name), "rb")) + else: + source = source_index.get_sources()[name] + + if "params" in source: + params = source["params"] + for old_param in source["params"]: + if old_param in opts and source["params"][old_param] != opts[old_param]: + logger.info("Updating source parameter '%s': '%s' -> '%s'." % ( + old_param, source["params"][old_param], opts[old_param])) + params[old_param] = opts[old_param] + + if "subscribe-url" in source: + print("The source %s requires a subscription. Subscribe here:" % (name)) + print(" %s" % source["subscribe-url"]) + + if "parameters" in source: + for param in source["parameters"]: + if param in opts: + params[param] = opts[param] + else: + prompt = source["parameters"][param]["prompt"] + while True: + r = input("%s (%s): " % (prompt, param)) + r = r.strip() + if r: + break + params[param] = r.strip() + + if "checksum" in source: + checksum = source["checksum"] + else: + checksum = source.get("checksum", True) + + new_source = sources.SourceConfiguration( + name, params=params, checksum=checksum) + + # If the source directory does not exist, create it. Also create + # the default rule-source of et/open, unless the source being + # enabled replaces it. + source_directory = sources.get_source_directory() + if not os.path.exists(source_directory): + try: + logger.info("Creating directory %s", source_directory) + os.makedirs(source_directory) + except Exception as err: + logger.error( + "Failed to create directory %s: %s", source_directory, err) + return 1 + + if "replaces" in source and default_source in source["replaces"]: + logger.debug( + "Not enabling default source as selected source replaces it") + elif new_source.name == default_source: + logger.debug( + "Not enabling default source as selected source is the default") + else: + logger.info("Enabling default source %s", default_source) + if not source_index.get_source_by_name(default_source): + logger.error("Default source %s not in index", default_source) + else: + default_source_config = sources.SourceConfiguration( + default_source) + write_source_config(default_source_config, True) + + write_source_config(new_source, True) + logger.info("Source %s enabled", new_source.name) + + if "replaces" in source: + for replaces in source["replaces"]: + filename = sources.get_enabled_source_filename(replaces) + if os.path.exists(filename): + logger.info( + "Removing source %s as its replaced by %s", replaces, + new_source.name) + logger.debug("Deleting %s", filename) + os.unlink(filename) + +def write_source_config(config, enabled): + if enabled: + filename = sources.get_enabled_source_filename(config.name) + else: + filename = sources.get_disabled_source_filename(config.name) + with open(filename, "w") as fileobj: + logger.debug("Writing %s", filename) + fileobj.write(yaml.safe_dump(config.dict(), default_flow_style=False)) diff --git a/suricata/update/commands/listsources.py b/suricata/update/commands/listsources.py new file mode 100644 index 0000000..d35c3cd --- /dev/null +++ b/suricata/update/commands/listsources.py @@ -0,0 +1,116 @@ +# Copyright (C) 2017 Open Information Security Foundation +# +# You can copy, redistribute or modify this Program under the terms of +# the GNU General Public License version 2 as published by the Free +# Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# version 2 along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +from __future__ import print_function + +import logging + +from suricata.update import config +from suricata.update import sources +from suricata.update import util +from suricata.update import exceptions + +logger = logging.getLogger() + +def register(parser): + parser.add_argument("--free", action="store_true", + default=False, help="List all freely available sources") + parser.add_argument("--enabled", action="store_true", + help="List all enabled sources") + parser.add_argument("--all", action="store_true", + help="List all sources (including deprecated and obsolete)") + parser.set_defaults(func=list_sources) + +def list_sources(): + enabled = config.args().enabled or \ + config.args().subcommand == "list-enabled-sources" + + if enabled: + found = False + + # First list sources from the main config. + config_sources = config.get("sources") + if config_sources: + found = True + print("From %s:" % (config.filename)) + for source in config_sources: + print(" - %s" % (source)) + + # And local files. + local = config.get("local") + if local: + found = True + print("Local files/directories:") + for filename in local: + print(" - %s" % (filename)) + + enabled_sources = sources.get_enabled_sources() + if enabled_sources: + found = True + print("Enabled sources:") + for source in enabled_sources.values(): + print(" - %s" % (source["source"])) + + # If no enabled sources were found, log it. + if not found: + logger.warning("No enabled sources.") + return 0 + + free_only = config.args().free + if not sources.source_index_exists(config): + logger.warning("Source index does not exist, will use bundled one.") + logger.warning("Please run suricata-update update-sources.") + + index = sources.load_source_index(config) + for name, source in index.get_sources().items(): + is_not_free = source.get("subscribe-url") + if free_only and is_not_free: + continue + if not config.args().all: + if source.get("deprecated") is not None or \ + source.get("obsolete") is not None: + continue + print("%s: %s" % (util.bright_cyan("Name"), util.bright_magenta(name))) + print(" %s: %s" % ( + util.bright_cyan("Vendor"), util.bright_magenta(source["vendor"]))) + print(" %s: %s" % ( + util.bright_cyan("Summary"), util.bright_magenta(source["summary"]))) + print(" %s: %s" % ( + util.bright_cyan("License"), util.bright_magenta(source["license"]))) + if "tags" in source: + print(" %s: %s" % ( + util.bright_cyan("Tags"), + util.bright_magenta(", ".join(source["tags"])))) + if "replaces" in source: + print(" %s: %s" % ( + util.bright_cyan("Replaces"), + util.bright_magenta(", ".join(source["replaces"])))) + if "parameters" in source: + print(" %s: %s" % ( + util.bright_cyan("Parameters"), + util.bright_magenta(", ".join(source["parameters"])))) + if "subscribe-url" in source: + print(" %s: %s" % ( + util.bright_cyan("Subscription"), + util.bright_magenta(source["subscribe-url"]))) + if "deprecated" in source: + print(" %s: %s" % ( + util.orange("Deprecated"), + util.bright_magenta(source["deprecated"]))) + if "obsolete" in source: + print(" %s: %s" % ( + util.orange("Obsolete"), + util.bright_magenta(source["obsolete"]))) diff --git a/suricata/update/commands/removesource.py b/suricata/update/commands/removesource.py new file mode 100644 index 0000000..f75d5ca --- /dev/null +++ b/suricata/update/commands/removesource.py @@ -0,0 +1,49 @@ +# Copyright (C) 2017 Open Information Security Foundation +# +# You can copy, redistribute or modify this Program under the terms of +# the GNU General Public License version 2 as published by the Free +# Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# version 2 along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +from __future__ import print_function + +import os +import logging + +from suricata.update import config +from suricata.update import sources + +logger = logging.getLogger() + +def register(parser): + parser.add_argument("name") + parser.set_defaults(func=remove_source) + +def remove_source(): + name = config.args().name + + enabled_source_filename = sources.get_enabled_source_filename(name) + if os.path.exists(enabled_source_filename): + logger.debug("Deleting file %s.", enabled_source_filename) + os.remove(enabled_source_filename) + logger.info("Source %s removed, previously enabled.", name) + return 0 + + disabled_source_filename = sources.get_disabled_source_filename(name) + if os.path.exists(disabled_source_filename): + logger.debug("Deleting file %s.", disabled_source_filename) + os.remove(disabled_source_filename) + logger.info("Source %s removed, previously disabled.", name) + return 0 + + logger.warning("Source %s does not exist.", name) + return 1 diff --git a/suricata/update/commands/updatesources.py b/suricata/update/commands/updatesources.py new file mode 100644 index 0000000..06a0d11 --- /dev/null +++ b/suricata/update/commands/updatesources.py @@ -0,0 +1,105 @@ +# Copyright (C) 2017 Open Information Security Foundation +# +# You can copy, redistribute or modify this Program under the terms of +# the GNU General Public License version 2 as published by the Free +# Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# version 2 along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +from __future__ import print_function + +import io +import logging +import os + +import yaml +from suricata.update import config, exceptions, net, sources + +logger = logging.getLogger() + + +def register(parser): + parser.set_defaults(func=update_sources) + + +def get_initial_content(): + initial_content = None + if os.path.exists(local_index_filename): + with open(local_index_filename, "r") as stream: + initial_content = yaml.safe_load(stream) + return initial_content + + +def get_sources(before, after): + all_sources = {source: after[source] + for source in after if source not in before} + return all_sources + + +def log_sources(sources_map): + for name, all_sources in sources_map.items(): + if not all_sources: + continue + for source in all_sources: + logger.info("Source %s was %s", source, name) + + +def compare_sources(initial_content, final_content): + if not initial_content: + logger.info("Adding all sources") + return + if initial_content == final_content: + logger.info("No change in sources") + return + initial_sources = initial_content.get("sources") + final_sources = final_content.get("sources") + added_sources = get_sources(before=initial_sources, after=final_sources) + removed_sources = get_sources(before=final_sources, after=initial_sources) + log_sources(sources_map={"added": added_sources, + "removed": removed_sources}) + for source in set(initial_sources) & set(final_sources): + if initial_sources[source] != final_sources[source]: + logger.info("Source %s was changed", source) + + +def write_and_compare(initial_content, fileobj): + try: + with open(local_index_filename, "wb") as outobj: + outobj.write(fileobj.getvalue()) + except IOError as ioe: + logger.error("Failed to open directory: %s", ioe) + return 1 + with open(local_index_filename, "rb") as stream: + final_content = yaml.safe_load(stream) + compare_sources(initial_content, final_content) + logger.info("Saved %s", local_index_filename) + + +def update_sources(): + global local_index_filename + local_index_filename = sources.get_index_filename() + initial_content = get_initial_content() + with io.BytesIO() as fileobj: + url = sources.get_source_index_url() + logger.info("Downloading %s", url) + try: + net.get(url, fileobj) + except Exception as err: + raise exceptions.ApplicationError( + "Failed to download index: %s: %s" % (url, err)) + if not os.path.exists(config.get_cache_dir()): + try: + os.makedirs(config.get_cache_dir()) + except Exception as err: + logger.error("Failed to create directory %s: %s", + config.get_cache_dir(), err) + return 1 + write_and_compare(initial_content=initial_content, fileobj=fileobj) |