Skip to content

Commit dee2f64

Browse files
committed
add policy configurator and OL7 builder + util scripts
1 parent cc80e62 commit dee2f64

12 files changed

Lines changed: 315 additions & 246 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,8 @@
3636
# only keep setup notes
3737
# !./src/Notes/setup.md
3838

39+
# ignore system-specific SCAP data
40+
**/SCAP/*
41+
3942
# only keep the example config JSON
4043
!**example-config.json

install/utils/build_tenablecore.sh

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ function install_rpms(){
2222
rpm -i "$INSTALL_TEMPDIR/install/rpms/acas/CM307352_Nessus-10.7.3-el8.x86_64.rpm" || true
2323
rpm -i "$INSTALL_TEMPDIR/install/rpms/acas/dialog-1.3-32.20210117.el9.x86_64.rpm" || true
2424
rpm -i "$INSTALL_TEMPDIR/install/rpms/acas/CM306733_acas_configure-24.03-4.noarch.rpm" || true
25+
# install rpm extras
26+
rpm -ivh "$INSTALL_TEMPDIR/install/rpms/jdk-11/*.rpm" || true
2527
}
2628

2729
function configure_nessus(){
@@ -60,9 +62,7 @@ function configure_networking(){
6062
# install networkctl
6163
cp "$INSTALL_TEMPDIR/TenableCore/NetworkManager/networkctl.sh" /opt
6264
chmod 755 /opt/networkctl.sh
63-
systemctl restart NetworkManager || true
64-
65-
ln -s /opt/networkctl.sh /usr/bin/networkctl || true
65+
systemctl restart NetworkManager || true
6666
}
6767

6868
function install_notes(){
@@ -84,14 +84,21 @@ function install_api(){
8484
# cd /opt/NessusAPI/src
8585
# pyinstaller --onefile --distpath /opt/NessusAPI/bin --workpath /tmp --specpath /tmp
8686
# cd -
87-
8887
# ln -s /opt/NessusAPI/bin/nessus-configure /usr/bin/nessus-configure
88+
8989
ln -s /opt/NessusAPI/src/nessus-configure.py /usr/bin/nessus-configure || true
90+
ln -s /opt/NessusAPI/src/nessus-update-policy.py /usr/bin/nessus-update-policy || true
9091
}
9192

92-
function install_scap_tools(){
93-
# TODO
94-
echo "TODO: Install SCAP Automation Tools"
93+
function install_utility_scripts(){
94+
cp -r "$INSTALL_TEMPDIR"/TenableCore/scripts /opt/
95+
# force ownership and permissions
96+
chmod 755 /opt/scripts/*
97+
chown -R root:root /opt/scripts/*
98+
# symlink only bins so all users can see it
99+
ln -s /opt/scripts/bin/* /usr/bin/
100+
# other scripts get stored here
101+
95102
}
96103

97104
####################### Main #######################
@@ -137,7 +144,6 @@ configure_nessus
137144
configure_networking
138145
install_notes
139146
install_api
140-
install_scap_tools
141147

142148
echo "Nessus Install Completed"
143149

install/vm/.gitkeep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
put the VDI here with the TenableCore.sh installer script

src/NessusAPI/nessus-configure.py

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,110 @@
22

33
import code
44
import argparse
5+
from argparse import ArgumentError
56
import os
7+
from getpass import getpass
68
from nessusapi import NessusAPI
79

8-
if __name__ == "__main__":
10+
def interact(nessus: NessusAPI) -> None:
11+
n = nessus.Nessus
12+
code.interact(local=locals())
13+
14+
def parse_args() -> None:
915
parser = argparse.ArgumentParser(description="A command-line interface for nessus. Used to initialize/export a nessus instance or interact with the API.")
10-
parser.add_argument('config', metavar="CONFIG")
11-
group = parser.add_mutually_exclusive_group(required=True)
12-
group.add_argument('-i', "--initialize", action='store_true', help="Initialize Nessus Scans/Policies.")
13-
group.add_argument('-e', "--export", nargs='*', metavar=("OUTDIR", "SCANFOLDER"), help="Export all complete scans to this directory. Can limit scans based on folder.")
14-
group.add_argument('--interactive', action='store_true', help="Interact directly with the Nessus API. (Use locals() to see vars)")
16+
parser.add_argument('-v', '--verbose', help='Add Verbosity')
17+
login_group = parser.add_argument_group("Connection Info")
18+
login_group.add_argument('-c', '--config', metavar="CONFIG", help='JSON Nessus Credential/Policy/Scan configuration file')
19+
login_group.add_argument('-U', '--user', metavar="USERNAME", help='User to authenticate to Nessus')
20+
login_group.add_argument('-H', '--host', metavar="HOST", help='IP/hostname of the Nessus instance')
21+
login_group.add_argument('-p', '--port', metavar="PORT", help='Port to connect to Nessus')
22+
23+
subparser = parser.add_subparsers(title='Commands', dest='command')
24+
25+
export_parser = subparser.add_parser('export', description="Export Nessus Scans to Directory")
26+
export_parser.add_argument('-o', '--outdir', metavar="OUTPUT_DIR", help='Directory to export scan results to')
27+
export_parser.add_argument('-f', '--format', metavar="FMT", nargs='*', default=['nessus'], required=False, help='Formats for scan exports: nessus, pdf, csv, html (default=nessus)')
28+
export_parser.add_argument('--scan_folder', metavar="FOLDER", required=False, help='Export all scans from a folder in Nessus')
29+
30+
init_parser = subparser.add_parser('init', description='Initialize Nessus Policies/Credentials and Scans')
31+
init_parser.add_argument('-e', '--exec', action='store_true', help="Inits Nessus and executes all scans from the config")
32+
33+
exec_parser = subparser.add_parser('exec', description='Use Python to interact with Nessus')
34+
exec_parser.add_argument('-f', '--folder', metavar="SCAN_FOLDER", help='Execute all scans in this folder')
35+
exec_parser.add_argument('-s', '--scan', metavar="SCAN_NAME", nargs='*', help='Execute one or more scans with this name')
36+
37+
subparser.add_parser('interact', description='Use Python to interact with Nessus')
38+
1539
args = parser.parse_args()
1640

17-
if not os.path.exists(args.config):
18-
raise ValueError("ERROR: Config file does not Exist")
41+
if args.verbose:
42+
print(args)
43+
44+
# login error checking
45+
if not ((args.user and args.host and args.port) or args.config):
46+
raise ArgumentError(message="Must specify either a config file and/or a user with a host/port")
47+
48+
# check config existence
49+
if args.config:
50+
if not os.path.exists(args.config):
51+
raise FileNotFoundError("ERROR: Config file does not Exist")
52+
53+
return args
1954

20-
nessus = NessusAPI(file=args.config, initialize=args.initialize)
21-
if args.export:
22-
nessus.export_all_scans(outdir=args.export)
23-
if args.interactive:
24-
from pprint import pprint
25-
printvars = lambda obj: pprint(vars(obj))
26-
n = nessus.Nessus
27-
code.interact(local=locals())
55+
def init() -> NessusAPI:
56+
args = parse_args()
2857

29-
nessus.logout()
58+
nessus = None
59+
if args.config:
60+
if args.user:
61+
nessus = NessusAPI(file=args.config, initialize=(args.command == 'init'),
62+
host=args.host, port=args.port,
63+
credentials={
64+
"type": "password",
65+
"username": args.user,
66+
"password": getpass("Nessus Password: ")
67+
})
68+
else:
69+
nessus = NessusAPI(file=args.config, initialize=(args.command == 'init'))
70+
71+
elif args.host and args.user:
72+
if args.command == 'initialize':
73+
raise ArgumentError("Cannot initialize Nessus scans/policies without a config file.")
74+
if not args.port:
75+
args.port = 8834
76+
nessus = NessusAPI(host=args.host,
77+
port=args.port,
78+
credentials={
79+
"type": "password",
80+
"username": "acasuser",
81+
"password": getpass("Nessus Password: ")
82+
})
83+
else:
84+
pass # this error (lack of connection info) was checked earlier
85+
86+
if args.command == "init":
87+
if args.exec:
88+
raise NotImplementedError("Auto execution of scans after init is not implemented")
89+
elif args.command == "exec":
90+
raise NotImplementedError("CLI execution of nessus scans is not implemented")
91+
92+
elif args.command == "export":
93+
nessus.export_all_scans(outdir=args.outdir, export_formats=[f.lower() for f in args.format], scan_folder=args.scan_folder)
94+
95+
elif args.command == 'interact':
96+
interact(nessus)
97+
98+
else:
99+
raise ArgumentError("Unsupported Command") # control never reaches here
100+
101+
return nessus
102+
103+
104+
if __name__ == "__main__":
105+
nessus = None
106+
try:
107+
nessus = init()
108+
except Exception as e:
109+
print(e)
110+
if nessus:
111+
nessus.logout()
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
from argparse import ArgumentError
5+
import json
6+
import os
7+
from getpass import getpass
8+
from sys import argv
9+
import textwrap
10+
import re
11+
12+
def _split_args(argv: list[str], delimiters: list[str]) -> tuple[list, list]:
13+
parseable_args = []
14+
remaining_args = []
15+
for x, arg in enumerate(argv):
16+
if arg not in delimiters:
17+
parseable_args.append(arg)
18+
else:
19+
remaining_args = argv[x:]
20+
break
21+
return parseable_args, remaining_args
22+
23+
def parse_args(argv: list[str]) -> dict:
24+
commands = {
25+
'setpasswords': None,
26+
'configureserver': None,
27+
'setscanpolicy': None
28+
}
29+
30+
args = {}
31+
parser = argparse.ArgumentParser(
32+
description="Update a NessusAPI JSON configuration's core settings.",
33+
usage="USAGE: ./nessus-policy-update.py [-vh] CONFIG {-o OUTFILE | --overwrite} COMMAND ...",
34+
epilog=textwrap.dedent('''\
35+
Commands:
36+
setpasswords: Set credentials for a scan policy
37+
configureserver: Set server connection settings
38+
setscanpolicy: Set path to the Nessus scan policy XML
39+
''')
40+
)
41+
parser.add_argument("config", required=True, metavar="CONFIG", help="NessusAPI Policy JSON config")
42+
output_group = parser.add_mutually_exclusive_group(required=True)
43+
output_group.add_argument("--overwrite", action="store_true", help="Overwrite the existing config file.")
44+
output_group.add_argument("-o", "--outfile", metavar="OUTFILE", help="Path to write the new config to")
45+
46+
setpasswords_parser = argparse.ArgumentParser(description="Replace all passwords in a NessusAPI JSON configuration with real system passwords")
47+
setpasswords_parser.add_argument("--password_placeholder", required=False, metavar="STRING", default='*', help="Scan config for a custom password placeholder string.")
48+
49+
configureserver_parser = argparse.ArgumentParser(description="Configure Nessus connection in a NessusAPI JSON configuration with real system passwords")
50+
configureserver_parser.add_argument('-H', '--host', metavar="HOST", help='IP or hostname to connect to Nessus')
51+
configureserver_parser.add_argument('-P', '--port', metavar="PORT", default=8834, help='Port to connect to Nessus (default=8834)')
52+
auth_group = configureserver_parser.add_mutually_exclusive_group()
53+
auth_group.add_argument('-p', '--usepassword', required=True, action='store_true', help='Prompt for user/password to authenticate to Nessus')
54+
auth_group.add_argument('-k', '--usekeys', required=True, action='store_true', help='Prompt for authentication keys to authenticate to Nessus')
55+
56+
setscanpolicy_parser = argparse.ArgumentParser(description="Update path to the scan policy to use")
57+
setscanpolicy_parser.add_argument('policy', metavar="POLICY", required=True, help='Path to the scan policy XML')
58+
59+
# Parse global args
60+
remaining_args = argv
61+
while len(remaining_args) > 0:
62+
parseable_args, remaining_args = _split_args(argv, commands)
63+
if len(parseable_args) == 0:
64+
raise ArgumentError(f'Unknown argument: "{remaining_args[0]}"')
65+
if parseable_args[0] not in commands:
66+
args += vars(parser.parse_args(parseable_args))
67+
else:
68+
command = parseable_args.pop(0)
69+
if args.get(command):
70+
raise ArgumentError(f'Duplicate command found: "{command}"')
71+
args[command] = vars(parser.parse_args(parseable_args))
72+
73+
return args
74+
75+
def replace_passwords(dictionary: dict, path: list[str], placeholder_string: str = '*') -> None:
76+
current_path = path
77+
for key, value in dictionary.items():
78+
current_path = path + [key]
79+
if 'password' in key.lower():
80+
if isinstance(value, str) and (placeholder_string == '*' or value == placeholder_string):
81+
passwd, confirm_passwd = '', ''
82+
while not passwd or passwd != confirm_passwd:
83+
print("Update Password Configuration:")
84+
# print configuration using password (omit nested items)
85+
print('.'.join(current_path), '= {')
86+
for k,v in dictionary:
87+
if isinstance(k, str) and isinstance(v, str):
88+
print(f" {k}: {v}")
89+
print('}')
90+
# actually change the password
91+
if (user := dictionary.get('username')):
92+
passwd = getpass(f'New Password for "{user}": ')
93+
else:
94+
passwd = getpass(f'New Password": ')
95+
passwd = passwd.strip()
96+
confirm_passwd = getpass(f'Confirm Password": ')
97+
print('\n') # clear the screen a bit
98+
dictionary[key] = passwd
99+
elif isinstance(value, dict):
100+
replace_passwords(value, current_path, placeholder_string=placeholder_string)
101+
elif isinstance(value, list):
102+
for x, nested_list in enumerate(value):
103+
replace_passwords(value, current_path + [f'[{x}]'], placeholder_string=placeholder_string)
104+
105+
def is_username_valid(username: str) -> bool:
106+
if (3 <= len(username) <= 20) and re.match(r'^[a-zA-Z][a-zA-Z0-9-_]*[a-zA-Z]'):
107+
return True
108+
return False
109+
110+
if __name__ == '__main__':
111+
args = parse_args(argv)
112+
113+
if not os.path.exists(args['config']):
114+
raise OSError("Config file does not exist")
115+
116+
config = json.loads(open(args['config'], 'rb').read())
117+
118+
if command := args.get('setpasswords'):
119+
placeholder_string = command.get('password_placeholder', '*')
120+
replace_passwords(config['policies']['credentials'], [], placeholder_string='*')
121+
122+
elif command := args.get('configureserver'):
123+
if command.get('usepassword'):
124+
username = ''
125+
while not is_username_valid(username):
126+
username = input('Server Username: ')
127+
passwd, _passwd = None, None
128+
while not passwd:
129+
passwd = getpass("Server Password: ")
130+
_passwd = getpass("Server Password [Confirm]: ")
131+
if passwd != _passwd:
132+
passwd = None
133+
print("Error: Passwords do not match!")
134+
135+
config['server']['credentials'] = {
136+
"type": "password",
137+
"username": username,
138+
"password": passwd
139+
}
140+
elif command.get('usekeys'):
141+
raise NotImplementedError()
142+
143+
elif command := args.get('setscanpolicy'):
144+
if not os.path.exists(command['policy']):
145+
print(f'Cannot find policy file: '{command['policy']}'')
146+
confirm = input("Update anyways? [Y/n]: ")
147+
if confirm.lower() in ['y', 'yes']:
148+
config['policies']['file']= command['policy']
149+
else:
150+
print('Policy not updated.')
151+
152+
outfile_name = args.config if args.get('overwrite') else args['outfile']
153+
with open(outfile_name, 'w', encoding='ascii') as outfile:
154+
outfile.write(json.dumps(config, indent=4))
155+
156+
print('Config updated successfully.')

src/NessusAPI/nessusapi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def export_all_scans(self, outdir: str, scan_folder: str = None, export_formats:
179179
if not os.path.exists(outdir):
180180
os.mkdir(outdir, mode=755)
181181
if not os.path.isdir(outdir):
182-
raise OSError(f"Cannot use '{outdir}' to store scans")
182+
raise FileExistsError(f"Cannot use '{outdir}' to store scans")
183183

184184
for scan in self.list_scans(scan_folder):
185185
if scan['status'] in "completed imported":

0 commit comments

Comments
 (0)