├── .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 | 
2 |
3 | # pfFocus
4 |
5 | [](https://github.com/TKCERT/pfFocus/actions/workflows/check.yml)
6 | [](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 | 
21 |
22 | **After:** Markdown documentation
23 |
24 | 
25 | 
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 |
--------------------------------------------------------------------------------