1r"""
2A salt util for modifying firewall settings.
3
4.. versionadded:: 2018.3.4
5.. versionadded:: 2019.2.0
6
7This util allows you to modify firewall settings in the local group policy in
8addition to the normal firewall settings. Parameters are taken from the
9netsh advfirewall prompt.
10
11.. note::
12    More information can be found in the advfirewall context in netsh. This can
13    be access by opening a netsh prompt. At a command prompt type the following:
14
15    c:\>netsh
16    netsh>advfirewall
17    netsh advfirewall>set help
18    netsh advfirewall>set domain help
19
20Usage:
21
22.. code-block:: python
23
24    import salt.utils.win_lgpo_netsh
25
26    # Get the inbound/outbound firewall settings for connections on the
27    # local domain profile
28    salt.utils.win_lgpo_netsh.get_settings(profile='domain',
29                                           section='firewallpolicy')
30
31    # Get the inbound/outbound firewall settings for connections on the
32    # domain profile as defined by local group policy
33    salt.utils.win_lgpo_netsh.get_settings(profile='domain',
34                                           section='firewallpolicy',
35                                           store='lgpo')
36
37    # Get all firewall settings for connections on the domain profile
38    salt.utils.win_lgpo_netsh.get_all_settings(profile='domain')
39
40    # Get all firewall settings for connections on the domain profile as
41    # defined by local group policy
42    salt.utils.win_lgpo_netsh.get_all_settings(profile='domain', store='lgpo')
43
44    # Get all firewall settings for all profiles
45    salt.utils.win_lgpo_netsh.get_all_settings()
46
47    # Get all firewall settings for all profiles as defined by local group
48    # policy
49    salt.utils.win_lgpo_netsh.get_all_settings(store='lgpo')
50
51    # Set the inbound setting for the domain profile to block inbound
52    # connections
53    salt.utils.win_lgpo_netsh.set_firewall_settings(profile='domain',
54                                                    inbound='blockinbound')
55
56    # Set the outbound setting for the domain profile to allow outbound
57    # connections
58    salt.utils.win_lgpo_netsh.set_firewall_settings(profile='domain',
59                                                    outbound='allowoutbound')
60
61    # Set inbound/outbound settings for the domain profile in the group
62    # policy to block inbound and allow outbound
63    salt.utils.win_lgpo_netsh.set_firewall_settings(profile='domain',
64                                                    inbound='blockinbound',
65                                                    outbound='allowoutbound',
66                                                    store='lgpo')
67"""
68
69import logging
70import os
71import re
72import socket
73import tempfile
74from textwrap import dedent
75
76import salt.modules.cmdmod
77from salt.exceptions import CommandExecutionError
78
79log = logging.getLogger(__name__)
80__hostname__ = socket.gethostname()
81__virtualname__ = "netsh"
82
83
84# Although utils are often directly imported, it is also possible to use the
85# loader.
86def __virtual__():
87    """
88    Only load if on a Windows system
89    """
90    if not salt.utils.platform.is_windows():
91        return False, "This utility only available on Windows"
92
93    return __virtualname__
94
95
96def _netsh_file(content):
97    """
98    helper function to get the results of ``netsh -f content.txt``
99
100    Running ``netsh`` will drop you into a ``netsh`` prompt where you can issue
101    ``netsh`` commands. You can put a series of commands in an external file and
102    run them as if from a ``netsh`` prompt using the ``-f`` switch. That's what
103    this function does.
104
105    Args:
106
107        content (str):
108            The contents of the file that will be run by the ``netsh -f``
109            command
110
111    Returns:
112        str: The text returned by the netsh command
113    """
114    with tempfile.NamedTemporaryFile(
115        mode="w", prefix="salt-", suffix=".netsh", delete=False, encoding="utf-8"
116    ) as fp:
117        fp.write(content)
118    try:
119        log.debug("%s:\n%s", fp.name, content)
120        return salt.modules.cmdmod.run("netsh -f {}".format(fp.name), python_shell=True)
121    finally:
122        os.remove(fp.name)
123
124
125def _netsh_command(command, store):
126    if store.lower() not in ("local", "lgpo"):
127        raise ValueError("Incorrect store: {}".format(store))
128    # set the store for local or lgpo
129    if store.lower() == "local":
130        netsh_script = dedent(
131            """\
132            advfirewall
133            set store local
134            {}
135        """.format(
136                command
137            )
138        )
139    else:
140        netsh_script = dedent(
141            """\
142            advfirewall
143            set store gpo = {}
144            {}
145        """.format(
146                __hostname__, command
147            )
148        )
149    return _netsh_file(content=netsh_script).splitlines()
150
151
152def get_settings(profile, section, store="local"):
153    """
154    Get the firewall property from the specified profile in the specified store
155    as returned by ``netsh advfirewall``.
156
157    Args:
158
159        profile (str):
160            The firewall profile to query. Valid options are:
161
162            - domain
163            - public
164            - private
165
166        section (str):
167            The property to query within the selected profile. Valid options
168            are:
169
170            - firewallpolicy : inbound/outbound behavior
171            - logging : firewall logging settings
172            - settings : firewall properties
173            - state : firewalls state (on | off)
174
175        store (str):
176            The store to use. This is either the local firewall policy or the
177            policy defined by local group policy. Valid options are:
178
179            - lgpo
180            - local
181
182            Default is ``local``
183
184    Returns:
185        dict: A dictionary containing the properties for the specified profile
186
187    Raises:
188        CommandExecutionError: If an error occurs
189        ValueError: If the parameters are incorrect
190    """
191    # validate input
192    if profile.lower() not in ("domain", "public", "private"):
193        raise ValueError("Incorrect profile: {}".format(profile))
194    if section.lower() not in ("state", "firewallpolicy", "settings", "logging"):
195        raise ValueError("Incorrect section: {}".format(section))
196    if store.lower() not in ("local", "lgpo"):
197        raise ValueError("Incorrect store: {}".format(store))
198    command = "show {}profile {}".format(profile, section)
199    # run it
200    results = _netsh_command(command=command, store=store)
201    # sample output:
202    # Domain Profile Settings:
203    # ----------------------------------------------------------------------
204    # LocalFirewallRules                    N/A (GPO-store only)
205    # LocalConSecRules                      N/A (GPO-store only)
206    # InboundUserNotification               Disable
207    # RemoteManagement                      Disable
208    # UnicastResponseToMulticast            Enable
209
210    # if it's less than 3 lines it failed
211    if len(results) < 3:
212        raise CommandExecutionError("Invalid results: {}".format(results))
213    ret = {}
214    # Skip the first 2 lines. Add everything else to a dictionary
215    for line in results[3:]:
216        ret.update(dict(list(zip(*[iter(re.split(r"\s{2,}", line))] * 2))))
217
218    # Remove spaces from the values so that `Not Configured` is detected
219    # correctly
220    for item in ret:
221        ret[item] = ret[item].replace(" ", "")
222
223    # special handling for firewallpolicy
224    if section == "firewallpolicy":
225        inbound, outbound = ret["Firewall Policy"].split(",")
226        return {"Inbound": inbound, "Outbound": outbound}
227
228    return ret
229
230
231def get_all_settings(profile, store="local"):
232    """
233    Gets all the properties for the specified profile in the specified store
234
235    Args:
236
237        profile (str):
238            The firewall profile to query. Valid options are:
239
240            - domain
241            - public
242            - private
243
244        store (str):
245            The store to use. This is either the local firewall policy or the
246            policy defined by local group policy. Valid options are:
247
248            - lgpo
249            - local
250
251            Default is ``local``
252
253    Returns:
254        dict: A dictionary containing the specified settings
255    """
256    ret = dict()
257    ret.update(get_settings(profile=profile, section="state", store=store))
258    ret.update(get_settings(profile=profile, section="firewallpolicy", store=store))
259    ret.update(get_settings(profile=profile, section="settings", store=store))
260    ret.update(get_settings(profile=profile, section="logging", store=store))
261    return ret
262
263
264def get_all_profiles(store="local"):
265    """
266    Gets all properties for all profiles in the specified store
267
268    Args:
269
270        store (str):
271            The store to use. This is either the local firewall policy or the
272            policy defined by local group policy. Valid options are:
273
274            - lgpo
275            - local
276
277            Default is ``local``
278
279    Returns:
280        dict: A dictionary containing the specified settings for each profile
281    """
282    return {
283        "Domain Profile": get_all_settings(profile="domain", store=store),
284        "Private Profile": get_all_settings(profile="private", store=store),
285        "Public Profile": get_all_settings(profile="public", store=store),
286    }
287
288
289def set_firewall_settings(profile, inbound=None, outbound=None, store="local"):
290    """
291    Set the firewall inbound/outbound settings for the specified profile and
292    store
293
294    Args:
295
296        profile (str):
297            The firewall profile to configure. Valid options are:
298
299            - domain
300            - public
301            - private
302
303        inbound (str):
304            The inbound setting. If ``None`` is passed, the setting will remain
305            unchanged. Valid values are:
306
307            - blockinbound
308            - blockinboundalways
309            - allowinbound
310            - notconfigured
311
312            Default is ``None``
313
314        outbound (str):
315            The outbound setting. If ``None`` is passed, the setting will remain
316            unchanged. Valid values are:
317
318            - allowoutbound
319            - blockoutbound
320            - notconfigured
321
322            Default is ``None``
323
324        store (str):
325            The store to use. This is either the local firewall policy or the
326            policy defined by local group policy. Valid options are:
327
328            - lgpo
329            - local
330
331            Default is ``local``
332
333    Returns:
334        bool: ``True`` if successful
335
336    Raises:
337        CommandExecutionError: If an error occurs
338        ValueError: If the parameters are incorrect
339    """
340    # Input validation
341    if profile.lower() not in ("domain", "public", "private"):
342        raise ValueError("Incorrect profile: {}".format(profile))
343    if inbound and inbound.lower() not in (
344        "blockinbound",
345        "blockinboundalways",
346        "allowinbound",
347        "notconfigured",
348    ):
349        raise ValueError("Incorrect inbound value: {}".format(inbound))
350    if outbound and outbound.lower() not in (
351        "allowoutbound",
352        "blockoutbound",
353        "notconfigured",
354    ):
355        raise ValueError("Incorrect outbound value: {}".format(outbound))
356    if not inbound and not outbound:
357        raise ValueError("Must set inbound or outbound")
358
359    # You have to specify inbound and outbound setting at the same time
360    # If you're only specifying one, you have to get the current setting for the
361    # other
362    if not inbound or not outbound:
363        ret = get_settings(profile=profile, section="firewallpolicy", store=store)
364        if not inbound:
365            inbound = ret["Inbound"]
366        if not outbound:
367            outbound = ret["Outbound"]
368
369    command = "set {}profile firewallpolicy {},{}".format(profile, inbound, outbound)
370
371    results = _netsh_command(command=command, store=store)
372
373    if results:
374        raise CommandExecutionError("An error occurred: {}".format(results))
375
376    return True
377
378
379def set_logging_settings(profile, setting, value, store="local"):
380    """
381    Configure logging settings for the Windows firewall.
382
383    Args:
384
385        profile (str):
386            The firewall profile to configure. Valid options are:
387
388            - domain
389            - public
390            - private
391
392        setting (str):
393            The logging setting to configure. Valid options are:
394
395            - allowedconnections
396            - droppedconnections
397            - filename
398            - maxfilesize
399
400        value (str):
401            The value to apply to the setting. Valid values are dependent upon
402            the setting being configured. Valid options are:
403
404            allowedconnections:
405
406                - enable
407                - disable
408                - notconfigured
409
410            droppedconnections:
411
412                - enable
413                - disable
414                - notconfigured
415
416            filename:
417
418                - Full path and name of the firewall log file
419                - notconfigured
420
421            maxfilesize:
422
423                - 1 - 32767 (Kb)
424                - notconfigured
425
426        store (str):
427            The store to use. This is either the local firewall policy or the
428            policy defined by local group policy. Valid options are:
429
430            - lgpo
431            - local
432
433            Default is ``local``
434
435    Returns:
436        bool: ``True`` if successful
437
438    Raises:
439        CommandExecutionError: If an error occurs
440        ValueError: If the parameters are incorrect
441    """
442    # Input validation
443    if profile.lower() not in ("domain", "public", "private"):
444        raise ValueError("Incorrect profile: {}".format(profile))
445    if setting.lower() not in (
446        "allowedconnections",
447        "droppedconnections",
448        "filename",
449        "maxfilesize",
450    ):
451        raise ValueError("Incorrect setting: {}".format(setting))
452    if setting.lower() in ("allowedconnections", "droppedconnections"):
453        if value.lower() not in ("enable", "disable", "notconfigured"):
454            raise ValueError("Incorrect value: {}".format(value))
455    # TODO: Consider adding something like the following to validate filename
456    # https://stackoverflow.com/questions/9532499/check-whether-a-path-is-valid-in-python-without-creating-a-file-at-the-paths-ta
457    if setting.lower() == "maxfilesize":
458        if value.lower() != "notconfigured":
459            # Must be a number between 1 and 32767
460            try:
461                int(value)
462            except ValueError:
463                raise ValueError("Incorrect value: {}".format(value))
464            if not 1 <= int(value) <= 32767:
465                raise ValueError("Incorrect value: {}".format(value))
466    # Run the command
467    command = "set {}profile logging {} {}".format(profile, setting, value)
468    results = _netsh_command(command=command, store=store)
469
470    # A successful run should return an empty list
471    if results:
472        raise CommandExecutionError("An error occurred: {}".format(results))
473
474    return True
475
476
477def set_settings(profile, setting, value, store="local"):
478    """
479    Configure firewall settings.
480
481    Args:
482
483        profile (str):
484            The firewall profile to configure. Valid options are:
485
486            - domain
487            - public
488            - private
489
490        setting (str):
491            The firewall setting to configure. Valid options are:
492
493            - localfirewallrules
494            - localconsecrules
495            - inboundusernotification
496            - remotemanagement
497            - unicastresponsetomulticast
498
499        value (str):
500            The value to apply to the setting. Valid options are
501
502            - enable
503            - disable
504            - notconfigured
505
506        store (str):
507            The store to use. This is either the local firewall policy or the
508            policy defined by local group policy. Valid options are:
509
510            - lgpo
511            - local
512
513            Default is ``local``
514
515    Returns:
516        bool: ``True`` if successful
517
518    Raises:
519        CommandExecutionError: If an error occurs
520        ValueError: If the parameters are incorrect
521    """
522    # Input validation
523    if profile.lower() not in ("domain", "public", "private"):
524        raise ValueError("Incorrect profile: {}".format(profile))
525    if setting.lower() not in (
526        "localfirewallrules",
527        "localconsecrules",
528        "inboundusernotification",
529        "remotemanagement",
530        "unicastresponsetomulticast",
531    ):
532        raise ValueError("Incorrect setting: {}".format(setting))
533    if value.lower() not in ("enable", "disable", "notconfigured"):
534        raise ValueError("Incorrect value: {}".format(value))
535
536    # Run the command
537    command = "set {}profile settings {} {}".format(profile, setting, value)
538    results = _netsh_command(command=command, store=store)
539
540    # A successful run should return an empty list
541    if results:
542        raise CommandExecutionError("An error occurred: {}".format(results))
543
544    return True
545
546
547def set_state(profile, state, store="local"):
548    """
549    Configure the firewall state.
550
551    Args:
552
553        profile (str):
554            The firewall profile to configure. Valid options are:
555
556            - domain
557            - public
558            - private
559
560        state (str):
561            The firewall state. Valid options are:
562
563            - on
564            - off
565            - notconfigured
566
567        store (str):
568            The store to use. This is either the local firewall policy or the
569            policy defined by local group policy. Valid options are:
570
571            - lgpo
572            - local
573
574            Default is ``local``
575
576    Returns:
577        bool: ``True`` if successful
578
579    Raises:
580        CommandExecutionError: If an error occurs
581        ValueError: If the parameters are incorrect
582    """
583    # Input validation
584    if profile.lower() not in ("domain", "public", "private"):
585        raise ValueError("Incorrect profile: {}".format(profile))
586    if state.lower() not in ("on", "off", "notconfigured"):
587        raise ValueError("Incorrect state: {}".format(state))
588
589    # Run the command
590    command = "set {}profile state {}".format(profile, state)
591    results = _netsh_command(command=command, store=store)
592
593    # A successful run should return an empty list
594    if results:
595        raise CommandExecutionError("An error occurred: {}".format(results))
596
597    return True
598