Skip to content

Commit a4627ed

Browse files
committed
Initial commit
0 parents  commit a4627ed

File tree

18 files changed

+3111
-0
lines changed

18 files changed

+3111
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.vscode
2+
dist
3+
build
4+
.eggs
5+
stormshield.sns.sslclient.egg-info
6+
__pycache__

LICENCE

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright 2018 Stormshield
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
include stormshield/sns/bundle.ca
2+
include stormshield/sns/cmd.complete

README.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# pySNSAPI
2+
3+
A Python client for the Stormshield Network Security appliance SSL API.
4+
5+
## API usage
6+
7+
```python
8+
from stormshield.sns.sslclient import SSLClient
9+
10+
client = SSLClient(
11+
host="10.0.0.254", port=443,
12+
user='admin', password='password',
13+
sslverifyhost=False)
14+
15+
response = client.send_command("SYSTEM PROPERTY")
16+
17+
if response:
18+
model = response.data['Result']['Model']
19+
version = response.data['Result']['Version']
20+
21+
print("Model: {}".format(model))
22+
print("Firmware version: {}".format(version))
23+
else:
24+
print("Command failed: {}".format(response.output))
25+
26+
client.disconnect()
27+
28+
```
29+
30+
### Command results
31+
32+
Command results are available in text, xml or python structure formats:
33+
34+
```python
35+
>>> response = client.send_command("CONFIG NTP SERVER LIST")
36+
37+
>>> print(response.output)
38+
101 code=00a01000 msg="Begin" format="section_line"
39+
[Result]
40+
name=ntp1.stormshieldcs.eu keynum=none type=host
41+
name=ntp2.stormshieldcs.eu keynum=none type=host
42+
100 code=00a00100 msg="Ok"
43+
44+
>>> print(response.xml)
45+
<?xml version="1.0"?>
46+
<nws code="100" msg="OK"><serverd ret="101" code="00a01000" msg="Begin"><data format="section_line"><section title="Result"><line><key name="name" value="ntp1.stormshieldcs.eu"/><key name="keynum" value="none"/><key name="type" value="host"/></line><line><key name="name" value="ntp2.stormshieldcs.eu"/><key name="keynum" value="none"/><key name="type" value="host"/></line></section></data></serverd><serverd ret="100" code="00a00100" msg="Ok"></serverd></nws>
47+
48+
>>> print(response.data)
49+
{'Result': [{'name': 'ntp1.stormshieldcs.eu', 'keynum': 'none', 'type': 'host'}, {'name': 'ntp2.stormshieldcs.eu', 'keynum': 'none', 'type': 'host'}]}
50+
51+
```
52+
53+
The keys of the `data` property are case insensitive, `response.data['Result'][0]['name']` and `response.data['ReSuLt'][0]['NaMe']` will return the same value.
54+
55+
Results token are also available via `response.parser.get()` method which accepts a default parameter to return if the token is not present.
56+
57+
```python
58+
>>> print(response.output)
59+
101 code=00a01000 msg="Begin" format="section"
60+
[Server]
61+
1=dns1.google.com
62+
2=dns2.google.com
63+
100 code=00a00100 msg="Ok"
64+
65+
>>> print(response.data['Server']['3'])
66+
Traceback (most recent call last):
67+
File "<stdin>", line 1, in <module>
68+
File "/usr/local/lib/python3.7/site-packages/requests/structures.py", line 52, in __getitem__
69+
return self._store[key.lower()][1]
70+
KeyError: '3'
71+
72+
>>> print(response.parser.get(section='Server', token='3', default=None))
73+
None
74+
75+
```
76+
77+
### File upload/download
78+
79+
Files can be downloaded or uploaded by adding a redirection to a file with '>' or '<' at the end of the configuration command.
80+
81+
```python
82+
>>> client.send_command("CONFIG BACKUP list=all > /tmp/mybackup.na")
83+
100 code=00a00100 msg="Ok"
84+
```
85+
86+
## snscli
87+
88+
`snscli` is a python cli for executing configuration commands and scripts on Stormshield Network Security appliances.
89+
90+
* Output format can be chosen between section/ini or xml
91+
* File upload and download available with adding `< upload` or `> download` at the end of the command
92+
* Client can execute script files using `--script` option.
93+
* Comments are allowed with `#`
94+
95+
`$ snscli --host <utm>`
96+
97+
`$ snscli --host <utm> --user admin --password admin --script config.script`
98+
99+
Concerning the SSL validation:
100+
101+
* For the first connection to a new appliance, ssl host name verification can be bypassed with `--no-sslverifyhost` option.
102+
* To connect to a known appliance with the default certificate use `--host <serial> --ip <ip address>` to validate the peer certificate.
103+
* If a custom CA and certificate is installed, use `--host myfirewall.tld --cabundle <ca.pem>`.
104+
* For client certificate authentication, the expected format is a pem file with the certificate and the unencrypted key concatenated.
105+
106+
107+
## Build
108+
109+
`$ python3 setup.py sdist bdist_wheel`
110+
111+
112+
## Install
113+
114+
`$ python3 setup.py install`
115+
116+
117+
## Tests
118+
119+
Warning: tests require a remote SNS appliance.
120+
121+
`$ APPLIANCE=10.0.0.254 python3 setup.py test`
122+
123+
124+
To run `snscli` from the source folder without install:
125+
126+
`$ PYTHONPATH=. python3 ./bin/snscli --help`
127+
128+
129+
## Links
130+
131+
* [Stormshield corporate website](https://www.stormshield.com)
132+
* [CLI commands reference guide](https://documentation.stormshield.eu/SNS/v3/en/Content/CLI_Serverd_Commands_reference_Guide_v3/Introduction.htm)
133+

bin/snscli

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/python
2+
3+
""" cli to connect to Stormshield Network Security appliances"""
4+
5+
import sys
6+
import os
7+
import re
8+
import logging
9+
import readline
10+
import getpass
11+
import atexit
12+
import xml.dom.minidom
13+
import begin
14+
from pygments import highlight
15+
from pygments.lexers import XmlLexer
16+
from pygments.formatters import TerminalFormatter
17+
from colorlog import ColoredFormatter
18+
19+
from stormshield.sns.sslclient import SSLClient, ServerError
20+
21+
FORMATTER = ColoredFormatter(
22+
"%(log_color)s%(levelname)-8s%(reset)s %(message)s",
23+
datefmt=None,
24+
reset=True,
25+
log_colors={
26+
'DEBUG': 'green',
27+
'INFO': 'cyan',
28+
'WARNING': 'yellow',
29+
'ERROR': 'red',
30+
'CRITICAL': 'red,bg_white'
31+
},
32+
secondary_log_colors={},
33+
style='%'
34+
)
35+
36+
EMPTY_RE = re.compile(r'^\s*$')
37+
38+
def make_completer():
39+
""" load completer for readline """
40+
vocabulary = []
41+
with open(SSLClient.get_completer(), "r") as completelist:
42+
for line in completelist:
43+
vocabulary.append(line.replace('.', ' ').strip('\n'))
44+
45+
def custom_complete(text, state):
46+
results = [x for x in vocabulary if x.startswith(text)] + [None]
47+
return results[state]
48+
return custom_complete
49+
50+
@begin.start(auto_convert=True, short_args=False, lexical_order=True)
51+
@begin.logging
52+
def main(host: 'Remote UTM' = None,
53+
ip: 'Remote UTM ip' = None,
54+
usercert: 'User certificate file' = None,
55+
cabundle: 'CA bundle file' = None,
56+
password: 'Password' = None,
57+
port: 'Remote port' = 443,
58+
user: 'User name' = 'admin',
59+
sslverifypeer: 'Strict SSL CA check' = True,
60+
sslverifyhost: 'Strict SSL host name check' = True,
61+
credentials: 'Privilege list' = None,
62+
script: 'Command script' = None,
63+
outputformat: 'Output format (ini|xml)' = 'ini'):
64+
65+
for handler in logging.getLogger().handlers:
66+
if handler.__class__ == logging.StreamHandler:
67+
handler.setFormatter(FORMATTER)
68+
69+
if script is not None:
70+
try:
71+
script = open(script, 'r')
72+
except Exception as exception:
73+
logging.error("Can't open script file - %s", str(exception))
74+
sys.exit(1)
75+
76+
if outputformat not in ['ini', 'xml']:
77+
logging.error("Unknown output format")
78+
sys.exit(1)
79+
80+
if host is None:
81+
logging.error("No host provided")
82+
sys.exit(1)
83+
84+
if password is None and usercert is None:
85+
password = getpass.getpass()
86+
87+
try:
88+
client = SSLClient(
89+
host=host, ip=ip, port=port, user=user, password=password,
90+
sslverifypeer=sslverifypeer, sslverifyhost=sslverifyhost,
91+
credentials=credentials,
92+
usercert=usercert, cabundle=cabundle, autoconnect=False)
93+
except Exception as exception:
94+
logging.error(str(exception))
95+
sys.exit(1)
96+
97+
try:
98+
client.connect()
99+
except Exception as exception:
100+
search = re.search(r'doesn\'t match \'(.*)\'', str(exception))
101+
if search:
102+
logging.error(("Appliance name can't be verified, to force connection "
103+
"use \"--host %s --ip %s\" or \"--no-sslverifyhost\" "
104+
"options"), search.group(1), host)
105+
else:
106+
logging.error(str(exception))
107+
sys.exit(1)
108+
109+
# disconnect gracefuly at exit
110+
atexit.register(client.disconnect)
111+
112+
if script is not None:
113+
for cmd in script.readlines():
114+
cmd = cmd.strip('\r\n')
115+
print(cmd)
116+
if cmd.startswith('#'):
117+
continue
118+
if EMPTY_RE.match(cmd):
119+
continue
120+
try:
121+
response = client.send_command(cmd)
122+
except Exception as exception:
123+
logging.error(str(exception))
124+
sys.exit(1)
125+
if outputformat == 'xml':
126+
print(highlight(xml.dom.minidom.parseString(response.xml).toprettyxml(),
127+
XmlLexer(), TerminalFormatter()))
128+
else:
129+
print(response.output)
130+
sys.exit(0)
131+
132+
# Start cli
133+
134+
# load history
135+
histfile = os.path.join(os.path.expanduser("~"), ".sslclient_history")
136+
try:
137+
readline.read_history_file(histfile)
138+
readline.set_history_length(1000)
139+
except FileNotFoundError:
140+
pass
141+
142+
def save_history(histfile):
143+
try:
144+
readline.write_history_file(histfile)
145+
except:
146+
logging.warning("Can't write history")
147+
148+
atexit.register(save_history, histfile)
149+
150+
# load auto-complete
151+
readline.parse_and_bind('tab: complete')
152+
readline.set_completer_delims('')
153+
readline.set_completer(make_completer())
154+
155+
while True:
156+
try:
157+
cmd = input("> ")
158+
except EOFError:
159+
break
160+
161+
# skip comments
162+
if cmd.startswith('#'):
163+
continue
164+
165+
try:
166+
response = client.send_command(cmd)
167+
except ServerError as exception:
168+
# do not log error on QUIT
169+
if "quit".startswith(cmd.lower()) \
170+
and str(exception) == "Server disconnected":
171+
sys.exit(0)
172+
logging.error(str(exception))
173+
sys.exit(1)
174+
except Exception as exception:
175+
logging.error(str(exception))
176+
sys.exit(1)
177+
178+
if response.ret == client.SRV_RET_DOWNLOAD:
179+
filename = input("File to save: ")
180+
try:
181+
client.download(filename)
182+
logging.info("File downloaded")
183+
except Exception as exception:
184+
logging.error(str(exception))
185+
elif response.ret == client.SRV_RET_UPLOAD:
186+
filename = input("File to upload: ")
187+
try:
188+
client.upload(filename)
189+
logging.info("File uploaded")
190+
except Exception as exception:
191+
logging.error(str(exception))
192+
else:
193+
if outputformat == 'xml':
194+
print(highlight(xml.dom.minidom.parseString(response.xml).toprettyxml(),
195+
XmlLexer(), TerminalFormatter()))
196+
else:
197+
print(response.output)

examples/example.script

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#system information
2+
SYSTEM PROPERTY
3+

examples/getproperty.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/python
2+
3+
"""
4+
This example show how to connect to a SNS appliance, send a command
5+
to get appliance properties and parse the result to extract the
6+
appliance model and firmware version.
7+
"""
8+
9+
import getpass
10+
11+
from stormshield.sns.sslclient import SSLClient
12+
13+
# user input
14+
host = input("Appliance ip address: ")
15+
user = input("User:")
16+
password = getpass.getpass("Password: ")
17+
18+
# connect to the appliance
19+
client = SSLClient(
20+
host=host, port=443,
21+
user=user, password=password,
22+
sslverifyhost=False)
23+
24+
# request appliance properties
25+
response = client.send_command("SYSTEM PROPERTY")
26+
27+
if response:
28+
#get value using parser get method
29+
model = response.parser.get(section='Result', token='Model')
30+
# get value with direct access to data
31+
version = response.data['Result']['Version']
32+
33+
print("")
34+
print("Model: {}".format(model))
35+
print("Firmware version: {}".format(version))
36+
else:
37+
print("Command failed: {}".format(response.output))
38+
39+
client.disconnect()

0 commit comments

Comments
 (0)