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