python: remove python implementation of suricatasc/suricatactl

pull/12685/head
Jason Ish 1 year ago committed by Victor Julien
parent 8fa347410e
commit a0089190df

@ -1,28 +1,13 @@
LIBS = \
suricata/__init__.py \
suricata/config/__init__.py \
suricata/ctl/__init__.py \
suricata/ctl/filestore.py \
suricata/ctl/loghandler.py \
suricata/ctl/main.py \
suricata/ctl/test_filestore.py \
suricata/sc/__init__.py \
suricata/sc/specs.py \
suricata/sc/suricatasc.py \
suricatasc/__init__.py
suricata/config/__init__.py
EXTRA_DIST = $(LIBS) bin suricata/config/defaults.py
EXTRA_DIST = $(LIBS) suricata/config/defaults.py
if HAVE_PYTHON
install-exec-local:
install -d -m 0755 "$(DESTDIR)$(prefix)/lib/suricata/python/suricata/config"
install -d -m 0755 "$(DESTDIR)$(prefix)/lib/suricata/python/suricata/ctl"
install -d -m 0755 "$(DESTDIR)$(prefix)/lib/suricata/python/suricata/sc"
install -d -m 0755 "$(DESTDIR)$(prefix)/lib/suricata/python/suricatasc"
for src in $(LIBS); do \
install -m 0644 $(srcdir)/$$src "$(DESTDIR)$(prefix)/lib/suricata/python/$$src"; \
done
install suricata/config/defaults.py \
"$(DESTDIR)$(prefix)/lib/suricata/python/suricata/config/defaults.py"

@ -1,39 +0,0 @@
#! /usr/bin/env python
#
# Copyright (C) 2017-2022 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 sys
import os
import site
exec_dir = os.path.dirname(__file__)
if os.path.exists(os.path.join(exec_dir, "..", "suricata", "ctl", "main.py")):
# Looks like we're running from the development directory.
sys.path.insert(0, ".")
else:
# Check if the Python modules are installed in the Suricata installation
# prefix.
version_info = sys.version_info
pyver = "%d.%d" % (version_info.major, version_info.minor)
path = os.path.realpath(os.path.join(
exec_dir, "..", "lib", "suricata", "python", "suricata"))
if os.path.exists(path):
sys.path.insert(0, os.path.dirname(path))
from suricata.ctl.main import main
sys.exit(main())

@ -1,100 +0,0 @@
#! /usr/bin/env python
#
# Copyright(C) 2013-2023 Open Information Security Foundation
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 2 of the License.
#
# 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
# 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 sys
import os
import argparse
# Find the Python libdir.
exec_dir = os.path.dirname(__file__)
if os.path.exists(os.path.join(exec_dir, "..", "suricata", "ctl", "main.py")):
# Looks like we're running from the development directory.
sys.path.insert(0, ".")
else:
# Check if the Python modules are installed in the Suricata installation
# prefix.
version_info = sys.version_info
pyver = "%d.%d" % (version_info.major, version_info.minor)
path = os.path.realpath(os.path.join(
exec_dir, "..", "lib", "suricata", "python", "suricata"))
if os.path.exists(path):
sys.path.insert(0, os.path.dirname(path))
from suricata.sc import *
try:
from suricata.config import defaults
has_defaults = True
except:
has_defaults = False
parser = argparse.ArgumentParser(prog='suricatasc', description='Client for Suricata unix socket')
parser.add_argument('-v', '--verbose', action='store_const', const=True, help='verbose output (including JSON dump)')
parser.add_argument('-c', '--command', default=None, help='execute on single command and return JSON')
parser.add_argument('socket', metavar='socket', nargs='?', help='socket file to connect to', default=None)
args = parser.parse_args()
if args.socket != None:
SOCKET_PATH = args.socket
elif has_defaults:
SOCKET_PATH = os.path.join(defaults.localstatedir, "suricata-command.socket")
else:
print("Unable to determine path to suricata-command.socket.", file=sys.stderr)
sys.exit(1)
sc = SuricataSC(SOCKET_PATH, verbose=args.verbose)
try:
sc.connect()
except SuricataNetException as err:
print("Unable to connect to socket %s: %s" % (SOCKET_PATH, err), file=sys.stderr)
sys.exit(1)
except SuricataReturnException as err:
print("Unable to negotiate version with server: %s" % (err), file=sys.stderr)
sys.exit(1)
if args.command:
try:
(command, arguments) = sc.parse_command(args.command)
except SuricataCommandException as err:
print(err.value)
sys.exit(1)
try:
res = sc.send_command(command, arguments)
except (SuricataCommandException, SuricataReturnException) as err:
print(err.value)
sys.exit(1)
print(json.dumps(res))
sc.close()
if res['return'] == 'OK':
sys.exit(0)
else:
sys.exit(1)
try:
sc.interactive()
except SuricataNetException as err:
print("Communication error: %s" % (err))
sys.exit(1)
except SuricataReturnException as err:
print("Invalid return from server: %s" % (err))
sys.exit(1)
print("[+] Quit command client")
sc.close()

@ -1,129 +0,0 @@
# Copyright (C) 2018 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 sys
import os
import os.path
import time
import re
import logging
logger = logging.getLogger("filestore")
class InvalidAgeFormatError(Exception):
pass
def register_args(parser):
subparser = parser.add_subparsers(help="sub-command help")
prune_parser = subparser.add_parser("prune",
help="Remove files in specified directory older than specified age")
required_args = prune_parser.add_argument_group("required arguments")
required_args.add_argument("-d", "--directory",
help="filestore directory", required=True)
required_args.add_argument("--age",
help="prune files older than age, units: s, m, h, d")
prune_parser.add_argument(
"-n", "--dry-run", action="store_true", default=False,
help="only print what would happen")
prune_parser.add_argument(
"-v", "--verbose", action="store_true",
default=False, help="increase verbosity")
prune_parser.add_argument(
"-q", "--quiet", action="store_true", default=False,
help="be quiet, log warnings and errors only")
prune_parser.set_defaults(func=prune)
def is_fileinfo(path):
return path.endswith(".json")
def parse_age(age):
matched_age = re.match(r"(\d+)\s*(\w+)", age)
if not matched_age:
raise InvalidAgeFormatError(age)
val = int(matched_age.group(1))
unit = matched_age.group(2)
ts_units = ["s", "m", "h", "d"]
try:
idx = ts_units.index(unit)
except ValueError:
raise InvalidAgeFormatError("bad unit: %s" % (unit))
multiplier = 60 ** idx if idx != 3 else 24 * 60 ** 2
return val * multiplier
def get_filesize(path):
return os.stat(path).st_size
def remove_file(path, dry_run):
size = 0
size += get_filesize(path)
if not dry_run:
os.unlink(path)
return size
def set_logger_level(args):
if args.verbose:
logger.setLevel(logging.DEBUG)
if args.quiet:
logger.setLevel(logging.WARNING)
def perform_sanity_checks(args):
set_logger_level(args)
err_msg = {
"directory": "filestore directory must be provided",
"age": "no age provided, nothing to do",
}
for val, msg in err_msg.items():
if not getattr(args, val):
print("Error: {}".format(msg), file=sys.stderr)
sys.exit(1)
required_dirs = ["tmp", "00", "ff"]
for required_dir in required_dirs:
if not os.path.exists(os.path.join(args.directory, required_dir)):
logger.error("Provided directory is not a filestore directory")
sys.exit(1)
def prune(args):
perform_sanity_checks(args)
age = parse_age(args.age)
now = time.time()
size = 0
count = 0
for dirpath, dirnames, filenames in os.walk(args.directory, topdown=True):
# Do not go into the tmp directory.
if "tmp" in dirnames:
dirnames.remove("tmp")
for filename in filenames:
path = os.path.join(dirpath, filename)
mtime = os.path.getmtime(path)
this_age = now - mtime
if this_age > age:
logger.debug("Deleting %s; age=%ds", path, this_age)
size += remove_file(path, args.dry_run)
count += 1
logger.info("Removed %d files; %d bytes.", count, size)
return 0

@ -1,81 +0,0 @@
# Copyright (C) 2017 Open Information Security Foundation
# Copyright (c) 2016 Jason Ish
#
# 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 logging
import time
GREEN = "\x1b[32m"
BLUE = "\x1b[34m"
REDB = "\x1b[1;31m"
YELLOW = "\x1b[33m"
RED = "\x1b[31m"
YELLOWB = "\x1b[1;33m"
ORANGE = "\x1b[38;5;208m"
RESET = "\x1b[0m"
# A list of secrets that will be replaced in the log output.
secrets = {}
def add_secret(secret, replacement):
"""Register a secret to be masked. The secret will be replaced with:
<replacement>
"""
secrets[str(secret)] = str(replacement)
class SuriColourLogHandler(logging.StreamHandler):
"""An alternative stream log handler that logs with Suricata inspired
log colours."""
@staticmethod
def format_time(record):
local_time = time.localtime(record.created)
formatted_time = "%d/%d/%d -- %02d:%02d:%02d" % (local_time.tm_mday,
local_time.tm_mon,
local_time.tm_year,
local_time.tm_hour,
local_time.tm_min,
local_time.tm_sec)
return "%s" % (formatted_time)
def emit(self, record):
if record.levelname == "ERROR":
level_prefix = REDB
message_prefix = REDB
elif record.levelname == "WARNING":
level_prefix = ORANGE
message_prefix = ORANGE
else:
level_prefix = YELLOW
message_prefix = ""
self.stream.write("%s%s%s - <%s%s%s> -- %s%s%s\n" % (
GREEN,
self.format_time(record),
RESET,
level_prefix,
record.levelname.title(),
RESET,
message_prefix,
self.mask_secrets(record.getMessage()),
RESET))
@staticmethod
def mask_secrets(msg):
for secret in secrets:
msg = msg.replace(secret, "<%s>" % secrets[secret])
return msg

@ -1,46 +0,0 @@
# Copyright (C) 2018 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 sys
import os
import argparse
import logging
from suricata.ctl import filestore, loghandler
def init_logger():
""" Initialize logging, use colour if on a tty. """
if os.isatty(sys.stderr.fileno()):
logger = logging.getLogger()
logger.setLevel(level=logging.INFO)
logger.addHandler(loghandler.SuriColourLogHandler())
else:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - <%(levelname)s> - %(message)s")
def main():
init_logger()
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='sub-command help')
fs_parser = subparsers.add_parser("filestore", help="Filestore related commands")
filestore.register_args(parser=fs_parser)
args = parser.parse_args()
try:
func = args.func
except AttributeError:
parser.error("too few arguments")
func(args)

@ -1,18 +0,0 @@
from __future__ import print_function
import unittest
from suricata.ctl import filestore
class PruneTestCase(unittest.TestCase):
def test_parse_age(self):
self.assertEqual(filestore.parse_age("1s"), 1)
self.assertEqual(filestore.parse_age("1m"), 60)
self.assertEqual(filestore.parse_age("1h"), 3600)
self.assertEqual(filestore.parse_age("1d"), 86400)
with self.assertRaises(filestore.InvalidAgeFormatError):
filestore.parse_age("1")
with self.assertRaises(filestore.InvalidAgeFormatError):
filestore.parse_age("1y")

@ -1 +0,0 @@
from suricata.sc.suricatasc import *

@ -1,228 +0,0 @@
argsd = {
"pcap-file": [
{
"name": "filename",
"required": 1,
},
{
"name": "output-dir",
"required": 1,
},
{
"name": "tenant",
"type": int,
"required": 0,
},
{
"name": "continuous",
"required": 0,
},
{
"name": "delete-when-done",
"required": 0,
},
],
"pcap-file-continuous": [
{
"name": "filename",
"required": 1,
},
{
"name": "output-dir",
"required": 1,
},
{
"name": "continuous",
"val": True,
"required": 1,
},
{
"name": "tenant",
"type": int,
"required": 0,
},
{
"name": "delete-when-done",
"required": 0,
},
],
"iface-stat": [
{
"name": "iface",
"required": 1,
},
],
"conf-get": [
{
"name": "variable",
"required": 1,
}
],
"unregister-tenant-handler": [
{
"name": "id",
"type": int,
"required": 1,
},
{
"name": "htype",
"required": 1,
},
{
"name": "hargs",
"type": int,
"required": 0,
},
],
"register-tenant-handler": [
{
"name": "id",
"type": int,
"required": 1,
},
{
"name": "htype",
"required": 1,
},
{
"name": "hargs",
"type": int,
"required": 0,
},
],
"unregister-tenant": [
{
"name": "id",
"type": int,
"required": 1,
},
],
"register-tenant": [
{
"name": "id",
"type": int,
"required": 1,
},
{
"name": "filename",
"required": 1,
},
],
"reload-tenant": [
{
"name": "id",
"type": int,
"required": 1,
},
{
"name": "filename",
"required": 0,
},
],
"add-hostbit": [
{
"name": "ipaddress",
"required": 1,
},
{
"name": "hostbit",
"required": 1,
},
{
"name": "expire",
"type": int,
"required": 1,
},
],
"remove-hostbit": [
{
"name": "ipaddress",
"required": 1,
},
{
"name": "hostbit",
"required": 1,
},
],
"list-hostbit": [
{
"name": "ipaddress",
"required": 1,
},
],
"memcap-set": [
{
"name": "config",
"required": 1,
},
{
"name": "memcap",
"required": 1,
},
],
"memcap-show": [
{
"name": "config",
"required": 1,
},
],
"dataset-add": [
{
"name": "setname",
"required": 1,
},
{
"name": "settype",
"required": 1,
},
{
"name": "datavalue",
"required": 1,
},
],
"dataset-remove": [
{
"name": "setname",
"required": 1,
},
{
"name": "settype",
"required": 1,
},
{
"name": "datavalue",
"required": 1,
},
],
"get-flow-stats-by-id": [
{
"name": "flow_id",
"type": int,
"required": 1,
},
],
"dataset-clear": [
{
"name": "setname",
"required": 1,
},
{
"name": "settype",
"required": 1,
}
],
"dataset-lookup": [
{
"name": "setname",
"required": 1,
},
{
"name": "settype",
"required": 1,
},
{
"name": "datavalue",
"required": 1,
},
],
}

@ -1,293 +0,0 @@
# Copyright(C) 2012-2023 Open Information Security Foundation
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 2 of the License.
#
# 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
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
try:
import simplejson as json
except ImportError:
import json
import readline
import select
import sys
from socket import AF_UNIX, error, socket
from inspect import currentframe
from suricata.sc.specs import argsd
SURICATASC_VERSION = "1.0"
VERSION = "0.2"
INC_SIZE = 1024
def get_linenumber():
cf = currentframe()
return cf.f_back.f_lineno
class SuricataException(Exception):
"""
Generic class for suricatasc exception
"""
def __init__(self, value):
super(SuricataException, self).__init__(value)
self.value = value
def __str__(self):
return str(self.value)
class SuricataNetException(SuricataException):
"""
Exception raised when a network error occurs
"""
class SuricataCommandException(SuricataException):
"""
Exception raised when the command is incorrect
"""
class SuricataReturnException(SuricataException):
"""
Exception raised when return message is incorrect
"""
class SuricataCompleter:
def __init__(self, words):
self.words = words
self.generator = None
def complete(self, text):
for word in self.words:
if word.startswith(text):
yield word
def __call__(self, text, state):
if state == 0:
self.generator = self.complete(text)
try:
return next(self.generator)
except StopIteration:
return None
class SuricataSC:
def __init__(self, sck_path, verbose=False):
self.basic_commands = [
"shutdown",
"quit",
"pcap-file-number",
"pcap-file-list",
"pcap-last-processed",
"pcap-interrupt",
"iface-list",
"reload-tenants",
]
self.fn_commands = [
"pcap-file",
"pcap-file-continuous",
"iface-stat",
"conf-get",
"unregister-tenant-handler",
"register-tenant-handler",
"unregister-tenant",
"register-tenant",
"reload-tenant",
"add-hostbit",
"remove-hostbit",
"list-hostbit",
"memcap-set",
"memcap-show",
"dataset-add",
"dataset-remove",
"get-flow-stats-by-id",
"dataset-clear",
"dataset-lookup",
]
self.cmd_list = self.basic_commands + self.fn_commands
self.sck_path = sck_path
self.verbose = verbose
self.socket = socket(AF_UNIX)
def json_recv(self):
cmdret = None
data = ""
while True:
if sys.version < '3':
received = self.socket.recv(INC_SIZE)
else:
received = self.socket.recv(INC_SIZE).decode('iso-8859-1')
if not received:
break
data += received
if data.endswith('\n'):
cmdret = json.loads(data)
break
return cmdret
def send_command(self, command, arguments=None):
if command not in self.cmd_list and command != 'command-list':
raise SuricataCommandException("L{}: Command not found: {}".format(get_linenumber(), command))
cmdmsg = {}
cmdmsg['command'] = command
if arguments:
cmdmsg['arguments'] = arguments
if self.verbose:
print("SND: " + json.dumps(cmdmsg))
cmdmsg_str = json.dumps(cmdmsg) + "\n"
if sys.version < '3':
self.socket.send(cmdmsg_str)
else:
self.socket.send(bytes(cmdmsg_str, 'iso-8859-1'))
ready = select.select([self.socket], [], [], 600)
if ready[0]:
cmdret = self.json_recv()
else:
cmdret = None
if not cmdret:
raise SuricataReturnException("L{}: Unable to get message from server".format(get_linenumber))
if self.verbose:
print("RCV: "+ json.dumps(cmdret))
return cmdret
def connect(self):
try:
if self.socket is None:
self.socket = socket(AF_UNIX)
self.socket.connect(self.sck_path)
except error as err:
raise SuricataNetException("L{}: {}".format(get_linenumber(), err))
self.socket.settimeout(10)
#send version
if self.verbose:
print("SND: " + json.dumps({"version": VERSION}))
if sys.version < '3':
self.socket.send(json.dumps({"version": VERSION}))
else:
self.socket.send(bytes(json.dumps({"version": VERSION}), 'iso-8859-1'))
ready = select.select([self.socket], [], [], 600)
if ready[0]:
cmdret = self.json_recv()
else:
cmdret = None
if not cmdret:
raise SuricataReturnException("L{}: Unable to get message from server".format(get_linenumber()))
if self.verbose:
print("RCV: "+ json.dumps(cmdret))
if cmdret["return"] == "NOK":
raise SuricataReturnException("L{}: Error: {}".format(get_linenumber(), cmdret["message"]))
cmdret = self.send_command("command-list")
# we silently ignore NOK as this means server is old
if cmdret["return"] == "OK":
self.cmd_list = cmdret["message"]["commands"]
self.cmd_list.append("quit")
def close(self):
self.socket.close()
self.socket = None
def execute(self, command):
full_cmd = command.split()
cmd = full_cmd[0]
cmd_specs = argsd[cmd]
required_args_count = len([d["required"] for d in cmd_specs if d["required"] and not "val" in d])
arguments = dict()
for c, spec in enumerate(cmd_specs, 1):
spec_type = str if "type" not in spec else spec["type"]
if spec["required"]:
if spec.get("val"):
arguments[spec["name"]] = spec_type(spec["val"])
continue
try:
arguments[spec["name"]] = spec_type(full_cmd[c])
except IndexError:
phrase = " at least" if required_args_count != len(cmd_specs) else ""
msg = "Missing arguments: expected{} {}".format(phrase, required_args_count)
raise SuricataCommandException("L{}: {}".format(get_linenumber(), msg))
except ValueError as ve:
raise SuricataCommandException("L{}: Erroneous arguments: {}".format(get_linenumber(), ve))
elif c < len(full_cmd):
arguments[spec["name"]] = spec_type(full_cmd[c])
return cmd, arguments
def parse_command(self, command):
arguments = None
cmd = command.split()[0] if command else None
if cmd in self.cmd_list:
if cmd in self.fn_commands:
cmd, arguments = getattr(self, "execute")(command=command)
else:
raise SuricataCommandException("L{}: Unknown command: {}".format(get_linenumber(), command))
return cmd, arguments
def interactive(self):
print("Command list: " + ", ".join(self.cmd_list))
try:
readline.set_completer(SuricataCompleter(self.cmd_list))
readline.set_completer_delims(";")
readline.parse_and_bind('tab: complete')
while True:
if sys.version < '3':
command = raw_input(">>> ").strip()
else:
command = input(">>> ").strip()
if command == "quit":
break
if len(command.strip()) == 0:
continue
try:
cmd, arguments = self.parse_command(command)
except SuricataCommandException as err:
print(err)
continue
try:
cmdret = self.send_command(cmd, arguments)
except IOError as err:
# try to reconnect and resend command
print("Connection lost, trying to reconnect")
try:
self.close()
self.connect()
except (SuricataNetException, SuricataReturnException) as err:
print(err.value)
continue
cmdret = self.send_command(cmd, arguments)
except (SuricataCommandException, SuricataReturnException) as err:
print("An exception occured: " + str(err.value))
continue
#decode json message
if cmdret["return"] == "NOK":
print("Error:")
print(json.dumps(cmdret["message"], sort_keys=True, indent=4, separators=(',', ': ')))
else:
print("Success:")
print(json.dumps(cmdret["message"], sort_keys=True, indent=4, separators=(',', ': ')))
except KeyboardInterrupt:
print("[!] Interrupted")
sys.exit(0)

@ -1 +0,0 @@
from suricata.sc import *
Loading…
Cancel
Save