├── .dockerignore ├── .github └── workflows │ ├── check.yml │ ├── docker.yml │ └── install.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── logos └── pfFocus.png ├── pf_focus ├── bbcode.py ├── format.py ├── markdown.py ├── parse.py ├── pfsense.py ├── progress.py └── util.py ├── requirements.txt ├── screenshots ├── pfFocus_Filter_rules.png ├── pfFocus_System_Interfaces.png └── pfFocus_xml.png ├── setup.py └── tests ├── Makefile ├── configs ├── .gitignore └── pfSense-CE-2.3.4-RELEASE-amd64-config.xml └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .coverage 3 | .vscode 4 | *.egg-info 5 | test.* 6 | 7 | .git 8 | .dockerignore 9 | Dockerfile 10 | 11 | logos 12 | screenshots 13 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ ubuntu-latest, macos-latest ] 17 | python-version: [ '3.6', '3.7', '3.8', '3.9', 'pypy-3.6', 'pypy-3.7' ] 18 | 19 | name: Python ${{ matrix.python-version }}@${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | architecture: x64 26 | 27 | - run: pip install -r tests/requirements.txt 28 | - run: pip install -r requirements.txt 29 | - run: make 30 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build-and-push: 9 | runs-on: ubuntu-latest 10 | 11 | name: Container Image 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: mback2k/action-docker-package@master 15 | with: 16 | registry_username: ${{ github.repository_owner }} 17 | registry_password: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/install.yml: -------------------------------------------------------------------------------- 1 | name: Install 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ ubuntu-latest, macos-latest ] 15 | python-version: [ '3.6', '3.7', '3.8', '3.9', 'pypy-3.6', 'pypy-3.7' ] 16 | 17 | name: Python ${{ matrix.python-version }}@${{ matrix.os }} 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | architecture: x64 24 | 25 | - run: pip install git+https://github.com/TKCERT/pfFocus.git#egg=pfFocus 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .coverage 3 | .vscode 4 | *.egg-info 5 | test.* 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | COPY ./ /app 4 | WORKDIR /app 5 | 6 | RUN pip install -r /app/requirements.txt 7 | RUN pip install /app 8 | 9 | ENTRYPOINT ["/usr/local/bin/pfFocus-format"] 10 | CMD ["-q", "-f", "md", "-i", "-", "-o", "-"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.py 2 | include LICENSE 3 | include README.md 4 | include requirements.txt 5 | recursive-include pf_focus *.py 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: Pipfile.lock requirements.txt tests/requirements.txt tests 2 | 3 | Pipfile.lock: Pipfile 4 | pipenv lock 5 | 6 | requirements.txt: Pipfile Pipfile.lock 7 | pipenv requirements | grep -v "^\." | grep -v "^-i" > requirements.txt 8 | 9 | tests/requirements.txt: Pipfile Pipfile.lock 10 | pipenv requirements --dev > tests/requirements.txt 11 | 12 | tests: 13 | $(MAKE) -C tests clean 14 | $(MAKE) -C tests all 15 | 16 | build: setup.py 17 | pipenv run python3 setup.py sdist bdist_wheel 18 | 19 | .PHONY: tests 20 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | defusedxml = ">=0.5.0" 8 | pyyaml = ">=4.2b1" 9 | pffocus = {editable = true,path = "."} 10 | 11 | [dev-packages] 12 | coverage = "*" 13 | pylint = "*" 14 | 15 | [requires] 16 | python_version = "3" 17 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "b02e91c8216073ce5bff2a0409aba545e9f0c1fccfd93054afd9c5f1829e1d01" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "defusedxml": { 20 | "hashes": [ 21 | "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", 22 | "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" 23 | ], 24 | "index": "pypi", 25 | "version": "==0.7.1" 26 | }, 27 | "pffocus": { 28 | "editable": true, 29 | "path": "." 30 | }, 31 | "pyyaml": { 32 | "hashes": [ 33 | "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", 34 | "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", 35 | "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", 36 | "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", 37 | "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", 38 | "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", 39 | "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", 40 | "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", 41 | "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", 42 | "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", 43 | "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", 44 | "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", 45 | "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", 46 | "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", 47 | "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", 48 | "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", 49 | "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", 50 | "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", 51 | "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", 52 | "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", 53 | "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", 54 | "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", 55 | "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", 56 | "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", 57 | "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", 58 | "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", 59 | "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", 60 | "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", 61 | "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", 62 | "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", 63 | "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", 64 | "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", 65 | "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", 66 | "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", 67 | "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", 68 | "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", 69 | "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", 70 | "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", 71 | "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", 72 | "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" 73 | ], 74 | "index": "pypi", 75 | "version": "==6.0.1" 76 | } 77 | }, 78 | "develop": { 79 | "astroid": { 80 | "hashes": [ 81 | "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c", 82 | "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd" 83 | ], 84 | "markers": "python_full_version >= '3.7.2'", 85 | "version": "==2.15.6" 86 | }, 87 | "coverage": { 88 | "hashes": [ 89 | "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", 90 | "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2", 91 | "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a", 92 | "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", 93 | "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", 94 | "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6", 95 | "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7", 96 | "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f", 97 | "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02", 98 | "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c", 99 | "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063", 100 | "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", 101 | "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5", 102 | "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959", 103 | "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", 104 | "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", 105 | "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", 106 | "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9", 107 | "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5", 108 | "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f", 109 | "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", 110 | "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", 111 | "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9", 112 | "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f", 113 | "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", 114 | "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb", 115 | "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1", 116 | "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb", 117 | "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250", 118 | "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e", 119 | "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", 120 | "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5", 121 | "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", 122 | "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2", 123 | "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", 124 | "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", 125 | "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", 126 | "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", 127 | "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9", 128 | "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", 129 | "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0", 130 | "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9", 131 | "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", 132 | "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050", 133 | "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d", 134 | "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6", 135 | "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353", 136 | "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb", 137 | "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e", 138 | "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8", 139 | "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495", 140 | "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2", 141 | "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd", 142 | "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27", 143 | "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1", 144 | "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818", 145 | "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", 146 | "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e", 147 | "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850", 148 | "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3" 149 | ], 150 | "index": "pypi", 151 | "version": "==7.2.7" 152 | }, 153 | "dill": { 154 | "hashes": [ 155 | "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", 156 | "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" 157 | ], 158 | "markers": "python_version < '3.11'", 159 | "version": "==0.3.7" 160 | }, 161 | "isort": { 162 | "hashes": [ 163 | "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", 164 | "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" 165 | ], 166 | "markers": "python_full_version >= '3.8.0'", 167 | "version": "==5.12.0" 168 | }, 169 | "lazy-object-proxy": { 170 | "hashes": [ 171 | "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382", 172 | "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82", 173 | "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9", 174 | "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494", 175 | "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46", 176 | "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30", 177 | "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63", 178 | "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4", 179 | "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae", 180 | "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be", 181 | "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701", 182 | "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd", 183 | "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006", 184 | "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a", 185 | "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586", 186 | "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8", 187 | "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821", 188 | "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07", 189 | "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b", 190 | "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171", 191 | "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b", 192 | "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2", 193 | "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7", 194 | "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4", 195 | "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8", 196 | "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e", 197 | "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f", 198 | "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda", 199 | "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4", 200 | "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e", 201 | "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671", 202 | "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11", 203 | "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455", 204 | "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734", 205 | "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb", 206 | "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59" 207 | ], 208 | "markers": "python_version >= '3.7'", 209 | "version": "==1.9.0" 210 | }, 211 | "mccabe": { 212 | "hashes": [ 213 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 214 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 215 | ], 216 | "markers": "python_version >= '3.6'", 217 | "version": "==0.7.0" 218 | }, 219 | "platformdirs": { 220 | "hashes": [ 221 | "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d", 222 | "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d" 223 | ], 224 | "markers": "python_version >= '3.7'", 225 | "version": "==3.10.0" 226 | }, 227 | "pylint": { 228 | "hashes": [ 229 | "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413", 230 | "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252" 231 | ], 232 | "index": "pypi", 233 | "version": "==2.17.5" 234 | }, 235 | "tomli": { 236 | "hashes": [ 237 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 238 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 239 | ], 240 | "markers": "python_version < '3.11'", 241 | "version": "==2.0.1" 242 | }, 243 | "tomlkit": { 244 | "hashes": [ 245 | "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86", 246 | "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899" 247 | ], 248 | "markers": "python_version >= '3.7'", 249 | "version": "==0.12.1" 250 | }, 251 | "typing-extensions": { 252 | "hashes": [ 253 | "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", 254 | "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2" 255 | ], 256 | "markers": "python_version < '3.11'", 257 | "version": "==4.7.1" 258 | }, 259 | "wrapt": { 260 | "hashes": [ 261 | "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0", 262 | "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420", 263 | "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a", 264 | "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c", 265 | "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079", 266 | "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923", 267 | "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f", 268 | "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1", 269 | "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8", 270 | "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86", 271 | "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0", 272 | "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364", 273 | "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e", 274 | "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c", 275 | "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e", 276 | "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c", 277 | "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727", 278 | "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff", 279 | "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e", 280 | "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29", 281 | "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7", 282 | "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72", 283 | "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475", 284 | "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a", 285 | "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317", 286 | "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2", 287 | "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd", 288 | "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640", 289 | "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98", 290 | "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248", 291 | "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e", 292 | "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d", 293 | "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec", 294 | "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1", 295 | "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e", 296 | "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9", 297 | "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92", 298 | "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb", 299 | "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094", 300 | "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46", 301 | "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29", 302 | "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd", 303 | "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705", 304 | "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8", 305 | "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975", 306 | "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb", 307 | "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e", 308 | "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b", 309 | "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418", 310 | "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019", 311 | "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1", 312 | "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba", 313 | "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6", 314 | "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2", 315 | "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3", 316 | "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7", 317 | "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752", 318 | "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416", 319 | "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f", 320 | "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1", 321 | "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc", 322 | "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145", 323 | "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee", 324 | "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a", 325 | "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7", 326 | "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b", 327 | "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653", 328 | "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0", 329 | "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90", 330 | "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29", 331 | "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6", 332 | "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034", 333 | "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09", 334 | "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559", 335 | "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639" 336 | ], 337 | "markers": "python_version < '3.11'", 338 | "version": "==1.15.0" 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![pfFocus](logos/pfFocus.png) 2 | 3 | # pfFocus 4 | 5 | [![Check](https://github.com/TKCERT/pfFocus/actions/workflows/check.yml/badge.svg)](https://github.com/TKCERT/pfFocus/actions/workflows/check.yml) 6 | [![Docker](https://github.com/TKCERT/pfFocus/actions/workflows/docker.yml/badge.svg)](https://github.com/TKCERT/pfFocus/actions/workflows/docker.yml) 7 | 8 | This simple tool allows you to convert a full configuration backup of a *pf*Sense firewall into some meaningful output format, like Markdown or YAML. It enables you to **focus** on the important parts of your firewall configuration and allows you to get a quick overview of the most important settings. 9 | 10 | ## Requirements 11 | 12 | * Python 3.6+ 13 | * defusedxml==0.5.0 14 | * PyYAML==5.4 15 | 16 | ## Screenshots 17 | 18 | **Before:** Configuration backup as XML 19 | 20 | ![Configuration backup as XML](screenshots/pfFocus_xml.png) 21 | 22 | **After:** Markdown documentation 23 | 24 | ![System and Interfaces](screenshots/pfFocus_System_Interfaces.png) 25 | ![Filter rules](screenshots/pfFocus_Filter_rules.png) 26 | 27 | ## Features 28 | 29 | pfFocus currently supports the following configuration sections: 30 | 31 | * Basic system information 32 | * List of interfaces, VLANs, bridges, gateways and static mappings 33 | * List of DHCP ranges and aliases 34 | * NAT rules with alias and interface resolution 35 | * Outbound NAT rules with alias and interface resolution 36 | * Filter rules with alias and interface resolution 37 | * DNS forwarder (DNSmasq) configuration 38 | * OpenVPN server and client configurations 39 | * Syslog and sysctl configuration 40 | 41 | ## Installation 42 | 43 | Install into existing Python environment: 44 | ```bash 45 | pip install git+https://github.com/TKCERT/pfFocus.git#egg=pfFocus 46 | ``` 47 | 48 | Combine this with `--user` or `pipx` or `pipenv` for isolated installation. 49 | 50 | ## Usage 51 | 52 | Main formatting tool: ```pf-format``` 53 | ```bash 54 | pf-format 55 | ``` 56 | 57 | Examples: 58 | ```bash 59 | pf-format -i config-backup.xml -f md -o test.md 60 | pf-format -i config-backup.xml -f yaml -o test.yaml 61 | ``` 62 | 63 | Test parsing tool: ```pf-parse``` 64 | ```bash 65 | pf-parse [-h] input_path 66 | ``` 67 | 68 | Examples: 69 | ```bash 70 | pf-parse config-backup.xml 71 | ``` 72 | 73 | ### Usage via Docker 74 | 75 | When using pfFocus via Docker, you don't need to download it from Github, and you don't need to install Python or any libraries. Only Docker is required. 76 | 77 | It runs this command inside Docker: `pfFocus-format -q -f md -i - -o -`, which means it works with `STDIN` and `STDOUT` instead of files. 78 | 79 | ```bash 80 | docker run --rm -i ghcr.io/tkcert/pffocus < input.xml > output.md 81 | ``` 82 | 83 | If you want you can set up an alias for it in bash: 84 | 85 | ```bash 86 | alias pf-format="docker run --rm -i ghcr.io/tkcert/pffocus" 87 | ``` 88 | 89 | Then you can use it like a normal Unix command, with pipes and redirects: 90 | 91 | ```bash 92 | pf-format < input.xml > output.md 93 | ``` 94 | 95 | ## Roadmap 96 | 97 | Some ideas for the future development of pfFocus: 98 | 99 | * Producing additional output formats, especially structured formats like CSV. 100 | * Using these structured formats to enable easy diff'ing of configurations. 101 | * Maybe functionality to correlate rule configurations of different firewalls. 102 | 103 | ## Credits 104 | 105 | * Thomas Patzke ([@thomaspatzke](https://github.com/thomaspatzke)) for 106 | * valuable suggestions and feedback 107 | * Florian Roth ([@Cyb3rOps](https://twitter.com/Cyb3rOps)) for 108 | * giving it the name *pfFocus* 109 | * the very nice and gorgeous logo 110 | -------------------------------------------------------------------------------- /logos/pfFocus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TKCERT/pfFocus/7112221236ccac4a7ce3cd7f1c8e1c9d4cf54fd4/logos/pfFocus.png -------------------------------------------------------------------------------- /pf_focus/bbcode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from pf_focus.pfsense import PfSenseNode, PfSenseRuleAlias, PfSenseRuleInterface, PfSenseRuleLocation 3 | from pf_focus.util import dict_to_list, obj_to_dict, obj_to_list, hasattr_r 4 | 5 | 6 | def size(s, size): 7 | return "[size={size}]{s}[/size]".format(size=size, s=s) 8 | 9 | def bold(s): 10 | return "[b]{s}[/b]".format(s=s) 11 | 12 | def xlarge(s): 13 | return size(s, 'x-large') 14 | 15 | def large(s): 16 | return size(s, 'large') 17 | 18 | def h1(s): 19 | return xlarge(bold(s)) 20 | 21 | def h2(s): 22 | return large(bold(s)) 23 | 24 | def h3(s): 25 | return bold(s) 26 | 27 | def format_rule_interface(rule_interface): 28 | if isinstance(rule_interface, list): 29 | rule_interface = ', '.join(map(format_rule_interface, rule_interface)) 30 | elif isinstance(rule_interface, dict): 31 | if 'descr' in rule_interface['interface']: 32 | rule_interface = '[{name}](#interfaces "{descr}")'.format(**rule_interface['interface']) 33 | else: 34 | rule_interface = rule_interface['interface']['name'] 35 | return str(rule_interface) 36 | 37 | def format_rule_alias(rule_alias): 38 | if isinstance(rule_alias, dict): 39 | if 'alias' in rule_alias: 40 | if 'address' in rule_alias['alias']: 41 | rule_alias = '[{name}](#aliases "{address}")'.format(**rule_alias['alias']) 42 | else: 43 | rule_alias = rule_alias['alias']['name'] 44 | elif 'interface' in rule_alias: 45 | if 'descr' in rule_alias['interface']: 46 | rule_alias = '[{name}](#interfaces "{descr}")'.format(**rule_alias['interface']) 47 | else: 48 | rule_alias = rule_alias['interface']['name'] 49 | return str(rule_alias) 50 | 51 | def format_rule_location(rule_location): 52 | if isinstance(rule_location, PfSenseRuleAlias): 53 | rule_location = format_rule_alias(rule_location.data) 54 | return str(rule_location) 55 | 56 | def format_bbcode_cell(cell): 57 | if cell is None or (isinstance(cell, PfSenseNode) and cell.data is None): 58 | cell = '' 59 | elif cell is True or (isinstance(cell, PfSenseNode) and cell.data is True): 60 | cell = 'x' 61 | elif isinstance(cell, PfSenseRuleAlias): 62 | cell = format_rule_alias(cell.data) 63 | elif isinstance(cell, PfSenseRuleInterface): 64 | cell = format_rule_interface(cell.data) 65 | elif isinstance(cell, PfSenseRuleLocation): 66 | if hasattr(cell, 'not'): 67 | data = '**!** ' 68 | else: 69 | data = '' 70 | if hasattr(cell, 'any'): 71 | data += 'any' 72 | elif hasattr(cell, 'address'): 73 | data += format_rule_location(cell.address) 74 | elif hasattr(cell, 'network'): 75 | data += format_rule_location(cell.network) 76 | if hasattr(cell, 'port'): 77 | data += ':' 78 | data += str(cell.port) 79 | cell = data 80 | return str(cell).replace('[', '{').replace(']', '}').replace('\n', ' ') 81 | 82 | def output_bbcode_table(stream, header, rows): 83 | stream.write("[table]\n") 84 | 85 | # Header 86 | stream.write("[tr]") 87 | for cell in header: 88 | stream.write("[td]%s[/td]" % cell) 89 | stream.write("[/tr]\n") 90 | 91 | # Seperator 92 | stream.write("[tr]") 93 | for cell in map(lambda x: '-'*len(x), header): 94 | stream.write("[td]%s[/td]" % cell) 95 | stream.write("[/tr]\n") 96 | 97 | # Rows 98 | for row in rows: 99 | stream.write("[tr]") 100 | for cell in map(format_bbcode_cell, row): 101 | stream.write("[td]%s[/td]" % cell) 102 | stream.write("[/tr]\n") 103 | stream.write("[/table]\n") 104 | 105 | def output_bbcode(doc, stream): 106 | stream.write(h1("pfSense\n")) 107 | stream.write("Version {}\n".format(doc.pfsense.version)) 108 | stream.write("\n") 109 | 110 | stream.write(h2("System\n")) 111 | info = obj_to_dict(doc.pfsense.system, ('hostname', 'domain', 'timeservers', 'timezone', 'language', 'dnsserver')) 112 | info['dnsserver'] = ', '.join(map(format_bbcode_cell, info['dnsserver'])) 113 | output_bbcode_table(stream, ('Option', 'Value'), info.items()) 114 | stream.write("\n") 115 | 116 | if hasattr_r(doc.pfsense, 'interfaces'): 117 | stream.write(h2("Interfaces\n")) 118 | interfaces = sorted(doc.pfsense.interfaces.data.items(), key=lambda interface: interface[0]) 119 | interfaces = [[interface_name]+dict_to_list(interface_data, ('enable', 'descr', 'if', 'ipaddr', 'subnet')) for interface_name, interface_data in interfaces] 120 | output_bbcode_table(stream, ('Name', 'Enabled', 'Description', 'Interface', 'Address', 'Subnet'), interfaces) 121 | stream.write("\n") 122 | 123 | if hasattr_r(doc.pfsense, 'vlans.vlan'): 124 | stream.write(h2("VLANs\n")) 125 | vlans = [obj_to_list(vlan, ('vlanif', 'tag', 'if', 'descr')) for vlan in doc.pfsense.vlans.vlan] 126 | output_bbcode_table(stream, ('Name', 'Tag', 'Interface', 'Description'), vlans) 127 | stream.write("\n") 128 | 129 | if hasattr_r(doc.pfsense, 'bridges.bridged'): 130 | stream.write(h2("Bridges\n")) 131 | bridges = [obj_to_list(bridge, ('bridgeif', 'members', 'descr')) for bridge in doc.pfsense.bridges.bridged] 132 | output_bbcode_table(stream, ('Name', 'Members', 'Description'), bridges) 133 | stream.write("\n") 134 | 135 | if hasattr_r(doc.pfsense, 'gateways.gateway_item'): 136 | stream.write(h2("Gateways\n")) 137 | gateways = [obj_to_list(gateway, ('defaultgw', 'name', 'interface', 'gateway', 'weight', 'ipprotocol', 'descr')) for gateway in doc.pfsense.gateways.gateway_item] 138 | output_bbcode_table(stream, ('Default', 'Name', 'Interface', 'Gateway', 'Weight', 'IP', 'Description'), gateways) 139 | stream.write("\n") 140 | 141 | if hasattr_r(doc.pfsense, 'staticroutes.route'): 142 | stream.write(h2("Static routes\n")) 143 | routes = [obj_to_list(route, ('network', 'gateway', 'descr')) for route in doc.pfsense.staticroutes.route] 144 | output_bbcode_table(stream, ('Network', 'Gateway', 'Description'), routes) 145 | stream.write("\n") 146 | 147 | if hasattr_r(doc.pfsense, 'dhcpd'): 148 | stream.write(h2("DHCP ranges\n")) 149 | for dhcpd_interface_name in sorted(doc.pfsense.dhcpd.data.keys()): 150 | dhcpd_interface = PfSenseRuleInterface(parent=doc.pfsense.dhcpd) 151 | dhcpd_interface.string = dhcpd_interface_name 152 | stream.write(h3("DHCPd configuration for {}\n".format(format_bbcode_cell(dhcpd_interface)))) 153 | dhcpd = getattr(doc.pfsense.dhcpd, dhcpd_interface_name) 154 | dhcpd_dict = obj_to_dict(dhcpd, ('enable', 'defaultleasetime', 'maxleasetime')) 155 | output_bbcode_table(stream, ('Option', 'Value'), dhcpd_dict.items()) 156 | stream.write("\n") 157 | if hasattr_r(dhcpd, 'range'): 158 | stream.write(h3("Ranges\n")) 159 | ranges = [obj_to_list(range, ('from', 'to')) for range in dhcpd.range] 160 | output_bbcode_table(stream, ('From', 'To'), ranges) 161 | stream.write("\n") 162 | if hasattr_r(dhcpd, 'staticmap'): 163 | stream.write(h3("Static mappings\n")) 164 | staticmaps = [obj_to_list(staticmap, ('mac', 'ipaddr', 'hostname')) for staticmap in dhcpd.staticmap] 165 | output_bbcode_table(stream, ('MAC', 'Address', 'Hostname'), staticmaps) 166 | stream.write("\n") 167 | stream.write("\n") 168 | 169 | if hasattr_r(doc.pfsense, 'aliases.alias'): 170 | stream.write(h2("Aliases\n")) 171 | aliases = [obj_to_list(alias, ('name', 'type', 'address', 'descr', 'detail')) for alias in doc.pfsense.aliases.alias] 172 | output_bbcode_table(stream, ('Name', 'Type', 'Address', 'Description', 'Detail'), aliases) 173 | stream.write("\n") 174 | 175 | if hasattr_r(doc.pfsense, 'nat.rule'): 176 | stream.write(h2("NAT rules\n")) 177 | rules = [obj_to_list(rule, ('disabled', 'interface', 'source', 'destination', 'protocol', 'target', 'local_port', 'descr')) for rule in doc.pfsense.nat.rule] 178 | output_bbcode_table(stream, ('Disabled', 'Interface', 'Source', 'Destination', 'Protocol', 'Target', 'Local port', 'Description'), rules) 179 | stream.write("\n") 180 | 181 | if hasattr_r(doc.pfsense, 'nat.outbound.rule'): 182 | stream.write(h2("Outbound NAT rules\n")) 183 | rules = [obj_to_list(rule, ('disabled', 'interface', 'source', 'destination', 'dstport', 'protocol', 'target', 'descr')) for rule in doc.pfsense.nat.outbound.rule] 184 | output_bbcode_table(stream, ('Disabled', 'Interface', 'Source', 'Destination', 'Destination port', 'Protocol', 'Target', 'Description'), rules) 185 | stream.write("\n") 186 | 187 | if hasattr_r(doc.pfsense, 'filter.rule'): 188 | stream.write(h2("Filter rules\n")) 189 | rules = [obj_to_list(rule, ('disabled', 'interface', 'type', 'ipprotocol', 'protocol', 'source', 'destination', 'descr')) for rule in doc.pfsense.filter.rule] 190 | output_bbcode_table(stream, ('Disabled', 'Interface', 'Type', 'IP', 'Protocol', 'Source', 'Destination', 'Description'), rules) 191 | stream.write("\n") 192 | 193 | if hasattr_r(doc.pfsense, 'dnsmasq'): 194 | stream.write(h2("DNSmasq configuration\n")) 195 | dnsmasq = obj_to_dict(doc.pfsense.dnsmasq, ('enable', 'regdhcp', 'regdhcpstatic', 'strict_order', 'custom_options', 'interface')) 196 | output_bbcode_table(stream, ('Option', 'Value'), dnsmasq.items()) 197 | stream.write("\n") 198 | if hasattr_r(doc.pfsense.dnsmasq, 'hosts'): 199 | stream.write(h3("Host overrides\n")) 200 | hosts = [obj_to_dict(host, ('host', 'domain', 'ip', 'descr', 'aliases')) for host in doc.pfsense.dnsmasq.hosts] 201 | hostlists = [[host] + list(map(lambda item: (setattr(item, 'ip', host['ip']), 202 | setattr(item, 'descr', item.description), item.data)[-1], 203 | getattr(host['aliases'], 'item', []))) for host in hosts] 204 | hosts = [dict_to_list(host, ('host', 'domain', 'ip', 'descr')) for hostlist in hostlists for host in hostlist] 205 | output_bbcode_table(stream, ('Host', 'Domain', 'IP', 'Description'), hosts) 206 | stream.write("\n") 207 | if hasattr_r(doc.pfsense.dnsmasq, 'domainoverrides'): 208 | stream.write(h3("Domain overrides\n")) 209 | domains = [obj_to_list(domain, ('domain', 'ip', 'descr')) for domain in doc.pfsense.dnsmasq.domainoverrides] 210 | output_bbcode_table(stream, ('Domain', 'IP', 'Description'), domains) 211 | stream.write("\n") 212 | 213 | if hasattr_r(doc.pfsense, 'openvpn.openvpn_server'): 214 | stream.write(h2("OpenVPN servers\n")) 215 | openvpn_servers = [obj_to_dict(openvpn_server, ('vpnid', 'mode', 'authmode', 'protocol', 'dev_mode', 'interface', 'ipaddr', 'local_port', 216 | 'crypto', 'digest', 'tunnel_network', 'remote_network', 'local_network', 'dynamic_ip', 'pool_enable', 217 | 'topology', 'description', 'custom_options')) for openvpn_server in doc.pfsense.openvpn.openvpn_server] 218 | for openvpn_server in openvpn_servers: 219 | stream.write(h3("{}\n".format(format_bbcode_cell(openvpn_server['description'])))) 220 | output_bbcode_table(stream, ('Option', 'Value'), openvpn_server.items()) 221 | stream.write("\n") 222 | 223 | if hasattr_r(doc.pfsense, 'openvpn.openvpn_client'): 224 | stream.write(h2("OpenVPN clients\n")) 225 | openvpn_clients = [obj_to_dict(openvpn_client, ('vpnid', 'auth_user', 'mode', 'protocol', 'dev_mode', 'interface', 'ipaddr', 'local_port', 226 | 'server_addr', 'server_port', 'crypto', 'digest', 'tunnel_network', 'remote_network', 'local_network', 227 | 'topology', 'description', 'custom_options')) for openvpn_client in doc.pfsense.openvpn.openvpn_client] 228 | for openvpn_client in openvpn_clients: 229 | stream.write(h3("{}\n".format(format_bbcode_cell(openvpn_client['description'])))) 230 | output_bbcode_table(stream, ('Option', 'Value'), openvpn_client.items()) 231 | stream.write("\n") 232 | 233 | if hasattr_r(doc.pfsense, 'openvpn.openvpn_csc'): 234 | stream.write(h2("OpenVPN client specific overrides\n")) 235 | cscs = [obj_to_list(csc, ('server_list', 'common_name', 'description', 'tunnel_network')) for csc in doc.pfsense.openvpn.openvpn_csc] 236 | output_bbcode_table(stream, ('VPN IDs', 'Common Name', 'Description', 'Tunnel Network'), cscs) 237 | stream.write("\n") 238 | 239 | if hasattr_r(doc.pfsense, 'syslog'): 240 | stream.write(h2("Syslog configuration\n")) 241 | syslog = obj_to_dict(doc.pfsense.syslog, ('enable', 'logall', 'logfilesize', 'nentries', 'remoteserver', 'remoteserver2', 'remoteserver3', 'sourceip', 'ipproto')) 242 | output_bbcode_table(stream, ('Option', 'Value'), syslog.items()) 243 | stream.write("\n") 244 | 245 | if hasattr_r(doc.pfsense, 'sysctl.item'): 246 | stream.write(h2("System tunables\n")) 247 | tunables = [obj_to_list(tunable, ('tunable', 'value', 'descr')) for tunable in doc.pfsense.sysctl.item] 248 | output_bbcode_table(stream, ('Name', 'Value', 'Description'), tunables) 249 | stream.write("\n") 250 | -------------------------------------------------------------------------------- /pf_focus/format.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import sys 4 | 5 | import yaml 6 | 7 | from pf_focus.markdown import output_markdown 8 | from pf_focus.bbcode import output_bbcode 9 | from pf_focus.parse import parse_pfsense 10 | from pf_focus.pfsense import PfSenseDocument 11 | from pf_focus.progress import Animation 12 | 13 | 14 | def output_yaml(doc, stream): 15 | yaml.safe_dump(doc.data, stream) 16 | 17 | OUTPUT_FORMATS = { 18 | 'yaml': output_yaml, 19 | 'md': output_markdown, 20 | 'bbcode': output_bbcode, 21 | } 22 | 23 | def get_output_func(args): 24 | return OUTPUT_FORMATS.get(args.output_format, output_yaml) 25 | 26 | def get_progress_animation(args): 27 | return Animation(args.quiet or args.output_path == '-') 28 | 29 | def parse_args(): 30 | parser = argparse.ArgumentParser() 31 | parser.add_argument("-q", dest="quiet", action="store_const", const=True, default=False, help="Hide progress messages") 32 | parser.add_argument("-i", dest="input_path", help="XML input path", required=True) 33 | parser.add_argument("-o", dest="output_path", help="Output path", default="-") 34 | parser.add_argument("-f", dest="output_format", help="Output format", default="yaml", choices=OUTPUT_FORMATS.keys()) 35 | return parser.parse_args() 36 | 37 | def step_parse(args, doc): 38 | if not args.quiet: 39 | print('\u268b Parsing "{}" ...'.format(args.input_path), file=sys.stderr) 40 | with get_progress_animation(args): 41 | parse_pfsense(args.input_path, doc) 42 | if not args.quiet: 43 | print('\u268d Successfully parsed pfSense config version {}.'.format(doc.pfsense.version), file=sys.stderr) 44 | 45 | def step_stdout(args, doc, output_func): 46 | if not args.quiet: 47 | print('\u2631 Outputting to stdout ...', file=sys.stderr) 48 | with get_progress_animation(args): 49 | output_file = sys.stdout 50 | output_func(doc, output_file) 51 | if not args.quiet: 52 | print('\u2630 Successfully outputted pfSense config as {}.'.format(args.output_format), file=sys.stderr) 53 | 54 | def step_file(args, doc, output_func): 55 | if not args.quiet: 56 | print('\u2631 Outputting to "{}" ...'.format(args.output_path), file=sys.stderr) 57 | with get_progress_animation(args): 58 | with open(args.output_path, 'w+') as output_file: 59 | output_func(doc, output_file) 60 | if not args.quiet: 61 | print('\u2630 Successfully outputted pfSense config as {}.'.format(args.output_format), file=sys.stderr) 62 | 63 | def main(): 64 | args = parse_args() 65 | doc = PfSenseDocument() 66 | output_func = get_output_func(args) 67 | 68 | step_parse(args, doc) 69 | if args.output_path == '-': 70 | step_stdout(args, doc, output_func) 71 | else: 72 | step_file(args, doc, output_func) 73 | 74 | if __name__ == '__main__': 75 | main() 76 | -------------------------------------------------------------------------------- /pf_focus/markdown.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from pf_focus.pfsense import PfSenseNode, PfSenseRuleAlias, PfSenseRuleInterface, PfSenseRuleLocation 3 | from pf_focus.util import dict_to_list, obj_to_dict, obj_to_list, hasattr_r 4 | 5 | 6 | def format_rule_interface(rule_interface): 7 | if isinstance(rule_interface, list): 8 | rule_interface = ', '.join(map(format_rule_interface, rule_interface)) 9 | elif isinstance(rule_interface, dict): 10 | if 'descr' in rule_interface['interface']: 11 | rule_interface = '[{name}](#interfaces "{descr}")'.format(**rule_interface['interface']) 12 | else: 13 | rule_interface = rule_interface['interface']['name'] 14 | return str(rule_interface) 15 | 16 | def format_rule_alias(rule_alias): 17 | if isinstance(rule_alias, dict): 18 | if 'alias' in rule_alias: 19 | if 'address' in rule_alias['alias']: 20 | rule_alias = '[{name}](#aliases "{address}")'.format(**rule_alias['alias']) 21 | else: 22 | rule_alias = rule_alias['alias']['name'] 23 | elif 'interface' in rule_alias: 24 | if 'descr' in rule_alias['interface']: 25 | rule_alias = '[{name}](#interfaces "{descr}")'.format(**rule_alias['interface']) 26 | else: 27 | rule_alias = rule_alias['interface']['name'] 28 | return str(rule_alias) 29 | 30 | def format_rule_location(rule_location): 31 | if isinstance(rule_location, PfSenseRuleAlias): 32 | rule_location = format_rule_alias(rule_location.data) 33 | return str(rule_location) 34 | 35 | def format_markdown_cell(cell): 36 | if cell is None or (isinstance(cell, PfSenseNode) and cell.data is None): 37 | cell = '' 38 | elif cell is True or (isinstance(cell, PfSenseNode) and cell.data is True): 39 | cell = 'x' 40 | elif isinstance(cell, PfSenseRuleAlias): 41 | cell = format_rule_alias(cell.data) 42 | elif isinstance(cell, PfSenseRuleInterface): 43 | cell = format_rule_interface(cell.data) 44 | elif isinstance(cell, PfSenseRuleLocation): 45 | if hasattr(cell, 'not'): 46 | data = '**!** ' 47 | else: 48 | data = '' 49 | if hasattr(cell, 'any'): 50 | data += 'any' 51 | elif hasattr(cell, 'address'): 52 | data += format_rule_location(cell.address) 53 | elif hasattr(cell, 'network'): 54 | data += format_rule_location(cell.network) 55 | if hasattr(cell, 'port'): 56 | data += ':' 57 | data += str(cell.port) 58 | cell = data 59 | return str(cell).replace('|', '\\|').replace('\n', ' ') 60 | 61 | def output_markdown_table(stream, header, rows): 62 | # Header 63 | stream.write("| ") 64 | stream.write(" | ".join(header)) 65 | stream.write(" |\n") 66 | # Seperator 67 | stream.write("| ") 68 | stream.write(" | ".join(map(lambda x: '-'*max(len(x),3), header))) 69 | stream.write(" |\n") 70 | # Rows 71 | for row in rows: 72 | stream.write("| ") 73 | stream.write(" | ".join(map(format_markdown_cell, row))) 74 | stream.write(" |\n") 75 | 76 | def output_markdown(doc, stream): 77 | stream.write("# pfSense\n") 78 | stream.write("Version {}\n".format(doc.pfsense.version)) 79 | stream.write("\n") 80 | 81 | stream.write("## System\n") 82 | info = obj_to_dict(doc.pfsense.system, ('hostname', 'domain', 'timeservers', 'timezone', 'language', 'dnsserver')) 83 | info['dnsserver'] = ', '.join(map(format_markdown_cell, info['dnsserver'])) 84 | output_markdown_table(stream, ('Option', 'Value'), info.items()) 85 | stream.write("\n") 86 | 87 | if hasattr_r(doc.pfsense, 'interfaces'): 88 | stream.write("## Interfaces\n") 89 | interfaces = sorted(doc.pfsense.interfaces.data.items(), key=lambda interface: interface[0]) 90 | interfaces = [[interface_name]+dict_to_list(interface_data, ('enable', 'descr', 'if', 'ipaddr', 'subnet')) for interface_name, interface_data in interfaces] 91 | output_markdown_table(stream, ('Name', 'Enabled', 'Description', 'Interface', 'Address', 'Subnet'), interfaces) 92 | stream.write("\n") 93 | 94 | if hasattr_r(doc.pfsense, 'vlans.vlan'): 95 | stream.write("## VLANs\n") 96 | vlans = [obj_to_list(vlan, ('vlanif', 'tag', 'if', 'descr')) for vlan in doc.pfsense.vlans.vlan] 97 | output_markdown_table(stream, ('Name', 'Tag', 'Interface', 'Description'), vlans) 98 | stream.write("\n") 99 | 100 | if hasattr_r(doc.pfsense, 'bridges.bridged'): 101 | stream.write("## Bridges\n") 102 | bridges = [obj_to_list(bridge, ('bridgeif', 'members', 'descr')) for bridge in doc.pfsense.bridges.bridged] 103 | output_markdown_table(stream, ('Name', 'Members', 'Description'), bridges) 104 | stream.write("\n") 105 | 106 | if hasattr_r(doc.pfsense, 'gateways.gateway_item'): 107 | stream.write("## Gateways\n") 108 | gateways = [obj_to_list(gateway, ('defaultgw', 'name', 'interface', 'gateway', 'weight', 'ipprotocol', 'descr')) for gateway in doc.pfsense.gateways.gateway_item] 109 | output_markdown_table(stream, ('Default', 'Name', 'Interface', 'Gateway', 'Weight', 'IP', 'Description'), gateways) 110 | stream.write("\n") 111 | 112 | if hasattr_r(doc.pfsense, 'staticroutes.route'): 113 | stream.write("## Static routes\n") 114 | routes = [obj_to_list(route, ('network', 'gateway', 'descr')) for route in doc.pfsense.staticroutes.route] 115 | output_markdown_table(stream, ('Network', 'Gateway', 'Description'), routes) 116 | stream.write("\n") 117 | 118 | if hasattr_r(doc.pfsense, 'dhcpd'): 119 | stream.write("## DHCP ranges\n") 120 | for dhcpd_interface_name in sorted(doc.pfsense.dhcpd.data.keys()): 121 | dhcpd_interface = PfSenseRuleInterface(parent=doc.pfsense.dhcpd) 122 | dhcpd_interface.string = dhcpd_interface_name 123 | stream.write("### DHCPd configuration for {}\n".format(format_markdown_cell(dhcpd_interface))) 124 | dhcpd = getattr(doc.pfsense.dhcpd, dhcpd_interface_name) 125 | dhcpd_dict = obj_to_dict(dhcpd, ('enable', 'defaultleasetime', 'maxleasetime')) 126 | output_markdown_table(stream, ('Option', 'Value'), dhcpd_dict.items()) 127 | stream.write("\n") 128 | if hasattr_r(dhcpd, 'range'): 129 | stream.write("#### Ranges\n") 130 | ranges = [obj_to_list(range, ('from', 'to')) for range in dhcpd.range] 131 | output_markdown_table(stream, ('From', 'To'), ranges) 132 | stream.write("\n") 133 | if hasattr_r(dhcpd, 'staticmap'): 134 | stream.write("#### Static mappings\n") 135 | staticmaps = [obj_to_list(staticmap, ('mac', 'ipaddr', 'hostname')) for staticmap in dhcpd.staticmap] 136 | output_markdown_table(stream, ('MAC', 'Address', 'Hostname'), staticmaps) 137 | stream.write("\n") 138 | stream.write("\n") 139 | 140 | if hasattr_r(doc.pfsense, 'aliases.alias'): 141 | stream.write("## Aliases\n") 142 | aliases = [obj_to_list(alias, ('name', 'type', 'address', 'descr', 'detail')) for alias in doc.pfsense.aliases.alias] 143 | output_markdown_table(stream, ('Name', 'Type', 'Address', 'Description', 'Detail'), aliases) 144 | stream.write("\n") 145 | 146 | if hasattr_r(doc.pfsense, 'nat.rule'): 147 | stream.write("## NAT rules\n") 148 | rules = [obj_to_list(rule, ('disabled', 'interface', 'source', 'destination', 'protocol', 'target', 'local_port', 'descr')) for rule in doc.pfsense.nat.rule] 149 | output_markdown_table(stream, ('Disabled', 'Interface', 'Source', 'Destination', 'Protocol', 'Target', 'Local port', 'Description'), rules) 150 | stream.write("\n") 151 | 152 | if hasattr_r(doc.pfsense, 'nat.outbound.rule'): 153 | stream.write("## Outbound NAT rules\n") 154 | rules = [obj_to_list(rule, ('disabled', 'interface', 'source', 'destination', 'dstport', 'protocol', 'target', 'descr')) for rule in doc.pfsense.nat.outbound.rule] 155 | output_markdown_table(stream, ('Disabled', 'Interface', 'Source', 'Destination', 'Destination port', 'Protocol', 'Target', 'Description'), rules) 156 | stream.write("\n") 157 | 158 | if hasattr_r(doc.pfsense, 'filter.rule'): 159 | stream.write("## Filter rules\n") 160 | rules = [obj_to_list(rule, ('disabled', 'interface', 'type', 'ipprotocol', 'protocol', 'source', 'destination', 'descr')) for rule in doc.pfsense.filter.rule] 161 | output_markdown_table(stream, ('Disabled', 'Interface', 'Type', 'IP', 'Protocol', 'Source', 'Destination', 'Description'), rules) 162 | stream.write("\n") 163 | 164 | if hasattr_r(doc.pfsense, 'dnsmasq'): 165 | stream.write("## DNSmasq configuration\n") 166 | dnsmasq = obj_to_dict(doc.pfsense.dnsmasq, ('enable', 'regdhcp', 'regdhcpstatic', 'strict_order', 'custom_options', 'interface')) 167 | output_markdown_table(stream, ('Option', 'Value'), dnsmasq.items()) 168 | stream.write("\n") 169 | if hasattr_r(doc.pfsense.dnsmasq, 'hosts'): 170 | stream.write("### Host overrides\n") 171 | hosts = [obj_to_dict(host, ('host', 'domain', 'ip', 'descr', 'aliases')) for host in doc.pfsense.dnsmasq.hosts] 172 | hostlists = [[host] + list(map(lambda item: (setattr(item, 'ip', host['ip']), 173 | setattr(item, 'descr', item.description), item.data)[-1], 174 | getattr(host['aliases'], 'item', []))) for host in hosts] 175 | hosts = [dict_to_list(host, ('host', 'domain', 'ip', 'descr')) for hostlist in hostlists for host in hostlist] 176 | output_markdown_table(stream, ('Host', 'Domain', 'IP', 'Description'), hosts) 177 | stream.write("\n") 178 | if hasattr_r(doc.pfsense.dnsmasq, 'domainoverrides'): 179 | stream.write("### Domain overrides\n") 180 | domains = [obj_to_list(domain, ('domain', 'ip', 'descr')) for domain in doc.pfsense.dnsmasq.domainoverrides] 181 | output_markdown_table(stream, ('Domain', 'IP', 'Description'), domains) 182 | stream.write("\n") 183 | 184 | if hasattr_r(doc.pfsense, 'openvpn.openvpn_server'): 185 | stream.write("## OpenVPN servers\n") 186 | openvpn_servers = [obj_to_dict(openvpn_server, ('vpnid', 'mode', 'authmode', 'protocol', 'dev_mode', 'interface', 'ipaddr', 'local_port', 187 | 'crypto', 'digest', 'tunnel_network', 'remote_network', 'local_network', 'dynamic_ip', 'pool_enable', 188 | 'topology', 'description', 'custom_options')) for openvpn_server in doc.pfsense.openvpn.openvpn_server] 189 | for openvpn_server in openvpn_servers: 190 | stream.write("### {}\n".format(format_markdown_cell(openvpn_server['description']))) 191 | output_markdown_table(stream, ('Option', 'Value'), openvpn_server.items()) 192 | stream.write("\n") 193 | 194 | if hasattr_r(doc.pfsense, 'openvpn.openvpn_client'): 195 | stream.write("## OpenVPN clients\n") 196 | openvpn_clients = [obj_to_dict(openvpn_client, ('vpnid', 'auth_user', 'mode', 'protocol', 'dev_mode', 'interface', 'ipaddr', 'local_port', 197 | 'server_addr', 'server_port', 'crypto', 'digest', 'tunnel_network', 'remote_network', 'local_network', 198 | 'topology', 'description', 'custom_options')) for openvpn_client in doc.pfsense.openvpn.openvpn_client] 199 | for openvpn_client in openvpn_clients: 200 | stream.write("### {}\n".format(format_markdown_cell(openvpn_client['description']))) 201 | output_markdown_table(stream, ('Option', 'Value'), openvpn_client.items()) 202 | stream.write("\n") 203 | 204 | if hasattr_r(doc.pfsense, 'openvpn.openvpn_csc'): 205 | stream.write("## OpenVPN client specific overrides\n") 206 | cscs = [obj_to_list(csc, ('server_list', 'common_name', 'description', 'tunnel_network')) for csc in doc.pfsense.openvpn.openvpn_csc] 207 | output_markdown_table(stream, ('VPN IDs', 'Common Name', 'Description', 'Tunnel Network'), cscs) 208 | stream.write("\n") 209 | 210 | if hasattr_r(doc.pfsense, 'syslog'): 211 | stream.write("## Syslog configuration\n") 212 | syslog = obj_to_dict(doc.pfsense.syslog, ('enable', 'logall', 'logfilesize', 'nentries', 'remoteserver', 'remoteserver2', 'remoteserver3', 'sourceip', 'ipproto')) 213 | output_markdown_table(stream, ('Option', 'Value'), syslog.items()) 214 | stream.write("\n") 215 | 216 | if hasattr_r(doc.pfsense, 'sysctl.item'): 217 | stream.write("## System tunables\n") 218 | tunables = [obj_to_list(tunable, ('tunable', 'value', 'descr')) for tunable in doc.pfsense.sysctl.item] 219 | output_markdown_table(stream, ('Name', 'Value', 'Description'), tunables) 220 | stream.write("\n") 221 | -------------------------------------------------------------------------------- /pf_focus/parse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import io 4 | import sys 5 | from pprint import pprint 6 | from xml.sax import ContentHandler 7 | 8 | from defusedxml.sax import parse 9 | 10 | from pf_focus.pfsense import PfSenseDocument 11 | from pf_focus.util import DataList 12 | 13 | 14 | class PfSenseContentHandler(ContentHandler): 15 | def __init__(self, document): 16 | self.document = document 17 | self.stack = [] 18 | 19 | def startDocument(self): 20 | stack_chars = io.StringIO() 21 | stack_frame = (self.document, None, 'element', stack_chars) 22 | self.stack.append(stack_frame) 23 | 24 | def startElement(self, name, attrs): 25 | attr_name = name.replace('-', '_') 26 | top, _, _, _ = self.stack[-1] 27 | 28 | klass = None 29 | klass_type = 'unknown' 30 | klass_lookup = '_%s' % attr_name 31 | klass = getattr(top, klass_lookup, None) 32 | if isinstance(klass, list): 33 | klass = klass[0] 34 | klass_type = 'element' 35 | elif not klass is None: 36 | klass_type = 'attribute' 37 | if not klass is None: 38 | cur = klass(top) 39 | else: 40 | cur = None 41 | 42 | stack_chars = io.StringIO() 43 | stack_frame = (cur, name, klass_type, stack_chars) 44 | self.stack.append(stack_frame) 45 | 46 | def characters(self, content): 47 | cur, _, _, stack_chars = self.stack[-1] 48 | if not stack_chars is None: 49 | stack_chars.write(content) 50 | if not cur is None: 51 | cur(stack_chars.getvalue()) 52 | 53 | def endElement(self, name): 54 | cur, cur_name, cur_type, _ = self.stack.pop() 55 | if name != cur_name: 56 | raise RuntimeError("Invalid stack order") 57 | 58 | attr_name = name.replace('-', '_') 59 | top, _, _, _ = self.stack[-1] 60 | 61 | if cur_type == 'element': 62 | elements = getattr(top, attr_name, DataList()) 63 | elements.append(cur) 64 | setattr(top, attr_name, elements) 65 | 66 | elif cur_type == 'attribute': 67 | setattr(top, attr_name, cur) 68 | 69 | def endDocument(self): 70 | if self.stack[-1][0] != self.document: 71 | raise RuntimeError("Pending stack elements") 72 | 73 | def parse_pfsense(input_path, document): 74 | handler = PfSenseContentHandler(document) 75 | if input_path == '-': 76 | with sys.stdin as input_file: 77 | parse(input_file, handler) 78 | else: 79 | with open(input_path, 'rb') as input_file: 80 | parse(input_file, handler) 81 | 82 | def parse_args(): 83 | parser = argparse.ArgumentParser() 84 | parser.add_argument("input_path", help="XML input path") 85 | return parser.parse_args() 86 | 87 | def main(): 88 | args = parse_args() 89 | doc = PfSenseDocument() 90 | parse_pfsense(args.input_path, doc) 91 | pprint(doc) 92 | 93 | if __name__ == '__main__': 94 | main() 95 | -------------------------------------------------------------------------------- /pf_focus/pfsense.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | from datetime import datetime, timezone 4 | from pprint import pformat 5 | 6 | from pf_focus.util import DataNode, hasattr_r 7 | 8 | 9 | class PfSenseNode(DataNode): 10 | def __init__(self, parent=None): 11 | self._parent = parent 12 | 13 | def __getattr__(self, name): 14 | # This trick hides PyLint error messages... 15 | return super().__getattribute__(name) 16 | 17 | def __call__(self, content): 18 | pass # discard content 19 | 20 | def __repr__(self): 21 | return pformat(self.data) 22 | 23 | def __str__(self): 24 | return str(self.data) 25 | 26 | @property 27 | def parents(self): 28 | obj = self 29 | while obj._parent: 30 | yield obj._parent 31 | obj = obj._parent 32 | 33 | @property 34 | def rootdoc(self): 35 | return list(self.parents)[-1] 36 | 37 | class PfSenseString(PfSenseNode): 38 | string = None 39 | 40 | def __call__(self, content): 41 | self.string = str(content) 42 | 43 | @property 44 | def data(self): 45 | return self.string 46 | 47 | class PfSenseInteger(PfSenseNode): 48 | integer = None 49 | 50 | def __call__(self, content): 51 | self.integer = int(content) 52 | 53 | @property 54 | def data(self): 55 | return self.integer 56 | 57 | class PfSenseTimestamp(PfSenseNode): 58 | datetime = None 59 | 60 | def __call__(self, content): 61 | self.datetime = datetime.fromtimestamp(int(content), timezone.utc) 62 | 63 | @property 64 | def data(self): 65 | return self.datetime 66 | 67 | class PfSenseInterfacesNode(PfSenseNode): 68 | def __getattr__(self, name): 69 | if name.startswith('_opt'): 70 | return self._opt 71 | return super().__getattribute__(name) 72 | 73 | class PfSenseFlag(PfSenseNode): 74 | @property 75 | def data(self): 76 | return True 77 | 78 | class PfSenseAliasString(PfSenseString): 79 | @property 80 | def data(self): 81 | data = super().data 82 | if hasattr_r(self.rootdoc.pfsense, 'aliases.alias'): 83 | for alias in self.rootdoc.pfsense.aliases.alias: 84 | if alias.name.string == data: 85 | return {'alias': alias.data} 86 | return data 87 | 88 | class PfSensePortString(PfSenseAliasString): 89 | PORT_STRING = re.compile(r'(\d+((:|-)(\d+))?|[a-zA-Z0-9_]+)') 90 | 91 | def __call__(self, content): 92 | super().__call__(content) 93 | if self.PORT_STRING.fullmatch(self.string) is None: 94 | raise RuntimeError("Invalid port string: {}".format(self.string)) 95 | 96 | class PfSenseChange(PfSenseNode): 97 | _time = PfSenseTimestamp 98 | _username = PfSenseString 99 | 100 | class PfSenseRange(PfSenseNode): 101 | _from = PfSenseString 102 | _to = PfSenseString 103 | 104 | class PfSenseSysCtlItem(PfSenseNode): 105 | _tunable = PfSenseString 106 | _value = PfSenseString 107 | _descr = PfSenseString 108 | 109 | class PfSenseSysCtl(PfSenseNode): 110 | _item = [PfSenseSysCtlItem] 111 | 112 | class PfSenseStaticMap(PfSenseNode): 113 | _mac = PfSenseString 114 | _ipaddr = PfSenseString 115 | _hostname = PfSenseString 116 | 117 | class PfSenseDhcpdItem(PfSenseNode): 118 | _range = [PfSenseRange] 119 | _staticmap = [PfSenseStaticMap] 120 | _defaultleasetime = PfSenseInteger 121 | _maxleasetime = PfSenseInteger 122 | _enable = PfSenseFlag 123 | 124 | class PfSenseDhcpd(PfSenseInterfacesNode): 125 | _wan = PfSenseDhcpdItem 126 | _lan = PfSenseDhcpdItem 127 | _opt = PfSenseDhcpdItem 128 | 129 | class PfSenseRuleAlias(PfSenseString): 130 | @property 131 | def data(self): 132 | data = super().data 133 | for interface_name, interface_data in self.rootdoc.pfsense.interfaces.data.items(): 134 | alias_name = data 135 | if alias_name.endswith('ip'): 136 | alias_name = alias_name[:-2] 137 | if interface_name == alias_name: 138 | interface_data['name'] = data 139 | return {'interface': interface_data} 140 | if hasattr_r(self.rootdoc.pfsense, 'aliases.alias'): 141 | for alias in self.rootdoc.pfsense.aliases.alias: 142 | if alias.name.string == data: 143 | return {'alias': alias.data} 144 | return data 145 | 146 | class PfSenseRuleInterface(PfSenseString): 147 | @property 148 | def data(self): 149 | data = super().data 150 | if data is None: 151 | return data 152 | data_list = [] 153 | for iface_name in data.split(','): 154 | found = False 155 | for interface_name, interface_data in self.rootdoc.pfsense.interfaces.data.items(): 156 | if interface_name == iface_name: 157 | interface_data['name'] = iface_name 158 | data_list.append({'interface': interface_data}) 159 | found = True 160 | break 161 | if not found: 162 | data_list.append(iface_name) 163 | return data_list 164 | 165 | class PfSenseRuleLocation(PfSenseNode): 166 | _any = PfSenseNode 167 | _network = PfSenseRuleAlias 168 | _address = PfSenseRuleAlias 169 | _port = PfSensePortString 170 | _not = PfSenseFlag 171 | 172 | class PfSenseFilterRule(PfSenseNode): 173 | _id = PfSenseString 174 | _tracker = PfSenseString 175 | _type = PfSenseString 176 | _interface = PfSenseRuleInterface 177 | _ipprotocol = PfSenseString 178 | _tag = PfSenseString 179 | _tagged = PfSenseString 180 | _max = PfSenseString 181 | _max_src_nodes = PfSenseString 182 | _max_src_conn = PfSenseString 183 | _max_src_states = PfSenseString 184 | _statetimeout = PfSenseString 185 | _statetype = PfSenseString 186 | _os = PfSenseString 187 | _protocol = PfSenseString 188 | _source = PfSenseRuleLocation 189 | _destination = PfSenseRuleLocation 190 | _descr = PfSenseString 191 | _associated_rule_id = PfSenseString 192 | _created = PfSenseChange 193 | _updated = PfSenseChange 194 | _disabled = PfSenseFlag 195 | 196 | class PfSenseFilter(PfSenseNode): 197 | _rule = [PfSenseFilterRule] 198 | 199 | class PfSenseNatOutboundRule(PfSenseNode): 200 | _interface = PfSenseRuleInterface 201 | _source = PfSenseRuleLocation 202 | _dstport = PfSensePortString 203 | _target = PfSenseString 204 | _targetip = PfSenseString 205 | _targetip_subnet = PfSenseString 206 | _destination = PfSenseRuleLocation 207 | _natport = PfSensePortString 208 | _staticnatport = PfSensePortString 209 | _descr = PfSenseString 210 | _created = PfSenseChange 211 | _updated = PfSenseChange 212 | _disabled = PfSenseFlag 213 | 214 | class PfSenseNatOutbound(PfSenseNode): 215 | _mode = PfSenseString 216 | _rule = [PfSenseNatOutboundRule] 217 | 218 | class PfSenseNatRule(PfSenseNode): 219 | _source = PfSenseRuleLocation 220 | _destination = PfSenseRuleLocation 221 | _protocol = PfSenseString 222 | _target = PfSenseRuleAlias 223 | _local_port = PfSensePortString 224 | _interface = PfSenseRuleInterface 225 | _descr = PfSenseString 226 | _associated_rule_id = PfSenseString 227 | _created = PfSenseChange 228 | _updated = PfSenseChange 229 | _disabled = PfSenseFlag 230 | 231 | class PfSenseNat(PfSenseNode): 232 | _outbound = PfSenseNatOutbound 233 | _rule = [PfSenseNatRule] 234 | 235 | class PfSenseAlias(PfSenseNode): 236 | _name = PfSenseString 237 | _type = PfSenseString 238 | _address = PfSenseString 239 | _descr = PfSenseString 240 | _detail = PfSenseString 241 | 242 | class PfSenseAliases(PfSenseNode): 243 | _alias = [PfSenseAlias] 244 | 245 | class PfSenseDnsMasqDomainOverride(PfSenseNode): 246 | _domain = PfSenseString 247 | _ip = PfSenseString 248 | _idx = PfSenseInteger 249 | _descr = PfSenseString 250 | 251 | class PfSenseDnsMasqHostAliasItem(PfSenseNode): 252 | _host = PfSenseString 253 | _domain = PfSenseString 254 | _description = PfSenseString 255 | 256 | class PfSenseDnsMasqHostAliases(PfSenseNode): 257 | _item = [PfSenseDnsMasqHostAliasItem] 258 | 259 | class PfSenseDnsMasqHost(PfSenseNode): 260 | _host = PfSenseString 261 | _domain = PfSenseString 262 | _ip = PfSenseString 263 | _descr = PfSenseString 264 | _aliases = PfSenseDnsMasqHostAliases 265 | 266 | class PfSenseDnsMasq(PfSenseNode): 267 | _enable = PfSenseFlag 268 | _reqdhcp = PfSenseFlag 269 | _reqdhcpstatic = PfSenseFlag 270 | _strict_order = PfSenseFlag 271 | _custom_options = PfSenseString 272 | _interface = PfSenseRuleInterface 273 | _hosts = [PfSenseDnsMasqHost] 274 | _domainoverrides = [PfSenseDnsMasqDomainOverride] 275 | 276 | class PfSenseOpenVpnClient(PfSenseNode): 277 | _vpnid = PfSenseInteger 278 | _auth_user = PfSenseString 279 | _mode = PfSenseString 280 | _protocol = PfSenseString 281 | _dev_mode = PfSenseString 282 | _interface = PfSenseRuleInterface 283 | _ipaddr = PfSenseString 284 | _local_port = PfSenseInteger 285 | _server_addr = PfSenseString 286 | _server_port = PfSenseInteger 287 | _crypto = PfSenseString 288 | _digest = PfSenseString 289 | _tunnel_network = PfSenseString 290 | _remote_network = PfSenseString 291 | _local_network = PfSenseString 292 | _topology = PfSenseString 293 | _description = PfSenseString 294 | _custom_options = PfSenseString 295 | 296 | class PfSenseOpenVpnServer(PfSenseNode): 297 | _vpnid = PfSenseInteger 298 | _mode = PfSenseString 299 | _authmode = PfSenseString 300 | _protocol = PfSenseString 301 | _dev_mode = PfSenseString 302 | _interface = PfSenseRuleInterface 303 | _ipaddr = PfSenseString 304 | _local_port = PfSenseInteger 305 | _crypto = PfSenseString 306 | _digest = PfSenseString 307 | _tunnel_network = PfSenseString 308 | _remote_network = PfSenseString 309 | _local_network = PfSenseString 310 | _dynamic_ip = PfSenseString 311 | _pool_enable = PfSenseString 312 | _topology = PfSenseString 313 | _description = PfSenseString 314 | _custom_options = PfSenseString 315 | 316 | class PfSenseOpenVpnCsc(PfSenseNode): 317 | _server_list = PfSenseString 318 | _common_name = PfSenseString 319 | _description = PfSenseString 320 | _tunnel_network = PfSenseString 321 | 322 | class PfSenseOpenVpn(PfSenseNode): 323 | _openvpn_server = [PfSenseOpenVpnServer] 324 | _openvpn_client = [PfSenseOpenVpnClient] 325 | _openvpn_csc = [PfSenseOpenVpnCsc] 326 | 327 | class PfSenseRoute(PfSenseNode): 328 | _network = PfSenseString 329 | _gateway = PfSenseString 330 | _descr = PfSenseString 331 | 332 | class PfSenseStaticRoutes(PfSenseNode): 333 | _route = [PfSenseRoute] 334 | 335 | class PfSenseGatewayItem(PfSenseNode): 336 | _interface = PfSenseRuleInterface 337 | _gateway = PfSenseString 338 | _name = PfSenseString 339 | _weight = PfSenseInteger 340 | _ipprotocol = PfSenseString 341 | _interval = PfSenseInteger 342 | _alert_interval = PfSenseInteger 343 | _descr = PfSenseString 344 | _defaultgw = PfSenseFlag 345 | 346 | class PfSenseGateways(PfSenseNode): 347 | _gateway_item = [PfSenseGatewayItem] 348 | 349 | class PfSenseVlan(PfSenseNode): 350 | _vlanif = PfSenseString 351 | _tag = PfSenseInteger 352 | _if = PfSenseString 353 | _descr = PfSenseString 354 | 355 | class PfSenseVlans(PfSenseNode): 356 | _vlan = [PfSenseVlan] 357 | 358 | class PfSenseBridged(PfSenseNode): 359 | _bridgeif = PfSenseString 360 | _members = PfSenseRuleInterface 361 | _descr = PfSenseString 362 | 363 | class PfSenseBridges(PfSenseNode): 364 | _bridged = [PfSenseBridged] 365 | 366 | class PfSenseInterface(PfSenseNode): 367 | _if = PfSenseString 368 | _descr = PfSenseString 369 | _ipaddr = PfSenseString 370 | _subnet = PfSenseString 371 | _enable = PfSenseFlag 372 | 373 | class PfSenseInterfaces(PfSenseInterfacesNode): 374 | _wan = PfSenseInterface 375 | _lan = PfSenseInterface 376 | _opt = PfSenseInterface 377 | 378 | class PfSenseSyslog(PfSenseNode): 379 | _nentries = PfSenseInteger 380 | _logfilesize = PfSenseInteger 381 | _remoteserver = PfSenseString 382 | _remoteserver2 = PfSenseString 383 | _remoteserver3 = PfSenseString 384 | _sourceip = PfSenseRuleInterface 385 | _ipproto = PfSenseString 386 | _logall = PfSenseFlag 387 | _enable = PfSenseFlag 388 | 389 | class PfSenseSystem(PfSenseNode): 390 | _optimization = PfSenseString 391 | _hostname = PfSenseString 392 | _domain = PfSenseString 393 | _timeservers = PfSenseString 394 | _timezone = PfSenseString 395 | _language = PfSenseString 396 | _dnsserver = [PfSenseString] 397 | 398 | class PfSenseConfig(PfSenseNode): 399 | _version = PfSenseString 400 | _system = PfSenseSystem 401 | _interfaces = PfSenseInterfaces 402 | _vlans = PfSenseVlans 403 | _bridges = PfSenseBridges 404 | _gateways = PfSenseGateways 405 | _staticroutes = PfSenseStaticRoutes 406 | _aliases = PfSenseAliases 407 | _nat = PfSenseNat 408 | _filter = PfSenseFilter 409 | _dnsmasq = PfSenseDnsMasq 410 | _dhcpd = PfSenseDhcpd 411 | _openvpn = PfSenseOpenVpn 412 | _syslog = PfSenseSyslog 413 | _sysctl = PfSenseSysCtl 414 | 415 | class PfSenseDocument(PfSenseNode): 416 | _pfsense = PfSenseConfig 417 | -------------------------------------------------------------------------------- /pf_focus/progress.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import itertools 3 | import sys 4 | import threading 5 | import time 6 | 7 | 8 | class Animation(threading.Thread): 9 | CHARS = ('\u2630', '\u2631', '\u2632', '\u2634') 10 | 11 | def __init__(self, quiet=False): 12 | super().__init__() 13 | self.quiet = quiet 14 | self.is_running = False 15 | 16 | def __enter__(self): 17 | if not self.quiet: 18 | self.start() 19 | 20 | def __exit__(self, type, value, tb): 21 | if not self.quiet: 22 | self.is_running = False 23 | self.join() 24 | 25 | def run(self): 26 | self.is_running = True 27 | for char in itertools.cycle(self.CHARS): 28 | if self.is_running: 29 | sys.stderr.write('\r{} Working ...'.format(char)) 30 | sys.stderr.flush() 31 | time.sleep(0.1) 32 | else: 33 | sys.stderr.write('\r') 34 | break 35 | self.is_running = False 36 | -------------------------------------------------------------------------------- /pf_focus/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from collections import OrderedDict 3 | 4 | 5 | class DataNode(object): 6 | @property 7 | def data(self): 8 | attr_filter = lambda x: not x[0].startswith('_') 9 | data_items = filter(attr_filter, self.__dict__.items()) 10 | data = {} 11 | for key, value in data_items: 12 | if isinstance(value, DataNode): 13 | data[key] = value.data 14 | else: 15 | data[key] = value 16 | return data 17 | 18 | class DataList(list, DataNode): 19 | @property 20 | def data(self): 21 | data = [] 22 | for value in self: 23 | if isinstance(value, DataNode): 24 | data.append(value.data) 25 | else: 26 | data.append(value) 27 | return data 28 | 29 | 30 | def dict_to_dict(data, attributes): 31 | data_items = [(attribute, data.get(attribute, '')) for attribute in attributes] 32 | return OrderedDict(data_items) 33 | 34 | def dict_to_list(data, attributes): 35 | data_values = [data.get(attribute, '') for attribute in attributes] 36 | return list(data_values) 37 | 38 | def obj_to_dict(obj, attributes): 39 | return dict_to_dict(obj.__dict__, attributes) 40 | 41 | def obj_to_list(obj, attributes): 42 | return dict_to_list(obj.__dict__, attributes) 43 | 44 | def hasattr_r(obj, attribute): 45 | for attr in attribute.split('.'): 46 | if not hasattr(obj, attr): 47 | return False 48 | obj = getattr(obj, attr) 49 | return True 50 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | defusedxml==0.7.1 2 | pyyaml==6.0.1 3 | -------------------------------------------------------------------------------- /screenshots/pfFocus_Filter_rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TKCERT/pfFocus/7112221236ccac4a7ce3cd7f1c8e1c9d4cf54fd4/screenshots/pfFocus_Filter_rules.png -------------------------------------------------------------------------------- /screenshots/pfFocus_System_Interfaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TKCERT/pfFocus/7112221236ccac4a7ce3cd7f1c8e1c9d4cf54fd4/screenshots/pfFocus_System_Interfaces.png -------------------------------------------------------------------------------- /screenshots/pfFocus_xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TKCERT/pfFocus/7112221236ccac4a7ce3cd7f1c8e1c9d4cf54fd4/screenshots/pfFocus_xml.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os.path 3 | 4 | def read_file(filename): 5 | """Read a file into a string""" 6 | path = os.path.abspath(os.path.dirname(__file__)) 7 | filepath = os.path.join(path, filename) 8 | try: 9 | with open(filepath, 'r') as fh: 10 | return fh.read() 11 | except IOError: 12 | return '' 13 | 14 | setup( 15 | name='pfFocus', 16 | version='0.1', 17 | description='Generate meaningful output from your pfSense configuration backup', 18 | long_description=read_file('README.md'), 19 | long_description_content_type='text/markdown', 20 | author='thyssenkrupp CERT', 21 | author_email='tkag-cert@thyssenkrupp.com', 22 | license='GPL-V3', 23 | url='https://github.com/TKCERT/pfFocus', 24 | classifiers=[ 25 | 'Development Status :: 4 - Beta', 26 | 'Intended Audience :: System Administrators', 27 | 'Intended Audience :: Information Technology', 28 | 'Intended Audience :: Science/Research', 29 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 30 | 'Natural Language :: English', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | 'Topic :: System :: Systems Administration', 34 | 'Topic :: Text Editors :: Documentation', 35 | 'Topic :: Text Processing'], 36 | py_modules=[ 37 | 'pf_focus.util', 38 | 'pf_focus.pfsense', 39 | 'pf_focus.progress', 40 | 'pf_focus.parse', 41 | 'pf_focus.format', 42 | 'pf_focus.bbcode', 43 | 'pf_focus.markdown', 44 | ], 45 | entry_points = { 46 | 'console_scripts': [ 47 | 'pf-parse=pf_focus.parse:main', 48 | 'pf-format=pf_focus.format:main', 49 | 'pfFocus-parse=pf_focus.parse:main', 50 | 'pfFocus-format=pf_focus.format:main', 51 | ] 52 | }, 53 | install_requires=read_file('requirements.txt').splitlines(), 54 | include_package_data=True, 55 | ) 56 | -------------------------------------------------------------------------------- /tests/Makefile: -------------------------------------------------------------------------------- 1 | IN_XML := $(wildcard configs/*.xml) 2 | OUT_MD = $(IN_XML:%.xml=%.md) 3 | OUT_YAML = $(IN_XML:%.xml=%.yaml) 4 | OUT_BBCODE = $(IN_XML:%.xml=%.bbcode) 5 | 6 | all: md yaml bbcode 7 | clean: 8 | rm -f $(OUT_MD) $(OUT_YAML) $(OUT_BBCODE) 9 | 10 | md: $(OUT_MD) 11 | yaml: $(OUT_YAML) 12 | bbcode: $(OUT_BBCODE) 13 | 14 | %.md: %.xml 15 | PYTHONPATH=../ coverage run ../pf_focus/format.py -i $< -f md -o $@ 16 | coverage report -m 17 | 18 | %.yaml: %.xml 19 | PYTHONPATH=../ coverage run ../pf_focus/format.py -i $< -f yaml -o $@ 20 | coverage report -m 21 | 22 | %.bbcode: %.xml 23 | PYTHONPATH=../ coverage run ../pf_focus/format.py -i $< -f bbcode -o $@ 24 | coverage report -m 25 | -------------------------------------------------------------------------------- /tests/configs/.gitignore: -------------------------------------------------------------------------------- 1 | *.md 2 | *.yaml 3 | *.bbcode 4 | -------------------------------------------------------------------------------- /tests/configs/pfSense-CE-2.3.4-RELEASE-amd64-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15.4 4 | 5 | 6 | normal 7 | pfSense 8 | localdomain 9 | 10 | 11 | 12 | all 13 | 14 | system 15 | 1998 16 | 0 17 | 18 | 19 | admins 20 | 21 | system 22 | 1999 23 | 0 24 | page-all 25 | 26 | 27 | admin 28 | 29 | system 30 | admins 31 | $2b$10$13u6qwCOwODv34GyCMgdWub6oQF3RX0rG7c3d3X4JvzuEmAXLYDd2 32 | 0 33 | user-shell-access 34 | 35 | 2000 36 | 2000 37 | 0.pfsense.pool.ntp.org 38 | 39 | https 40 | 41 | 42 | yes 43 | 44 | 45 | 46 | hadp 47 | hadp 48 | hadp 49 | 50 | monthly 51 | 52 | 53 | 54 | 55 | 56 | em0 57 | 58 | dhcp 59 | dhcp6 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 0 69 | 70 | 71 | 72 | em1 73 | 192.168.1.1 74 | 24 75 | track6 76 | 64 77 | 78 | 79 | wan 80 | 0 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 192.168.1.100 89 | 192.168.1.199 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | ::1000 98 | ::2000 99 | 100 | assist 101 | medium 102 | 103 | 104 | 105 | 106 | 107 | public 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | automatic 118 | 119 | 120 | 121 | 122 | pass 123 | inet 124 | 125 | lan 126 | 0100000101 127 | 128 | lan 129 | 130 | 131 | 132 | 133 | 134 | 135 | pass 136 | inet6 137 | 138 | lan 139 | 0100000102 140 | 141 | lan 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 1,31 155 | 0-5 156 | * 157 | * 158 | * 159 | root 160 | /usr/bin/nice -n20 adjkerntz -a 161 | 162 | 163 | 1 164 | 3 165 | 1 166 | * 167 | * 168 | root 169 | /usr/bin/nice -n20 /etc/rc.update_bogons.sh 170 | 171 | 172 | */60 173 | * 174 | * 175 | * 176 | * 177 | root 178 | /usr/bin/nice -n20 /usr/local/sbin/expiretable -v -t 3600 sshlockout 179 | 180 | 181 | */60 182 | * 183 | * 184 | * 185 | * 186 | root 187 | /usr/bin/nice -n20 /usr/local/sbin/expiretable -v -t 3600 webConfiguratorlockout 188 | 189 | 190 | 1 191 | 1 192 | * 193 | * 194 | * 195 | root 196 | /usr/bin/nice -n20 /etc/rc.dyndns.update 197 | 198 | 199 | */60 200 | * 201 | * 202 | * 203 | * 204 | root 205 | /usr/bin/nice -n20 /usr/local/sbin/expiretable -v -t 3600 virusprot 206 | 207 | 208 | 30 209 | 12 210 | * 211 | * 212 | * 213 | root 214 | /usr/bin/nice -n20 /etc/rc.update_urltables 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | ICMP 224 | icmp 225 | 226 | 227 | 228 | 229 | TCP 230 | tcp 231 | 232 | 233 | 234 | 235 | HTTP 236 | http 237 | 238 | 239 | / 240 | 241 | 200 242 | 243 | 244 | 245 | HTTPS 246 | https 247 | 248 | 249 | / 250 | 251 | 200 252 | 253 | 254 | 255 | SMTP 256 | send 257 | 258 | 259 | 260 | 220 * 261 | 262 | 263 | 264 | 265 | system_information:col1:show,interfaces:col2:show 266 | 10 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://pypi.org/simple 2 | astroid==2.15.6; python_full_version >= '3.7.2' 3 | coverage==7.2.7 4 | dill==0.3.7; python_version < '3.11' 5 | isort==5.12.0; python_full_version >= '3.8.0' 6 | lazy-object-proxy==1.9.0; python_version >= '3.7' 7 | mccabe==0.7.0; python_version >= '3.6' 8 | platformdirs==3.10.0; python_version >= '3.7' 9 | pylint==2.17.5 10 | tomli==2.0.1; python_version < '3.11' 11 | tomlkit==0.12.1; python_version >= '3.7' 12 | typing-extensions==4.7.1; python_version < '3.11' 13 | wrapt==1.15.0; python_version < '3.11' 14 | defusedxml==0.7.1 15 | . 16 | pyyaml==6.0.1 17 | --------------------------------------------------------------------------------