1
0
mirror of https://github.com/kellyjonbrazil/jc.git synced 2026-04-03 17:44:07 +02:00

Compare commits

..

154 Commits

Author SHA1 Message Date
Kelly Brazil
572548b42f Merge pull request #141 from kellyjonbrazil/dev
Dev v1.15.5
2021-05-27 17:00:54 -07:00
Kelly Brazil
ff7ab0a1ed doc updates for v1.15.5 2021-05-27 16:58:14 -07:00
Kelly Brazil
5db71b05cb add en_US.UTF-8 to LANG info 2021-05-25 09:47:24 -07:00
Kelly Brazil
f9b952885a add python 3.9.5 packaged info 2021-05-21 10:21:21 -07:00
Kelly Brazil
e7983bc0b2 version bump 2021-05-21 10:16:19 -07:00
Kelly Brazil
473a643142 add windows msi info 2021-05-21 10:16:08 -07:00
Kelly Brazil
14f3d442cb formatting 2021-05-21 09:46:51 -07:00
Kelly Brazil
d6f4ed9ab5 add long-form UTC test 2021-05-21 09:46:34 -07:00
Kelly Brazil
1b8d654444 add error message info 2021-05-20 15:49:18 -07:00
Kelly Brazil
6002af0dca add more error message detail to the user for ParseError and LibraryNotFound exceptions 2021-05-20 15:46:31 -07:00
Kelly Brazil
0924d822a3 add windows example 2021-05-20 15:36:27 -07:00
Kelly Brazil
72a37b9289 version bump 2021-05-19 16:23:34 -07:00
Kelly Brazil
5eef7bd769 use LibraryNotInstalled exception instead of exiting via sys.exit 2021-05-19 16:14:26 -07:00
Kelly Brazil
c6893e1bd5 add LibraryNotInstalled exception 2021-05-19 16:13:05 -07:00
Kelly Brazil
039f6612e4 Merge pull request #139 from kellyjonbrazil/master
sync branches
2021-05-18 10:29:02 -07:00
Kelly Brazil
95aec9c6f9 add info about Magic syntax not supporting shell builtins 2021-05-18 10:27:36 -07:00
Kelly Brazil
4e9652a8ec spelling 2021-05-17 14:36:20 -07:00
Kelly Brazil
773b7f4b1f add space before exit code table 2021-05-17 08:38:09 -07:00
Kelly Brazil
d17ffde9cd Merge pull request #138 from kellyjonbrazil/dev
Dev v1.15.4
2021-05-17 08:35:13 -07:00
Kelly Brazil
9c57c09c00 doc update for release 2021-05-17 08:24:31 -07:00
Kelly Brazil
4d730a9de5 add UTC fix 2021-05-16 20:57:17 -07:00
Kelly Brazil
fc57bcfce2 fix for when UTC is referenced as "Coordinated Universal Time" 2021-05-16 20:51:39 -07:00
Kelly Brazil
fa5571486c simplify json_out function 2021-05-16 20:33:03 -07:00
Kelly Brazil
9996c4fe23 update docs for shell builtins 2021-05-16 19:55:53 -07:00
Kelly Brazil
038d429024 doc updates 2021-05-16 19:44:10 -07:00
Kelly Brazil
9bf6facb0d remove magic command capability since dir is a shell builtin 2021-05-16 19:43:10 -07:00
Kelly Brazil
965717886e add exceptions module info 2021-05-16 19:30:00 -07:00
Kelly Brazil
e9bfc3dd29 add time format, dig compatibility, windows colors fix 2021-05-16 19:21:37 -07:00
Kelly Brazil
f46b33eacf add windows time format 2021-05-16 19:20:13 -07:00
Kelly Brazil
f475fe44df add new time format for systeminfo 2021-05-16 19:09:53 -07:00
Kelly Brazil
5fdbe2962d make dig compatible with all platforms 2021-05-16 17:48:56 -07:00
Kelly Brazil
ab291b9eef only force enable colors when running on windows 2021-05-16 17:48:40 -07:00
Kelly Brazil
fd411fd772 attempt to get colors working on windows 2021-05-16 17:43:05 -07:00
Kelly Brazil
b1e95a60a2 remove unnecessary comment 2021-05-13 08:42:27 -07:00
Kelly Brazil
bb1439f0d5 use ParseError exception from jc.exceptions module 2021-05-13 08:20:58 -07:00
Kelly Brazil
ba963d98a0 add exceptions module including ParseError 2021-05-13 08:20:35 -07:00
Kelly Brazil
83440ccb55 error message capitalization 2021-05-13 08:02:38 -07:00
Kelly Brazil
796f61bfa4 handle case where the user pipes data and uses magic syntax simultaneously 2021-05-12 17:01:09 -07:00
Kelly Brazil
070cac4ae1 remove commented line 2021-05-12 15:36:06 -07:00
Kelly Brazil
3ed84f9f42 reorganize main function. remove pass condition. 2021-05-12 15:19:11 -07:00
Kelly Brazil
a205afb6f3 set run_command_str if run_command is set 2021-05-12 13:44:15 -07:00
Kelly Brazil
a6d983dd8f move run_command_str variable 2021-05-12 13:38:08 -07:00
Kelly Brazil
b6c8d6d01d add exception handling for filenotfound or other subprocess.popen and json.dumps exceptions 2021-05-12 13:18:58 -07:00
Kelly Brazil
b5a5d5b133 set parser_name for magic syntax use 2021-05-12 11:36:27 -07:00
Kelly Brazil
da528e7814 move separators to a variable 2021-05-12 09:40:22 -07:00
Kelly Brazil
4acebf4f62 move variables 2021-05-12 08:48:49 -07:00
Kelly Brazil
4d40808d2b update comments 2021-05-12 08:27:39 -07:00
Kelly Brazil
c543f00bd3 simplify piped_output function 2021-05-11 14:30:46 -07:00
Kelly Brazil
947cf41dfa add exit code info 2021-05-11 12:42:21 -07:00
Kelly Brazil
8d8c58742e formatting 2021-05-11 11:32:08 -07:00
Kelly Brazil
995ecc9bfb add exit codes section 2021-05-11 11:11:17 -07:00
Kelly Brazil
21a15225eb add exit codes section 2021-05-11 10:59:26 -07:00
Kelly Brazil
48921d4584 ensure exit code never exceeds 255 2021-05-11 10:50:35 -07:00
Kelly Brazil
342db45edc fix combined exit codes 2021-05-11 10:36:55 -07:00
Kelly Brazil
5f88ecf844 add comments to magic_parser return 2021-05-10 21:23:23 -07:00
Kelly Brazil
a56aebfe70 remove side-effect comment since it is no longer relevant 2021-05-10 21:09:00 -07:00
Kelly Brazil
422bb744a8 update man pages 2021-05-10 21:03:30 -07:00
Kelly Brazil
c3b814a15f move imports to the top 2021-05-10 21:02:28 -07:00
Kelly Brazil
e4574047a0 update tests for magic_parser function 2021-05-10 20:49:44 -07:00
Kelly Brazil
0d7d7951f8 don't reset sys.argv anymore. check for 'valid_command' instead 2021-05-10 18:58:45 -07:00
Kelly Brazil
da904e4770 remove final \n from stderr string when printing 2021-05-10 18:50:59 -07:00
Kelly Brazil
19b540041a proof of concept for passing command exit codes when using magic syntax. Needs more testing 2021-05-10 18:31:30 -07:00
Kelly Brazil
f2ffb93eea formatting 2021-05-10 10:43:40 -07:00
Kelly Brazil
c0c0e05642 add dig +noall +answer support 2021-05-10 10:40:13 -07:00
Kelly Brazil
966978f17e add more unparsable line ping tests 2021-05-10 10:39:51 -07:00
Kelly Brazil
8ab08a5231 doc update 2021-05-10 10:13:52 -07:00
Kelly Brazil
48e534fa03 add +noall +answer test 2021-05-07 16:55:18 -07:00
Kelly Brazil
61851c1bd0 add support for +noall +answer 2021-05-07 16:42:09 -07:00
Kelly Brazil
3c51b2d83d add tests for unparsable lines on linux 2021-05-07 13:50:28 -07:00
Kelly Brazil
ee3a28528e Merge branch 'dev' of https://github.com/kellyjonbrazil/jc into dev
# Conflicts:
#	templates/readme_template
2021-05-05 10:03:58 -07:00
Kelly Brazil
916bcdae38 Merge pull request #136 from kellyjonbrazil/master
merge changes from master
2021-05-05 10:02:25 -07:00
Kelly Brazil
efb1d3e6b2 add blog title and minor formatting 2021-05-05 09:23:21 -07:00
Kelly Brazil
4e6ae66bac formatting 2021-05-05 09:20:47 -07:00
Kelly Brazil
5ee88e7b67 add use cases section 2021-05-05 09:18:45 -07:00
Kelly Brazil
c3b68903cb add use cases section 2021-05-05 09:17:11 -07:00
Kelly Brazil
fe1f1013a7 add use cases section 2021-05-05 09:16:05 -07:00
Kelly Brazil
fb14f5439f fix and tests for unknown or unparsable errors 2021-05-05 08:03:27 -07:00
Kelly Brazil
5ca0fc364e add unparsed_line to docs 2021-05-04 19:05:20 -07:00
Kelly Brazil
a1fe7037e5 add unparsed_line field if line cannot be parsed 2021-05-04 19:04:16 -07:00
Kelly Brazil
c2af7d113e add ping updates 2021-05-04 15:35:47 -07:00
Kelly Brazil
ff034e401d use try/except to make parser more resilient against unknown error types 2021-05-04 15:34:45 -07:00
Kelly Brazil
5abe095beb update ping docs 2021-05-03 15:26:16 -07:00
Kelly Brazil
33de5f01e6 version bump 2021-05-03 15:16:47 -07:00
Kelly Brazil
8ce155d843 add support for error replies in v4 ping on osx and bsd 2021-05-03 15:16:33 -07:00
Kelly Brazil
b921d5ec95 initial support for error replies in bsd/osx 2021-04-30 16:53:52 -07:00
Kelly Brazil
e21542aaa2 Merge pull request #135 from kellyjonbrazil/dev
Dev clarify packaged binaries in readme
2021-04-30 10:31:55 -07:00
Kelly Brazil
6150aae0ae clarify packaged binaries 2021-04-30 10:31:02 -07:00
Kelly Brazil
f27b35f371 Merge pull request #131 from kellyjonbrazil/master
sync branches
2021-04-28 15:28:35 -07:00
Kelly Brazil
d24f9a885d fix contributing link 2021-04-28 15:26:52 -07:00
Kelly Brazil
48dd82c8d1 trigger page build on github pages 2021-04-28 15:15:41 -07:00
Kelly Brazil
e57167ad1f empty commit 2021-04-28 12:23:31 -07:00
Kelly Brazil
ada9137642 Merge pull request #130 from kellyjonbrazil/dev
Dev link updates
2021-04-28 11:53:15 -07:00
Kelly Brazil
bd428a9fd7 fix link 2021-04-28 11:52:24 -07:00
Kelly Brazil
934941332f minor link updates 2021-04-28 11:50:10 -07:00
Kelly Brazil
0c209dbd10 Merge pull request #129 from kellyjonbrazil/dev
Dev update readme
2021-04-28 11:36:05 -07:00
Kelly Brazil
56e041aa26 add Practical JSON at the Command Line blog link 2021-04-28 11:34:51 -07:00
Kelly Brazil
cf9d48582e Merge pull request #128 from kellyjonbrazil/master
sync branches
2021-04-26 13:17:20 -07:00
Kelly Brazil
92e2252bee fix typo 2021-04-26 12:18:21 -07:00
Kelly Brazil
fd7861db11 Merge pull request #127 from kellyjonbrazil/dev
Dev v1.15.3
2021-04-26 12:12:02 -07:00
Kelly Brazil
35464bbbfb date update 2021-04-26 12:08:10 -07:00
Kelly Brazil
db8ddd7f0e fix for scenarios where the default port range didn't always display, or overrode existing port ranges. 2021-04-26 12:04:03 -07:00
Kelly Brazil
72207c54ee update ufw app info docs to support multiple apps (ufw app info all) 2021-04-26 10:02:02 -07:00
Kelly Brazil
a683f68003 change schema to a list of dictionaries to support ufw app info all use case 2021-04-26 10:00:44 -07:00
Kelly Brazil
8d2d3db3fa formatting 2021-04-25 21:10:21 -07:00
Kelly Brazil
eca785450d add caveats to readme and manpage 2021-04-25 21:06:47 -07:00
Kelly Brazil
5b40a97ce4 add caveats to man page 2021-04-25 20:55:43 -07:00
Kelly Brazil
66cb4e9bde update ufw parser docs 2021-04-25 20:49:35 -07:00
Kelly Brazil
d1f33645ca update ufw tests 2021-04-25 20:40:38 -07:00
Kelly Brazil
5eff65c326 update schema to support port lists and port range lists. Also support other transports than tcp and udp 2021-04-25 20:10:52 -07:00
Kelly Brazil
929c38715d formatting 2021-04-24 12:53:29 -07:00
Kelly Brazil
8c91a7b760 formatting 2021-04-24 12:52:26 -07:00
Kelly Brazil
14289ecd6c more timezone info 2021-04-24 12:50:40 -07:00
Kelly Brazil
b833c44783 add ufw-appinfo parser 2021-04-24 12:47:24 -07:00
Kelly Brazil
e23aa818ea add tests section and timezone info 2021-04-24 12:47:12 -07:00
Kelly Brazil
473d919c13 add another ufw test sample 2021-04-24 10:58:31 -07:00
Kelly Brazil
9f925d6ac9 doc update 2021-04-24 10:22:37 -07:00
Kelly Brazil
f6ca32b3a3 update ufw docs with a note on lists of ports 2021-04-24 10:22:19 -07:00
Kelly Brazil
4cbe84c3d6 add ufw-appinfo tests 2021-04-23 16:15:04 -07:00
Kelly Brazil
ffac5bf9d3 doc update 2021-04-23 16:14:53 -07:00
Kelly Brazil
9aa424f9f8 fix ufw example 2021-04-23 16:14:39 -07:00
Kelly Brazil
0a8495c68e update docs 2021-04-23 16:14:25 -07:00
Kelly Brazil
6aea066cf0 make normalized list appear only when data exists. set magic commands 2021-04-23 16:14:11 -07:00
Kelly Brazil
ff3c2d809e add ufw-appinfo to docs 2021-04-23 09:57:41 -07:00
Kelly Brazil
145059fc89 clean up output if sections are blank 2021-04-23 09:04:11 -07:00
Kelly Brazil
20e5c19de4 fix key error if tcp or udp don't exist 2021-04-23 08:30:34 -07:00
Kelly Brazil
4c98fd4b87 doc update 2021-04-23 08:26:00 -07:00
Kelly Brazil
125e54213e update schema and add normalized fields 2021-04-23 08:13:53 -07:00
Kelly Brazil
0152e0665f initial working ufw-appinfo parser 2021-04-22 16:52:01 -07:00
Kelly Brazil
0679bcbc56 fix ufw tests 2021-04-22 08:33:57 -07:00
Kelly Brazil
5500648aa0 fix ufw docs for int conversions and service behavior 2021-04-22 08:33:45 -07:00
Kelly Brazil
bb50caad4d fix prefix int conversions. don't reset _transport if service is really a list of ports 2021-04-22 08:16:42 -07:00
Kelly Brazil
7eef5d94d5 ufw doc update 2021-04-21 16:55:25 -07:00
Kelly Brazil
74f623f8d6 add support for rule comments and LIMIT and REJECT actions 2021-04-21 16:55:14 -07:00
Kelly Brazil
f638aca092 add deb package name fix 2021-04-21 10:10:36 -07:00
Kelly Brazil
da35eaf80a formatting 2021-04-21 09:10:10 -07:00
Kelly Brazil
ade0e8e8fc add ufw tests 2021-04-21 09:09:23 -07:00
Kelly Brazil
90076090f0 fix int conversions if 'rules' key does not exist 2021-04-21 09:09:09 -07:00
Kelly Brazil
23635def8b add ufw to docs 2021-04-21 07:51:32 -07:00
Kelly Brazil
4eeec087bd add ufw example 2021-04-21 07:49:49 -07:00
Kelly Brazil
7331961038 update docs with new examples that have to_service always existing. 2021-04-21 07:41:32 -07:00
Kelly Brazil
e4acb3d5b7 always make to/from_service None if ports are assigned 2021-04-21 07:08:50 -07:00
Kelly Brazil
c60549a994 change field name from to/from_subnet to to/from_ip_prefix 2021-04-20 22:08:12 -07:00
Kelly Brazil
d46fc8bbfa set to/from_transport to None if to/from_service is set 2021-04-20 21:58:52 -07:00
Kelly Brazil
b133d1f90d strip interface value 2021-04-20 21:52:39 -07:00
Kelly Brazil
5be615a97e add examples for docs 2021-04-20 21:45:13 -07:00
Kelly Brazil
ea1d820f96 convert integer fields 2021-04-20 21:29:49 -07:00
Kelly Brazil
47e262cf72 clean up fields 2021-04-20 21:21:42 -07:00
Kelly Brazil
eec673be90 working raw parser 2021-04-20 20:32:59 -07:00
Kelly Brazil
9a0fb2a7c8 parse major sections 2021-04-20 14:17:01 -07:00
Kelly Brazil
b5145d6c14 version bump to v1.15.3 2021-04-20 13:39:12 -07:00
Kelly Brazil
9747ca414d minor optimization to convert_to_int(). No longer runs through convert_to_float(), but uses standard float() function. 2021-04-20 10:50:20 -07:00
Kelly Brazil
312d465b61 update link 2021-04-19 14:18:12 -07:00
Kelly Brazil
7dcf87d24a formatting 2021-04-19 13:12:55 -07:00
Kelly Brazil
cf3cfd16a9 rename tests 2021-04-19 09:59:58 -07:00
74 changed files with 2621 additions and 253 deletions

View File

@@ -1,5 +1,26 @@
jc changelog
20210520 v1.15.5
- Fix issue where help and about information would not display if a 3rd party parser library was missing. (e.g. xmltodict)
- Add more error message detail when encountering ParseError and LibraryNotFound exceptions
20210517 v1.15.4
- Update ping parser to support error responses in OSX and BSD
- Update ping parser to be more resilient against parsing errors for unknown error types
- Update dig parser to support `+noall +answer` use case
- Update dig parser compatibility to all platforms
- Fix colors in Windows terminals (cmd.exe and PowerShell)
- Fix epoch calculations when UTC is referenced as "Coordinated Universal Time"
- Add Windows time format for systeminfo output
- Add exceptions module to standardize parser exceptions
- JC no longer swallows exit codes when using the "magic" syntax. See the Exit Codes section of the README and man page for details
20210426 v1.15.3
- Add ufw status command parser tested on linux
- Add ufw-appinfo command parser tested on linux
- Fix deb package name to conform to standard
- Add Caveats section to readme and manpage
20210418 v1.15.2
- Add systeminfo parser tested on Windows
- Update dig parser to fix an issue with IPv6 addresses in the server field

View File

@@ -17,7 +17,7 @@ Pull requests are the best way to propose changes to the codebase (we use [Githu
2. Fork the repo and create your branch from `dev`, if available, otherwise `master`.
3. If you've added code that should be tested, add tests. All new parsers should have several sample outputs and tests.
4. Documentation is auto-generated from docstrings, so ensure they are clear and accurate.
5. Ensure the test suite passes.
5. Ensure the test suite passes. (Note: "**America/Los_Angeles**" timezone should be configured on the test system)
6. Make sure your code lints.
7. Issue that pull request!
@@ -61,6 +61,11 @@ Good:
]
```
## Tests
It is essential to have good command output sample coverage and tests to keep the `jc` parser quality high.
Many parsers include calculated timestamp fields using the `jc.utils.timestamp` class. Naive timestamps created with this class should be generated on a system configured with the "**America/Los_Angeles**" timezone on linux/macOS/unix and "**Pacific Standard Time**" timezone on Windows for tests to pass on the Github Actions CI tests. This timezone should be configured on your local system before running the tests locally, as well.
## Any contributions you make will be under the MIT Software License
In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.

View File

@@ -732,7 +732,7 @@ dig -x 1.1.1.1 | jc --dig -p # or: jc -p dig -x 1.1.1.1
```
### dir
```bash
dir | jc --dir -p # or: jc -p dir
dir | jc --dir -p
```
```json
[
@@ -1671,7 +1671,7 @@ iw dev wlan0 scan | jc --iw-scan -p # or: jc -p iw dev wlan0 scan
```
### jobs
```bash
jobs -l | jc --jobs -p # or: jc -p jobs
jobs -l | jc --jobs -p
```
```json
[
@@ -3126,6 +3126,115 @@ traceroute -m 3 8.8.8.8 | jc --traceroute -p # or: jc -p traceroute -m
]
}
```
### ufw status
```bash
ufw status verbose | jc --ufw -p # or jc -p ufw status verbose
```
```json
{
"status": "active",
"logging": "on",
"logging_level": "low",
"default": "deny (incoming), allow (outgoing), disabled (routed)",
"new_profiles": "skip",
"rules": [
{
"action": "ALLOW",
"action_direction": "IN",
"index": null,
"network_protocol": "ipv4",
"to_interface": "any",
"to_transport": "any",
"to_service": null,
"to_ports": [
22
],
"to_ip": "0.0.0.0",
"to_ip_prefix": 0,
"comment": null,
"from_ip": "0.0.0.0",
"from_ip_prefix": 0,
"from_interface": "any",
"from_transport": "any",
"from_port_ranges": [
{
"start": 0,
"end": 65535
}
],
"from_service": null
},
{
"action": "ALLOW",
"action_direction": "IN",
"index": null,
"network_protocol": "ipv4",
"to_interface": "any",
"to_transport": "tcp",
"to_service": null,
"to_ports": [
80,
443
],
"to_ip": "0.0.0.0",
"to_ip_prefix": 0,
"comment": null,
"from_ip": "0.0.0.0",
"from_ip_prefix": 0,
"from_interface": "any",
"from_transport": "any",
"from_port_ranges": [
{
"start": 0,
"end": 65535
}
],
"from_service": null
}
]
}
```
### ufw app info [application]
```bash
ufw app info MSN | jc --ufw-appinfo -p # or: jc -p ufw app info MSN
```
```json
[
{
"profile": "MSN",
"title": "MSN Chat",
"description": "MSN chat protocol (with file transfer and voice)",
"tcp_list": [
1863,
6901
],
"udp_list": [
1863,
6901
],
"tcp_ranges": [
{
"start": 6891,
"end": 6900
}
],
"normalized_tcp_list": [
1863,
6901
],
"normalized_tcp_ranges": [
{
"start": 6891,
"end": 6900
}
],
"normalized_udp_list": [
1863,
6901
]
}
]
```
### uname -a
```bash
uname -a | jc --uname -p # or: jc -p uname -a

View File

@@ -62,26 +62,28 @@ The `jc` parsers can also be used as python modules. In this case the output wil
```
Two representations of the data are possible. The default representation uses a strict schema per parser and converts known numbers to int/float JSON values. Certain known values of `None` are converted to JSON `null`, known boolean values are converted, and, in some cases, additional semantic context fields are added.
> Note: Some parsers have calculated epoch timestamp fields added to the output. Unless a timestamp field name has a `_utc` suffix it is considered naive. (i.e. based on the local timezone of the system the `jc` parser was run on).
>
> If a UTC timezone can be detected in the text of the command output, the timestamp will be timezone aware and have a `_utc` suffix on the key name. (e.g. `epoch_utc`) No other timezones are supported for aware timestamps.
To access the raw, pre-processed JSON, use the `-r` cli option or the `raw=True` function parameter in `parse()`.
Schemas for each parser can be found at the documentation link beside each parser below.
Schemas for each parser can be found at the documentation link beside each [**Parser**](#parsers) below.
Release notes can be found [here](https://blog.kellybrazil.com/category/jc-news/).
## Why Would Anyone Do This!?
For more information on the motivations for this project, please see my [blog post](https://blog.kellybrazil.com/2019/11/26/bringing-the-unix-philosophy-to-the-21st-century/).
For more information on the motivations for this project, please see my blog post on [Bringing the Unix Philosophy to the 21st Century](https://blog.kellybrazil.com/2019/11/26/bringing-the-unix-philosophy-to-the-21st-century/).
See also:
- [libxo on FreeBSD](http://juniper.github.io/libxo/libxo-manual.html)
- [powershell](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-7)
- [blog: linux apps should have a json flag](https://thomashunter.name/posts/2012-06-06-linux-cli-apps-should-have-a-json-flag)
Use Cases:
- [Bash scripting](https://blog.kellybrazil.com/2021/04/12/practical-json-at-the-command-line/)
- [Ansible command output parsing](https://blog.kellybrazil.com/2020/08/30/parsing-command-output-in-ansible-with-jc/)
- [Saltstack command output parsing](https://blog.kellybrazil.com/2020/09/15/parsing-command-output-in-saltstack-with-jc/)
- [Nornir command output parsing](https://blog.kellybrazil.com/2020/12/09/parsing-command-output-in-nornir-with-jc/)
## Installation
There are several ways to get `jc`. You can install via `pip`; other OS package repositories like `apt-get`, `dnf`, `zypper`, `pacman`, `nix-env`, `guix`, `brew`, or `portsnap`; via DEB/RPM packages; or by downloading the correct binary for your architecture and running it anywhere on your filesystem.
There are several ways to get `jc`. You can install via `pip`; other OS package repositories like `apt-get`, `dnf`, `zypper`, `pacman`, `nix-env`, `guix`, `brew`, or `portsnap`; via DEB, RPM, and MSI packaged binaries for linux and Windows; or by downloading the correct binary for your architecture and running it anywhere on your filesystem.
### Pip (macOS, linux, unix, Windows)
```bash
@@ -102,7 +104,7 @@ pip3 install jc
| FreeBSD | `portsnap fetch update && cd /usr/ports/textproc/py-jc && make install clean` |
| Ansible filter plugin | `ansible-galaxy collection install community.general` |
> For more packages and binaries, see https://kellyjonbrazil.github.io/jc-packaging/.
> For more packages and binaries, see the [jc packaging](https://kellyjonbrazil.github.io/jc-packaging/) site.
## Usage
`jc` accepts piped input from `STDIN` and outputs a JSON representation of the previous command's output to `STDOUT`.
@@ -115,8 +117,6 @@ jc [OPTIONS] COMMAND
```
The JSON output can be compact (default) or pretty formatted with the `-p` option.
> Note: For best results set the `LANG` locale environment variable to `C`. For example, either by setting directly on the command-line: `$ LANG=C date | jc --date`, or by exporting to the environment before running commands: `$ export LANG=C`.
### Parsers
- `--acpi` enables the `acpi` command parser ([documentation](https://kellyjonbrazil.github.io/jc/docs/parsers/acpi))
@@ -182,6 +182,8 @@ The JSON output can be compact (default) or pretty formatted with the `-p` optio
- `--timedatectl` enables the `timedatectl status` command parser ([documentation](https://kellyjonbrazil.github.io/jc/docs/parsers/timedatectl))
- `--tracepath` enables the `tracepath` and `tracepath6` command parser ([documentation](https://kellyjonbrazil.github.io/jc/docs/parsers/tracepath))
- `--traceroute` enables the `traceroute` and `traceroute6` command parser ([documentation](https://kellyjonbrazil.github.io/jc/docs/parsers/traceroute))
- `--ufw` enables the `ufw status` command parser ([documentation](https://kellyjonbrazil.github.io/jc/docs/parsers/ufw))
- `--ufw-appinfo` enables the `ufw app info [application]` command parser ([documentation](https://kellyjonbrazil.github.io/jc/docs/parsers/ufw_appinfo))
- `--uname` enables the `uname -a` command parser ([documentation](https://kellyjonbrazil.github.io/jc/docs/parsers/uname))
- `--upower` enables the `upower` command parser ([documentation](https://kellyjonbrazil.github.io/jc/docs/parsers/upower))
- `--uptime` enables the `uptime` command parser ([documentation](https://kellyjonbrazil.github.io/jc/docs/parsers/uptime))
@@ -201,6 +203,19 @@ The JSON output can be compact (default) or pretty formatted with the `-p` optio
- `-r` raw output. Provides a more literal JSON output, typically with string values and no additional semantic processing
- `-v` version information
### Exit Codes
Any fatal errors within `jc` will generate an exit code of `100`, otherwise the exit code will be `0`. When using the "magic" syntax (e.g. `jc ifconfig eth0`), `jc` will store the exit code of the program being parsed and add it to the `jc` exit code. This way it is easier to determine if an error was from the parsed program or `jc`.
Consider the following examples using `ifconfig`:
| `ifconfig` exit code | `jc` exit code | Combined exit code | Interpretation |
|----------------------|----------------|--------------------|------------------------------------|
| `0` | `0` | `0` | No errors |
| `1` | `0` | `1` | Error in `ifconfig` |
| `0` | `100` | `100` | Error in `jc` |
| `1` | `100` | `101` | Error in both `ifconfig` and `jc` |
### Setting Custom Colors via Environment Variable
You can specify custom colors via the `JC_COLORS` environment variable. The `JC_COLORS` environment variable takes four comma separated string values in the following format:
```bash
@@ -226,18 +241,41 @@ Custom local parser plugins may be placed in a `jc/jcparsers` folder in your loc
Local parser plugins are standard python module files. Use the [`jc/parsers/foo.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/foo.py) parser as a template and simply place a `.py` file in the `jcparsers` subfolder.
Local plugin filenames must be valid python module names, therefore must consist entirely of alphanumerics and start with a letter. Local plugins may override default plugins.
Local plugin filenames must be valid python module names, therefore must consist entirely of alphanumerics and start with a letter. Local plugins may override default parsers.
> Note: The application data directory follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
### Caveats
**Locale:**
For best results set the `LANG` locale environment variable to `C` or `en_US.UTF-8`. For example, either by setting directly on the command-line:
```
$ LANG=C date | jc --date
```
or by exporting to the environment before running commands:
```
$ export LANG=C
```
**Timezones:**
Some parsers have calculated epoch timestamp fields added to the output. Unless a timestamp field name has a `_utc` suffix it is considered naive. (i.e. based on the local timezone of the system the `jc` parser was run on).
If a UTC timezone can be detected in the text of the command output, the timestamp will be timezone aware and have a `_utc` suffix on the key name. (e.g. `epoch_utc`) No other timezones are supported for aware timestamps.
## Compatibility
Some parsers like `ls`, `ps`, `dig`, etc. will work on any platform. Other parsers that are platform-specific will generate a warning message if they are used on an unsupported platform. To see all parser information, including compatibility, run `jc -ap`.
Some parsers like `dig`, `xml`, `csv`, etc. will work on any platform. Other parsers that convert platform-specific output will generate a warning message if they are run on an unsupported platform. To see all parser information, including compatibility, run `jc -ap`.
You may still use a parser on an unsupported platform - for example, you may want to parse a file with linux `lsof` output on an macOS laptop. In that case you can suppress the warning message with the `-q` cli option or the `quiet=True` function parameter in `parse()`:
You may still use a parser on an unsupported platform - for example, you may want to parse a file with linux `lsof` output on an macOS or Windows laptop. In that case you can suppress the warning message with the `-q` cli option or the `quiet=True` function parameter in `parse()`:
macOS:
```bash
cat lsof.out | jc --lsof -q
```
or Windows:
```bash
type lsof.out | jc --lsof -q
```
Tested on:
- Centos 7.7
@@ -249,6 +287,8 @@ Tested on:
- NixOS
- FreeBSD12
- Windows 10
- Windows 2016 Server
- Windows 2019 Server
## Contributions
Feel free to add/improve code or parsers! You can use the [`jc/parsers/foo.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/foo.py) parser as a template and submit your parser with a pull request.

View File

@@ -3,6 +3,10 @@
# jc.parsers.dig
jc - JSON CLI output utility `dig` command output parser
Options supported:
- `+noall +answer` options are supported in cases where only the answer information is desired.
- `+axfr` option is supported on its own
The `when_epoch` calculated timestamp field is naive (i.e. based on the local time of the system the parser is run on)
The `when_epoch_utc` calculated timestamp field is timezone-aware and is only available if the timezone field is UTC.
@@ -274,6 +278,42 @@ Examples:
}
]
$ dig +noall +answer cnn.com | jc --dig -p
[
{
"answer": [
{
"name": "cnn.com.",
"class": "IN",
"type": "A",
"ttl": 60,
"data": "151.101.193.67"
},
{
"name": "cnn.com.",
"class": "IN",
"type": "A",
"ttl": 60,
"data": "151.101.65.67"
},
{
"name": "cnn.com.",
"class": "IN",
"type": "A",
"ttl": 60,
"data": "151.101.1.67"
},
{
"name": "cnn.com.",
"class": "IN",
"type": "A",
"ttl": 60,
"data": "151.101.129.67"
}
]
}
]
## info
```python
@@ -299,6 +339,6 @@ Returns:
List of Dictionaries. Raw or processed structured data.
## Parser Information
Compatibility: linux, aix, freebsd, darwin
Compatibility: linux, aix, freebsd, darwin, win32, cygwin
Version 2.0 by Kelly Brazil (kellyjonbrazil@gmail.com)
Version 2.1 by Kelly Brazil (kellyjonbrazil@gmail.com)

View File

@@ -9,16 +9,14 @@ Options supported:
- `/C, /-C`
- `/S`
The "Magic" syntax is not supported since the `dir` command is a shell builtin.
The `epoch` calculated timestamp field is naive (i.e. based on the local time of the system the parser is run on)
Usage (cli):
C:> dir | jc --dir
or
C:> jc dir
Usage (module):
import jc.parsers.dir
@@ -145,4 +143,4 @@ Returns:
## Parser Information
Compatibility: win32
Version 1.2 by Rasheed Elsaleh (rasheed@rebelliondefense.com)
Version 1.3 by Rasheed Elsaleh (rasheed@rebelliondefense.com)

View File

@@ -5,6 +5,8 @@ jc - JSON CLI output utility `history` command output parser
This parser will output a list of dictionaries each containing `line` and `command` keys. If you would like a simple dictionary output, then use the `-r` command-line option or the `raw=True` argument in the `parse()` function.
The "Magic" syntax is not supported since the `history` command is a shell builtin.
Usage (cli):
$ history | jc --history

View File

@@ -5,14 +5,12 @@ jc - JSON CLI output utility `jobs` command output parser
Also supports the `-l` option.
The "Magic" syntax is not supported since the `jobs` command is a shell builtin.
Usage (cli):
$ jobs | jc --jobs
or
$ jc jobs
Usage (module):
import jc.parsers.jobs

View File

@@ -38,14 +38,26 @@ Schema:
"round_trip_ms_stddev": float,
"responses": [
{
"type": string, # ('reply' or 'timeout')
"type": string, # 'reply', 'timeout', 'unparsable_line', etc. See `_error_type.type_map` for all options
"unparsed_line": string, # only if an 'unparsable_line' type
"timestamp": float,
"bytes": integer,
"response_ip": string,
"icmp_seq": integer,
"ttl": integer,
"time_ms": float,
"duplicate": boolean
"duplicate": boolean,
"vr": integer, # hex value converted to decimal
"hl": integer, # hex value converted to decimal
"tos": integer, # hex value converted to decimal
"len": integer, # hex value converted to decimal
"id": integer, # hex value converted to decimal
"flg": integer, # hex value converted to decimal
"off": integer, # hex value converted to decimal
"pro": integer, # hex value converted to decimal
"cks": ingeger, # hex value converted to decimal
"src": string,
"dst": string
}
]
}
@@ -97,7 +109,6 @@ Examples:
]
}
$ ping -c 3 -p ff cnn.com | jc --ping -p -r
{
"destination_ip": "151.101.129.67",
@@ -170,4 +181,4 @@ Returns:
## Parser Information
Compatibility: linux, darwin, freebsd
Version 1.4 by Kelly Brazil (kellyjonbrazil@gmail.com)
Version 1.5 by Kelly Brazil (kellyjonbrazil@gmail.com)

225
docs/parsers/ufw.md Normal file
View File

@@ -0,0 +1,225 @@
[Home](https://kellyjonbrazil.github.io/jc/)
# jc.parsers.ufw
jc - JSON CLI output utility `ufw status` command output parser
Usage (cli):
$ ufw status | jc --ufw
or
$ jc ufw status
Usage (module):
import jc.parsers.ufw
result = jc.parsers.ufw.parse(ufw_command_output)
Schema:
{
"status": string,
"logging": string,
"logging_level": string,
"default": string,
"new_profiles": string,
"rules": [
{
"action": string,
"action_direction": string, # null if blank
"index": integer, # null if blank
"network_protocol": string,
"to_ip": string,
"to_ip_prefix": integer,
"to_interface": string,
"to_transport": string,
"to_ports": [
integer
],
"to_port_ranges": [
{
"start": integer,
"end": integer
}
],
"to_service": string, # null if any to ports or port_ranges are set
"from_ip": string,
"from_ip_prefix": integer,
"from_interface": string,
"from_transport": string,
"from_ports": [
integer
],
"from_port_ranges": [
{
"start": integer,
"end": integer
}
],
"from_service": string, # null if any from ports or port_ranges are set
"comment": string # null if no comment
}
]
}
Examples:
$ ufw status verbose | jc --ufw -p
{
"status": "active",
"logging": "on",
"logging_level": "low",
"default": "deny (incoming), allow (outgoing), disabled (routed)",
"new_profiles": "skip",
"rules": [
{
"action": "ALLOW",
"action_direction": "IN",
"index": null,
"network_protocol": "ipv4",
"to_interface": "any",
"to_transport": "any",
"to_service": null,
"to_ports": [
22
],
"to_ip": "0.0.0.0",
"to_ip_prefix": 0,
"comment": null,
"from_ip": "0.0.0.0",
"from_ip_prefix": 0,
"from_interface": "any",
"from_transport": "any",
"from_port_ranges": [
{
"start": 0,
"end": 65535
}
],
"from_service": null
},
{
"action": "ALLOW",
"action_direction": "IN",
"index": null,
"network_protocol": "ipv4",
"to_interface": "any",
"to_transport": "tcp",
"to_service": null,
"to_ports": [
80,
443
],
"to_ip": "0.0.0.0",
"to_ip_prefix": 0,
"comment": null,
"from_ip": "0.0.0.0",
"from_ip_prefix": 0,
"from_interface": "any",
"from_transport": "any",
"from_port_ranges": [
{
"start": 0,
"end": 65535
}
],
"from_service": null
},
...
]
}
$ ufw status verbose | jc --ufw -p -r
{
"status": "active",
"logging": "on",
"logging_level": "low",
"default": "deny (incoming), allow (outgoing), disabled (routed)",
"new_profiles": "skip",
"rules": [
{
"action": "ALLOW",
"action_direction": "IN",
"index": null,
"network_protocol": "ipv4",
"to_interface": "any",
"to_transport": "any",
"to_service": null,
"to_ports": [
"22"
],
"to_ip": "0.0.0.0",
"to_ip_prefix": "0",
"comment": null,
"from_ip": "0.0.0.0",
"from_ip_prefix": "0",
"from_interface": "any",
"from_transport": "any",
"from_port_ranges": [
{
"start": "0",
"end": "65535"
}
],
"from_service": null
},
{
"action": "ALLOW",
"action_direction": "IN",
"index": null,
"network_protocol": "ipv4",
"to_interface": "any",
"to_transport": "tcp",
"to_service": null,
"to_ports": [
"80",
"443"
],
"to_ip": "0.0.0.0",
"to_ip_prefix": "0",
"comment": null,
"from_ip": "0.0.0.0",
"from_ip_prefix": "0",
"from_interface": "any",
"from_transport": "any",
"from_port_ranges": [
{
"start": "0",
"end": "65535"
}
],
"from_service": null
},
...
]
}
## info
```python
info()
```
Provides parser metadata (version, author, etc.)
## parse
```python
parse(data, raw=False, quiet=False)
```
Main text parsing function
Parameters:
data: (string) text data to parse
raw: (boolean) output preprocessed JSON if True
quiet: (boolean) suppress warning messages if True
Returns:
Dictionary. Raw or processed structured data.
## Parser Information
Compatibility: linux
Version 1.0 by Kelly Brazil (kellyjonbrazil@gmail.com)

158
docs/parsers/ufw_appinfo.md Normal file
View File

@@ -0,0 +1,158 @@
[Home](https://kellyjonbrazil.github.io/jc/)
# jc.parsers.ufw_appinfo
jc - JSON CLI output utility `ufw app info [application]` command output parser
Supports individual apps via `ufw app info [application]` and all apps list via `ufw app info all`.
Because `ufw` application definitions allow overlapping ports and port ranges, this parser preserves that behavior, but also provides `normalized` lists and ranges that remove duplicate ports and merge overlapping ranges.
Usage (cli):
$ ufw app info OpenSSH | jc --ufw-appinfo
or
$ jc ufw app info OpenSSH
Usage (module):
import jc.parsers.ufw_appinfo
result = jc.parsers.ufw_appinfo.parse(ufw_appinfo_command_output)
Schema:
[
{
"profile": string,
"title": string,
"description": string,
"tcp_list": [
integer
],
"tcp_ranges": [
{
"start": integer, # 'any' is converted to start/end: 0/65535
"end": integer
}
],
"udp_list": [
integer
],
"udp_ranges": [
{
"start": integer, # 'any' is converted to start/end: 0/65535
"end": integer
}
],
"normalized_tcp_list": [
integers # duplicates and overlapping are removed
],
"normalized_tcp_ranges": [
{
"start": integer, # 'any' is converted to start/end: 0/65535
"end": integers # overlapping are merged
}
],
"normalized_udp_list": [
integers # duplicates and overlapping are removed
],
"normalized_udp_ranges": [
{
"start": integer, # 'any' is converted to start/end: 0/65535
"end": integers # overlapping are merged
}
]
}
]
Examples:
$ ufw app info MSN | jc --ufw-appinfo -p
[
{
"profile": "MSN",
"title": "MSN Chat",
"description": "MSN chat protocol (with file transfer and voice)",
"tcp_list": [
1863,
6901
],
"udp_list": [
1863,
6901
],
"tcp_ranges": [
{
"start": 6891,
"end": 6900
}
],
"normalized_tcp_list": [
1863,
6901
],
"normalized_tcp_ranges": [
{
"start": 6891,
"end": 6900
}
],
"normalized_udp_list": [
1863,
6901
]
}
]
$ ufw app info MSN | jc --ufw-appinfo -p -r
[
{
"profile": "MSN",
"title": "MSN Chat",
"description": "MSN chat protocol (with file transfer and voice)",
"tcp_list": [
"1863",
"6901"
],
"udp_list": [
"1863",
"6901"
],
"tcp_ranges": [
{
"start": "6891",
"end": "6900"
}
]
}
]
## info
```python
info()
```
Provides parser metadata (version, author, etc.)
## parse
```python
parse(data, raw=False, quiet=False)
```
Main text parsing function
Parameters:
data: (string) text data to parse
raw: (boolean) output preprocessed JSON if True
quiet: (boolean) suppress warning messages if True
Returns:
List of Dictionaries. Raw or processed structured data.
## Parser Information
Compatibility: linux
Version 1.0 by Kelly Brazil (kellyjonbrazil@gmail.com)

View File

@@ -95,4 +95,4 @@ Returns:
## Parser Information
Compatibility: linux, darwin, cygwin, win32, aix, freebsd
Version 1.4 by Kelly Brazil (kellyjonbrazil@gmail.com)
Version 1.5 by Kelly Brazil (kellyjonbrazil@gmail.com)

View File

@@ -109,4 +109,4 @@ Returns:
## Parser Information
Compatibility: linux, darwin, cygwin, win32, aix, freebsd
Version 1.4 by Kelly Brazil (kellyjonbrazil@gmail.com)
Version 1.5 by Kelly Brazil (kellyjonbrazil@gmail.com)

View File

@@ -74,7 +74,7 @@ Returns:
convert_to_int(value)
```
Converts string input to integer by stripping all non-numeric characters
Converts string and float input to int. Strips all non-numeric characters from strings.
Parameters:
@@ -90,7 +90,7 @@ Returns:
convert_to_float(value)
```
Converts string input to float by stripping all non-numeric characters
Converts string and int input to float. Strips all non-numeric characters from strings.
Parameters:

View File

@@ -62,15 +62,15 @@ Module Example:
... ;; Got answer:
... ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 64612
... ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
...
...
... ;; OPT PSEUDOSECTION:
... ; EDNS: version: 0, flags:; udp: 4096
... ;; QUESTION SECTION:
... ;example.com. IN A
...
...
... ;; ANSWER SECTION:
... example.com. 29658 IN A 93.184.216.34
...
...
... ;; Query time: 52 msec
... ;; SERVER: 2600:1700:bab0:d40::1#53(2600:1700:bab0:d40::1)
... ;; WHEN: Fri Apr 16 16:13:00 PDT 2021
@@ -86,4 +86,4 @@ Module Example:
"""
name = 'jc'
__version__ = '1.15.2'
__version__ = '1.15.5'

249
jc/cli.py
View File

@@ -10,9 +10,14 @@ import shlex
import importlib
import textwrap
import signal
import subprocess
import json
import jc
import jc.appdirs as appdirs
import jc.utils
import jc.tracebackplus
from jc.exceptions import LibraryNotInstalled, ParseError
# make pygments import optional
try:
import pygments
@@ -102,6 +107,8 @@ parsers = [
'timedatectl',
'tracepath',
'traceroute',
'ufw',
'ufw-appinfo',
'uname',
'upower',
'uptime',
@@ -112,6 +119,9 @@ parsers = [
'yaml'
]
JC_ERROR_EXIT = 100
# List of custom or override parsers.
# Allow any <user_data_dir>/jc/jcparsers/*.py
local_parsers = []
@@ -184,10 +194,9 @@ def set_env_colors(env_colors=None):
Default colors:
JC_COLORS=blue,brightblack,magenta,green
JC_COLORS=blue,brightblack,magenta,green
or
JC_COLORS=default,default,default,default
JC_COLORS=default,default,default,default
"""
input_error = False
@@ -205,7 +214,7 @@ def set_env_colors(env_colors=None):
# if there is an issue with the env variable, just set all colors to default and move on
if input_error:
jc.utils.warning_message('could not parse JC_COLORS environment variable')
jc.utils.warning_message('Could not parse JC_COLORS environment variable')
color_list = ['default', 'default', 'default', 'default']
# Try the color set in the JC_COLORS env variable first. If it is set to default, then fall back to default colors
@@ -219,15 +228,12 @@ def set_env_colors(env_colors=None):
def piped_output():
"""Return False if stdout is a TTY. True if output is being piped to another program"""
if sys.stdout.isatty():
return False
else:
return True
return False if sys.stdout.isatty() else True
def ctrlc(signum, frame):
"""Exit with error on SIGINT"""
sys.exit(1)
sys.exit(JC_ERROR_EXIT)
def parser_shortname(parser_argument):
@@ -375,33 +381,38 @@ def versiontext():
def json_out(data, pretty=False, env_colors=None, mono=False, piped_out=False):
"""Return a JSON formatted string. String may include color codes or be pretty printed."""
separators = (',', ':')
indent = None
if pretty:
separators = None
indent = 2
if not mono and not piped_out:
# set colors
class JcStyle(Style):
styles = set_env_colors(env_colors)
if pretty:
return str(highlight(json.dumps(data, indent=2, ensure_ascii=False),
JsonLexer(), Terminal256Formatter(style=JcStyle))[0:-1])
else:
return str(highlight(json.dumps(data, separators=(',', ':'), ensure_ascii=False),
JsonLexer(), Terminal256Formatter(style=JcStyle))[0:-1])
return str(highlight(json.dumps(data, indent=indent, separators=separators, ensure_ascii=False),
JsonLexer(), Terminal256Formatter(style=JcStyle))[0:-1])
else:
if pretty:
return json.dumps(data, indent=2, ensure_ascii=False)
else:
return json.dumps(data, separators=(',', ':'), ensure_ascii=False)
return json.dumps(data, indent=indent, separators=separators, ensure_ascii=False)
def generate_magic_command(args):
"""
Return a tuple with a boolean and a command, where the boolean signifies that
the command is valid, and the command is either a command string or None.
def magic_parser(args):
"""
Parse command arguments for magic syntax: jc -p ls -al
# Parse with magic syntax: jc -p ls -al
Return a tuple:
valid_command (bool) is this a valid command? (exists in magic dict)
run_command (list) list of the user's command to run. None if no command.
jc_parser (str) parser to use for this user's command.
jc_options (list) list of jc options
"""
# bail immediately if there are no args or a parser is defined
if len(args) <= 1 or args[1].startswith('--'):
return False, None
return False, None, None, []
# correctly parse escape characters and spaces with shlex
args_given = ' '.join(map(shlex.quote, args[1:])).split()
@@ -411,7 +422,7 @@ def generate_magic_command(args):
for arg in list(args_given):
# parser found - use standard syntax
if arg.startswith('--'):
return False, None
return False, None, None, []
# option found - populate option list
elif arg.startswith('-'):
@@ -423,11 +434,11 @@ def generate_magic_command(args):
# if -h, -a, or -v found in options, then bail out
if 'h' in options or 'a' in options or 'v' in options:
return False, None
return False, None, None, []
# all options popped and no command found - for case like 'jc -a'
# all options popped and no command found - for case like 'jc -x'
if len(args_given) == 0:
return False, None
return False, None, None, []
magic_dict = {}
parser_info = about_jc()['parsers']
@@ -444,31 +455,34 @@ def generate_magic_command(args):
# try to get a parser for two_word_command, otherwise get one for one_word_command
found_parser = magic_dict.get(two_word_command, magic_dict.get(one_word_command))
# construct a new command line using the standard syntax: COMMAND | jc --PARSER -OPTIONS
run_command = ' '.join(args_given)
if found_parser:
cmd_options = ('-' + ''.join(options)) if options else ''
return True, ' '.join([run_command, '|', 'jc', found_parser, cmd_options])
else:
return False, run_command
return (
True if found_parser else False, # was a suitable parser found?
args_given, # run_command
found_parser, # the parser selected
options # jc options to preserve
)
def magic():
"""Runs the command generated by generate_magic_command() to support magic syntax"""
valid_command, run_command = generate_magic_command(sys.argv)
if valid_command:
os.system(run_command)
sys.exit(0)
elif run_command is None:
return
else:
jc.utils.error_message(f'parser not found for "{run_command}". Use "jc -h" for help.')
sys.exit(1)
def run_user_command(command):
"""Use subprocess to run the user's command. Returns the STDOUT, STDERR, and the Exit Code as a tuple."""
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
stdout, stderr = proc.communicate()
return (
stdout or '\n',
stderr,
proc.returncode
)
def combined_exit_code(program_exit=0, jc_exit=0):
exit_code = program_exit + jc_exit
if exit_code > 255:
exit_code = 255
return exit_code
def main():
import jc.utils
# break on ctrl-c keyboard interrupt
signal.signal(signal.SIGINT, ctrlc)
@@ -478,17 +492,26 @@ def main():
except AttributeError:
pass
# try magic syntax first: e.g. jc -p ls -al
magic()
# enable colors for Windows cmd.exe terminal
if sys.platform.startswith('win32'):
os.system('')
# parse magic syntax first: e.g. jc -p ls -al
magic_options = []
valid_command, run_command, magic_found_parser, magic_options = magic_parser(sys.argv)
# set colors
jc_colors = os.getenv('JC_COLORS')
# set options
options = []
options.extend(magic_options)
# options
for opt in sys.argv:
if opt.startswith('-') and not opt.startswith('--'):
options.extend(opt[1:])
# find options if magic_parser did not find a command
if not valid_command:
for opt in sys.argv:
if opt.startswith('-') and not opt.startswith('--'):
options.extend(opt[1:])
about = 'a' in options
debug = 'd' in options
@@ -500,6 +523,9 @@ def main():
raw = 'r' in options
version_info = 'v' in options
if verbose_debug:
jc.tracebackplus.enable(context=11)
if not pygments_installed:
mono = True
@@ -515,44 +541,101 @@ def main():
print(versiontext())
sys.exit(0)
if verbose_debug:
import jc.tracebackplus
jc.tracebackplus.enable(context=11)
# if magic syntax used, try to run the command and error if it's not found, etc.
magic_stdout, magic_stderr, magic_exit_code = None, None, 0
if run_command:
run_command_str = ' '.join(run_command)
if sys.stdin.isatty():
jc.utils.error_message('Missing piped data. Use "jc -h" for help.')
sys.exit(1)
if valid_command:
try:
magic_stdout, magic_stderr, magic_exit_code = run_user_command(run_command)
if magic_stderr:
print(magic_stderr[:-1], file=sys.stderr)
data = sys.stdin.read()
except FileNotFoundError:
if debug:
raise
else:
jc.utils.error_message(f'"{run_command_str}" command could not be found. For details use the -d or -dd option.')
sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT))
found = False
except Exception:
if debug:
raise
else:
jc.utils.error_message(f'"{run_command_str}" command could not be run. For details use the -d or -dd option.')
sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT))
for arg in sys.argv:
parser_name = parser_shortname(arg)
elif run_command is not None:
jc.utils.error_message(f'"{run_command_str}" cannot be used with Magic syntax. Use "jc -h" for help.')
sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT))
if parser_name in parsers:
# load parser module just in time so we don't need to load all modules
parser = parser_module(arg)
try:
result = parser.parse(data, raw=raw, quiet=quiet)
# find the correct parser
if magic_found_parser:
parser = parser_module(magic_found_parser)
parser_name = parser_shortname(magic_found_parser)
else:
found = False
for arg in sys.argv:
parser_name = parser_shortname(arg)
if parser_name in parsers:
parser = parser_module(arg)
found = True
break
except Exception:
if debug:
raise
else:
import jc.utils
jc.utils.error_message(
f'{parser_name} parser could not parse the input data. Did you use the correct parser?\n'
' For details use the -d or -dd option. Use "jc -h" for help.')
sys.exit(1)
if not found:
jc.utils.error_message('Missing or incorrect arguments. Use "jc -h" for help.')
sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT))
if not found:
jc.utils.error_message('Missing or incorrect arguments. Use "jc -h" for help.')
sys.exit(1)
# check for input errors (pipe vs magic)
if not sys.stdin.isatty() and magic_stdout:
jc.utils.error_message('Piped data and Magic syntax used simultaneously. Use "jc -h" for help.')
sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT))
print(json_out(result, pretty=pretty, env_colors=jc_colors, mono=mono, piped_out=piped_output()))
elif sys.stdin.isatty() and magic_stdout is None:
jc.utils.error_message('Missing piped data. Use "jc -h" for help.')
sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT))
# parse the data
data = magic_stdout or sys.stdin.read()
try:
result = parser.parse(data, raw=raw, quiet=quiet)
except (ParseError, LibraryNotInstalled) as e:
if debug:
raise
else:
jc.utils.error_message(
f'Parser issue with {parser_name}:\n'
f' {e}\n'
' For details use the -d or -dd option. Use "jc -h" for help.')
sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT))
except Exception:
if debug:
raise
else:
jc.utils.error_message(
f'{parser_name} parser could not parse the input data. Did you use the correct parser?\n'
' For details use the -d or -dd option. Use "jc -h" for help.')
sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT))
# output the json
try:
print(json_out(result, pretty=pretty, env_colors=jc_colors, mono=mono, piped_out=piped_output()))
sys.exit(combined_exit_code(magic_exit_code, 0))
except Exception:
if debug:
raise
else:
jc.utils.error_message(
'There was an issue generating the JSON output.\n'
' For details use the -d or -dd option.')
sys.exit(combined_exit_code(magic_exit_code, JC_ERROR_EXIT))
if __name__ == '__main__':

9
jc/exceptions.py Normal file
View File

@@ -0,0 +1,9 @@
"""jc - JSON CLI output utility exceptions"""
class ParseError(Exception):
pass
class LibraryNotInstalled(Exception):
pass

Binary file not shown.

View File

@@ -1,5 +1,9 @@
"""jc - JSON CLI output utility `dig` command output parser
Options supported:
- `+noall +answer` options are supported in cases where only the answer information is desired.
- `+axfr` option is supported on its own
The `when_epoch` calculated timestamp field is naive (i.e. based on the local time of the system the parser is run on)
The `when_epoch_utc` calculated timestamp field is timezone-aware and is only available if the timezone field is UTC.
@@ -270,19 +274,55 @@ Examples:
"rcvd": "78"
}
]
$ dig +noall +answer cnn.com | jc --dig -p
[
{
"answer": [
{
"name": "cnn.com.",
"class": "IN",
"type": "A",
"ttl": 60,
"data": "151.101.193.67"
},
{
"name": "cnn.com.",
"class": "IN",
"type": "A",
"ttl": 60,
"data": "151.101.65.67"
},
{
"name": "cnn.com.",
"class": "IN",
"type": "A",
"ttl": 60,
"data": "151.101.1.67"
},
{
"name": "cnn.com.",
"class": "IN",
"type": "A",
"ttl": 60,
"data": "151.101.129.67"
}
]
}
]
"""
import jc.utils
class info():
"""Provides parser metadata (version, author, etc.)"""
version = '2.0'
version = '2.1'
description = '`dig` command parser'
author = 'Kelly Brazil'
author_email = 'kellyjonbrazil@gmail.com'
# compatible options: linux, darwin, cygwin, win32, aix, freebsd
compatible = ['linux', 'aix', 'freebsd', 'darwin']
compatible = ['linux', 'aix', 'freebsd', 'darwin', 'win32', 'cygwin']
magic_commands = ['dig']
@@ -500,6 +540,7 @@ def parse(data, raw=False, quiet=False):
# section can be: header, flags, question, authority, answer, axfr, additional, opt_pseudosection, footer
section = ''
output_entry = {}
answer_list = []
if jc.utils.has_data(data):
for line in cleandata:
@@ -581,7 +622,12 @@ def parse(data, raw=False, quiet=False):
output_entry.update({'authority': authority_list})
continue
if not line.startswith(';') and section == 'answer':
# https://github.com/kellyjonbrazil/jc/issues/133
# to allow parsing of output that only has the answer section - e.g:
# dig +noall +answer example.com
# we allow section to be 'answer' (normal output) or
# '', which means +noall +answer was used.
if not line.startswith(';') and (section == 'answer' or section == ''):
answer_list.append(_parse_answer(line))
output_entry.update({'answer': answer_list})
continue

View File

@@ -6,16 +6,14 @@ Options supported:
- `/C, /-C`
- `/S`
The "Magic" syntax is not supported since the `dir` command is a shell builtin.
The `epoch` calculated timestamp field is naive (i.e. based on the local time of the system the parser is run on)
Usage (cli):
C:> dir | jc --dir
or
C:> jc dir
Usage (module):
import jc.parsers.dir
@@ -121,14 +119,13 @@ import jc.utils
class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.2'
version = '1.3'
description = '`dir` command parser'
author = 'Rasheed Elsaleh'
author_email = 'rasheed@rebelliondefense.com'
# compatible options: win32
compatible = ['win32']
magic_commands = ['dir']
__version__ = info.version

View File

@@ -2,6 +2,8 @@
This parser will output a list of dictionaries each containing `line` and `command` keys. If you would like a simple dictionary output, then use the `-r` command-line option or the `raw=True` argument in the `parse()` function.
The "Magic" syntax is not supported since the `history` command is a shell builtin.
Usage (cli):
$ history | jc --history

View File

@@ -2,14 +2,12 @@
Also supports the `-l` option.
The "Magic" syntax is not supported since the `jobs` command is a shell builtin.
Usage (cli):
$ jobs | jc --jobs
or
$ jc jobs
Usage (module):
import jc.parsers.jobs

View File

@@ -35,14 +35,26 @@ Schema:
"round_trip_ms_stddev": float,
"responses": [
{
"type": string, # ('reply' or 'timeout')
"type": string, # 'reply', 'timeout', 'unparsable_line', etc. See `_error_type.type_map` for all options
"unparsed_line": string, # only if an 'unparsable_line' type
"timestamp": float,
"bytes": integer,
"response_ip": string,
"icmp_seq": integer,
"ttl": integer,
"time_ms": float,
"duplicate": boolean
"duplicate": boolean,
"vr": integer, # hex value converted to decimal
"hl": integer, # hex value converted to decimal
"tos": integer, # hex value converted to decimal
"len": integer, # hex value converted to decimal
"id": integer, # hex value converted to decimal
"flg": integer, # hex value converted to decimal
"off": integer, # hex value converted to decimal
"pro": integer, # hex value converted to decimal
"cks": ingeger, # hex value converted to decimal
"src": string,
"dst": string
}
]
}
@@ -94,7 +106,6 @@ Examples:
]
}
$ ping -c 3 -p ff cnn.com | jc --ping -p -r
{
"destination_ip": "151.101.129.67",
@@ -141,12 +152,13 @@ Examples:
}
"""
import string
import ipaddress
import jc.utils
class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.4'
version = '1.5'
description = '`ping` and `ping6` command parser'
author = 'Kelly Brazil'
author_email = 'kellyjonbrazil@gmail.com'
@@ -171,7 +183,8 @@ def _process(proc_data):
Dictionary. Structured data to conform to the schema.
"""
int_list = ['data_bytes', 'packets_transmitted', 'packets_received', 'bytes', 'icmp_seq', 'ttl', 'duplicates']
int_list = ['data_bytes', 'packets_transmitted', 'packets_received', 'bytes', 'icmp_seq', 'ttl',
'duplicates', 'vr', 'hl', 'tos', 'len', 'id', 'flg', 'off', 'pro', 'cks']
float_list = ['packet_loss_percent', 'round_trip_ms_min', 'round_trip_ms_avg', 'round_trip_ms_max',
'round_trip_ms_stddev', 'timestamp', 'time_ms']
@@ -193,6 +206,54 @@ def _process(proc_data):
return proc_data
def _ipv6_in(line):
line_list = line.replace('(', ' ').replace(')', ' ').replace(',', ' ').replace('%', ' ').split()
ipv6 = False
for item in line_list:
try:
_ = ipaddress.IPv6Address(item)
ipv6 = True
except Exception:
pass
return ipv6
def _error_type(line):
# from https://github.com/dgibson/iputils/blob/master/ping.c
# https://android.googlesource.com/platform/external/ping/+/8fc3c91cf9e7f87bc20b9e6d3ea2982d87b70d9a/ping.c
# https://opensource.apple.com/source/network_cmds/network_cmds-328/ping.tproj/ping.c
type_map = {
'Destination Net Unreachable': 'destination_net_unreachable',
'Destination Host Unreachable': 'destination_host_unreachable',
'Destination Protocol Unreachable': 'destination_protocol_unreachable',
'Destination Port Unreachable': 'destination_port_unreachable',
'Frag needed and DF set': 'frag_needed_and_df_set',
'Source Route Failed': 'source_route_failed',
'Destination Net Unknown': 'destination_net_unknown',
'Destination Host Unknown': 'destination_host_unknown',
'Source Host Isolated': 'source_host_isolated',
'Destination Net Prohibited': 'destination_net_prohibited',
'Destination Host Prohibited': 'destination_host_prohibited',
'Destination Net Unreachable for Type of Service': 'destination_net_unreachable_for_type_of_service',
'Destination Host Unreachable for Type of Service': 'destination_host_unreachable_for_type_of_service',
'Packet filtered': 'packet_filtered',
'Precedence Violation': 'precedence_violation',
'Precedence Cutoff': 'precedence_cutoff',
'Dest Unreachable, Bad Code': 'dest_unreachable_bad_code',
'Redirect Network': 'redirect_network',
'Redirect Host': 'redirect_host',
'Redirect Type of Service and Network': 'redirect_type_of_service_and_network',
'Redirect, Bad Code': 'redirect_bad_code',
'Time to live exceeded': 'time_to_live_exceeded',
'Frag reassembly time exceeded': 'frag_reassembly_time_exceeded',
'Time exceeded, Bad Code': 'time_exceeded_bad_code'
}
for err_type, code in type_map.items():
if err_type in line:
return code
def _linux_parse(data):
raw_output = {}
ping_responses = []
@@ -303,36 +364,41 @@ def _linux_parse(data):
continue
# normal responses
else:
elif ' bytes from ' in line:
try:
line = line.replace('(', ' ').replace(')', ' ').replace('=', ' ')
line = line.replace('(', ' ').replace(')', ' ').replace('=', ' ')
# positions of items depend on whether ipv4/ipv6 and/or ip/hostname is used
if ipv4 and not hostname:
bts, rip, iseq, t2l, tms = (0, 3, 5, 7, 9)
elif ipv4 and hostname:
bts, rip, iseq, t2l, tms = (0, 4, 7, 9, 11)
elif not ipv4 and not hostname:
bts, rip, iseq, t2l, tms = (0, 3, 5, 7, 9)
elif not ipv4 and hostname:
bts, rip, iseq, t2l, tms = (0, 4, 7, 9, 11)
# positions of items depend on whether ipv4/ipv6 and/or ip/hostname is used
if ipv4 and not hostname:
bts, rip, iseq, t2l, tms = (0, 3, 5, 7, 9)
elif ipv4 and hostname:
bts, rip, iseq, t2l, tms = (0, 4, 7, 9, 11)
elif not ipv4 and not hostname:
bts, rip, iseq, t2l, tms = (0, 3, 5, 7, 9)
elif not ipv4 and hostname:
bts, rip, iseq, t2l, tms = (0, 4, 7, 9, 11)
# if timestamp option is specified, then shift everything right by one
timestamp = False
if line[0] == '[':
timestamp = True
bts, rip, iseq, t2l, tms = (bts + 1, rip + 1, iseq + 1, t2l + 1, tms + 1)
# if timestamp option is specified, then shift everything right by one
timestamp = False
if line[0] == '[':
timestamp = True
bts, rip, iseq, t2l, tms = (bts + 1, rip + 1, iseq + 1, t2l + 1, tms + 1)
response = {
'type': 'reply',
'timestamp': line.split()[0].lstrip('[').rstrip(']') if timestamp else None,
'bytes': line.split()[bts],
'response_ip': line.split()[rip].rstrip(':'),
'icmp_seq': line.split()[iseq],
'ttl': line.split()[t2l],
'time_ms': line.split()[tms],
'duplicate': True if 'DUP!' in line else False
}
response = {
'type': 'reply',
'timestamp': line.split()[0].lstrip('[').rstrip(']') if timestamp else None,
'bytes': line.split()[bts],
'response_ip': line.split()[rip].rstrip(':'),
'icmp_seq': line.split()[iseq],
'ttl': line.split()[t2l],
'time_ms': line.split()[tms],
'duplicate': True if 'DUP!' in line else False
}
except Exception:
response = {
'type': 'unparsable_line',
'unparsed_line': line
}
ping_responses.append(response)
continue
@@ -347,6 +413,7 @@ def _bsd_parse(data):
ping_responses = []
pattern = None
footer = False
ping_error = False
linedata = data.splitlines()
@@ -420,7 +487,7 @@ def _bsd_parse(data):
# ping response lines
else:
# ipv4 lines
if ',' not in line:
if not _ipv6_in(line):
# request timeout
if line.startswith('Request timeout for '):
@@ -431,9 +498,80 @@ def _bsd_parse(data):
ping_responses.append(response)
continue
# catch error responses
err = _error_type(line)
if err:
response = {
'type': err
}
try:
response['bytes'] = line.split()[0]
response['response_ip'] = line.split()[4].strip(':').strip('(').strip(')')
except Exception:
pass
ping_error = True
continue
if ping_error:
if line.startswith('Vr'):
continue
else:
error_line = line.split()
try:
response.update(
{
'vr': int(error_line[0], 16), # convert from hex to decimal
'hl': int(error_line[1], 16),
'tos': int(error_line[2], 16),
'len': int(error_line[3], 16),
'id': int(error_line[4], 16),
'flg': int(error_line[5], 16),
'off': int(error_line[6], 16),
'ttl': int(error_line[7], 16),
'pro': int(error_line[8], 16),
'cks': int(error_line[9], 16),
'src': error_line[10],
'dst': error_line[11],
}
)
except Exception:
pass
if response:
ping_responses.append(response)
ping_error = False
continue
# normal response
else:
line = line.replace(':', ' ').replace('=', ' ')
elif ' bytes from ' in line:
try:
line = line.replace(':', ' ').replace('=', ' ')
response = {
'type': 'reply',
'bytes': line.split()[0],
'response_ip': line.split()[3],
'icmp_seq': line.split()[5],
'ttl': line.split()[7],
'time_ms': line.split()[9]
}
except Exception:
response = {
'type': 'unparsable_line',
'unparsed_line': line
}
ping_responses.append(response)
continue
# ipv6 lines
elif ' bytes from ' in line:
try:
line = line.replace(',', ' ').replace('=', ' ')
response = {
'type': 'reply',
'bytes': line.split()[0],
@@ -442,20 +580,12 @@ def _bsd_parse(data):
'ttl': line.split()[7],
'time_ms': line.split()[9]
}
ping_responses.append(response)
continue
except Exception:
response = {
'type': 'unparsable_line',
'unparsed_line': line
}
# ipv6 lines
else:
line = line.replace(',', ' ').replace('=', ' ')
response = {
'type': 'reply',
'bytes': line.split()[0],
'response_ip': line.split()[3],
'icmp_seq': line.split()[5],
'ttl': line.split()[7],
'time_ms': line.split()[9]
}
ping_responses.append(response)
continue
@@ -463,8 +593,9 @@ def _bsd_parse(data):
if ping_responses:
seq_list = []
for reply in ping_responses:
seq_list.append(reply['icmp_seq'])
reply['duplicate'] = True if seq_list.count(reply['icmp_seq']) > 1 else False
if 'icmp_seq' in reply:
seq_list.append(reply['icmp_seq'])
reply['duplicate'] = True if seq_list.count(reply['icmp_seq']) > 1 else False
raw_output['responses'] = ping_responses

466
jc/parsers/ufw.py Normal file
View File

@@ -0,0 +1,466 @@
"""jc - JSON CLI output utility `ufw status` command output parser
Usage (cli):
$ ufw status | jc --ufw
or
$ jc ufw status
Usage (module):
import jc.parsers.ufw
result = jc.parsers.ufw.parse(ufw_command_output)
Schema:
{
"status": string,
"logging": string,
"logging_level": string,
"default": string,
"new_profiles": string,
"rules": [
{
"action": string,
"action_direction": string, # null if blank
"index": integer, # null if blank
"network_protocol": string,
"to_ip": string,
"to_ip_prefix": integer,
"to_interface": string,
"to_transport": string,
"to_ports": [
integer
],
"to_port_ranges": [
{
"start": integer,
"end": integer
}
],
"to_service": string, # null if any to ports or port_ranges are set
"from_ip": string,
"from_ip_prefix": integer,
"from_interface": string,
"from_transport": string,
"from_ports": [
integer
],
"from_port_ranges": [
{
"start": integer,
"end": integer
}
],
"from_service": string, # null if any from ports or port_ranges are set
"comment": string # null if no comment
}
]
}
Examples:
$ ufw status verbose | jc --ufw -p
{
"status": "active",
"logging": "on",
"logging_level": "low",
"default": "deny (incoming), allow (outgoing), disabled (routed)",
"new_profiles": "skip",
"rules": [
{
"action": "ALLOW",
"action_direction": "IN",
"index": null,
"network_protocol": "ipv4",
"to_interface": "any",
"to_transport": "any",
"to_service": null,
"to_ports": [
22
],
"to_ip": "0.0.0.0",
"to_ip_prefix": 0,
"comment": null,
"from_ip": "0.0.0.0",
"from_ip_prefix": 0,
"from_interface": "any",
"from_transport": "any",
"from_port_ranges": [
{
"start": 0,
"end": 65535
}
],
"from_service": null
},
{
"action": "ALLOW",
"action_direction": "IN",
"index": null,
"network_protocol": "ipv4",
"to_interface": "any",
"to_transport": "tcp",
"to_service": null,
"to_ports": [
80,
443
],
"to_ip": "0.0.0.0",
"to_ip_prefix": 0,
"comment": null,
"from_ip": "0.0.0.0",
"from_ip_prefix": 0,
"from_interface": "any",
"from_transport": "any",
"from_port_ranges": [
{
"start": 0,
"end": 65535
}
],
"from_service": null
},
...
]
}
$ ufw status verbose | jc --ufw -p -r
{
"status": "active",
"logging": "on",
"logging_level": "low",
"default": "deny (incoming), allow (outgoing), disabled (routed)",
"new_profiles": "skip",
"rules": [
{
"action": "ALLOW",
"action_direction": "IN",
"index": null,
"network_protocol": "ipv4",
"to_interface": "any",
"to_transport": "any",
"to_service": null,
"to_ports": [
"22"
],
"to_ip": "0.0.0.0",
"to_ip_prefix": "0",
"comment": null,
"from_ip": "0.0.0.0",
"from_ip_prefix": "0",
"from_interface": "any",
"from_transport": "any",
"from_port_ranges": [
{
"start": "0",
"end": "65535"
}
],
"from_service": null
},
{
"action": "ALLOW",
"action_direction": "IN",
"index": null,
"network_protocol": "ipv4",
"to_interface": "any",
"to_transport": "tcp",
"to_service": null,
"to_ports": [
"80",
"443"
],
"to_ip": "0.0.0.0",
"to_ip_prefix": "0",
"comment": null,
"from_ip": "0.0.0.0",
"from_ip_prefix": "0",
"from_interface": "any",
"from_transport": "any",
"from_port_ranges": [
{
"start": "0",
"end": "65535"
}
],
"from_service": null
},
...
]
}
"""
import jc.utils
import re
import ipaddress
class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.0'
description = '`ufw status` command parser'
author = 'Kelly Brazil'
author_email = 'kellyjonbrazil@gmail.com'
compatible = ['linux']
magic_commands = ['ufw status']
__version__ = info.version
def _process(proc_data):
"""
Final processing to conform to the schema.
Parameters:
proc_data: (List of Dictionaries) raw structured data to process
Returns:
Dictionary. Structured to conform to the schema.
"""
int_list = ['index', 'to_ip_prefix', 'from_ip_prefix']
if 'rules' in proc_data:
for i, item in enumerate(proc_data['rules']):
for key in item:
if key in int_list:
proc_data['rules'][i][key] = jc.utils.convert_to_int(proc_data['rules'][i][key])
if key in ['to_ports', 'from_ports']:
for i2, item2 in enumerate(proc_data['rules'][i][key]):
proc_data['rules'][i][key][i2] = jc.utils.convert_to_int(item2)
if key in ['to_port_ranges', 'from_port_ranges']:
for i2, item2 in enumerate(proc_data['rules'][i][key]):
proc_data['rules'][i][key][i2]['start'] = jc.utils.convert_to_int(proc_data['rules'][i][key][i2]['start'])
proc_data['rules'][i][key][i2]['end'] = jc.utils.convert_to_int(proc_data['rules'][i][key][i2]['end'])
return proc_data
def _parse_to_from(linedata, direction, rule_obj=None):
if rule_obj is None:
rule_obj = {}
# pull out rule index, if they exist: [ 1]
if direction == 'to':
RE_LINE_NUM = re.compile(r'\[[ 0-9]+\]\s')
line_number_match = re.search(RE_LINE_NUM, linedata)
if line_number_match:
rule_obj['index'] = line_number_match.group(0).replace('[', '').replace(']', '').strip()
linedata = re.sub(RE_LINE_NUM, '', linedata)
else:
rule_obj['index'] = None
# pull out comments, if they exist
if direction == 'from':
RE_COMMENT = re.compile(r'#.+$')
comment_match = re.search(RE_COMMENT, linedata)
if comment_match:
rule_obj['comment'] = comment_match.group(0).lstrip('#').strip()
linedata = re.sub(RE_COMMENT, '', linedata)
else:
rule_obj['comment'] = None
# pull (v6)
RE_V6 = re.compile(r'\(v6\)')
v6_match = re.search(RE_V6, linedata)
if v6_match:
rule_obj['network_protocol'] = 'ipv6'
linedata = re.sub(RE_V6, '', linedata)
elif not rule_obj.get('network_protocol'):
rule_obj['network_protocol'] = 'ipv4'
# pull 'Anywhere' if exists. Assign to 0.0.0.0/0 or ::/0 depending on if (v6) is found
if 'Anywhere' in linedata:
if rule_obj.get('network_protocol') == 'ipv6':
rule_obj[direction + '_ip'] = '::'
rule_obj[direction + '_ip_prefix'] = '0'
elif rule_obj.get('network_protocol') == 'ipv4':
rule_obj[direction + '_ip'] = '0.0.0.0'
rule_obj[direction + '_ip_prefix'] = '0'
linedata = linedata.replace('Anywhere', '')
# pull out interface (after 'on')
linedata_list = linedata.split(' on ', maxsplit=1)
if len(linedata_list) > 1:
rule_obj[direction + '_interface'] = linedata_list[1].strip()
linedata = linedata_list[0]
else:
rule_obj[direction + '_interface'] = 'any'
# pull tcp/udp/etc. transport - strip on '/'
linedata_list = linedata.rsplit('/', maxsplit=1)
if len(linedata_list) > 1:
if linedata_list[1].strip() in ['tcp', 'udp', 'ah', 'esp', 'gre', 'ipv6', 'igmp']:
rule_obj[direction + '_transport'] = linedata_list[1].strip()
linedata = linedata_list[0]
else:
rule_obj[direction + '_transport'] = 'any'
else:
rule_obj[direction + '_transport'] = 'any'
# pull out ipv4 or ipv6 addresses
linedata_list = linedata.split()
new_linedata_list = []
valid_ip = None
for item in linedata_list:
try:
valid_ip = ipaddress.IPv4Interface(item)
except Exception:
try:
valid_ip = ipaddress.IPv6Interface(item)
except Exception:
new_linedata_list.append(item)
if valid_ip:
rule_obj[direction + '_ip'] = str(valid_ip.ip)
rule_obj[direction + '_ip_prefix'] = str(valid_ip.with_prefixlen.split('/')[1])
linedata = ' '.join(new_linedata_list)
# find the numeric port(s)
linedata_list = linedata.split(',')
port_list = []
port_ranges = []
for item in linedata_list:
if item.strip().isnumeric():
port_list.append(item.strip())
elif ':' in item:
p_range = item.strip().split(':', maxsplit=1)
port_ranges.append(
{
"start": p_range[0],
"end": p_range[1]
}
)
if port_list or port_ranges:
rule_obj[direction + '_service'] = None
linedata = ''
if port_list:
rule_obj[direction + '_ports'] = port_list
if port_ranges:
rule_obj[direction + '_port_ranges'] = port_ranges
# only thing left should be the service name.
if linedata.strip():
rule_obj[direction + '_service'] = linedata.strip()
rule_obj[direction + '_transport'] = None
# check if to/from IP addresses exist. If not, set to 0.0.0.0/0 or ::/0
if direction + '_ip' not in rule_obj:
if rule_obj.get('network_protocol') == 'ipv6':
rule_obj[direction + '_ip'] = '::'
rule_obj[direction + '_ip_prefix'] = '0'
elif rule_obj.get('network_protocol') == 'ipv4':
rule_obj[direction + '_ip'] = '0.0.0.0'
rule_obj[direction + '_ip_prefix'] = '0'
# finally set default ports if no ports exist and there should be some
if direction + '_transport' in rule_obj:
if rule_obj[direction + '_transport'] in ['tcp', 'udp', 'any']:
if not port_list and not port_ranges:
rule_obj[direction + '_port_ranges'] = [
{
'start': '0',
'end': '65535'
}
]
rule_obj[direction + '_service'] = None
return rule_obj
def parse(data, raw=False, quiet=False):
"""
Main text parsing function
Parameters:
data: (string) text data to parse
raw: (boolean) output preprocessed JSON if True
quiet: (boolean) suppress warning messages if True
Returns:
Dictionary. Raw or processed structured data.
"""
if not quiet:
jc.utils.compatibility(__name__, info.compatible)
raw_output = {}
rules_list = []
if jc.utils.has_data(data):
rule_lines = False
for line in filter(None, data.splitlines()):
if line.startswith('Status: '):
raw_output['status'] = line.split(': ', maxsplit=1)[1]
continue
if line.startswith('Logging: '):
log_line = line.split(': ', maxsplit=1)
log_line = log_line[1]
log_line = log_line.split()
raw_output['logging'] = log_line[0]
if len(log_line) == 2:
raw_output['logging_level'] = log_line[1].replace('(', '').replace(')', '').strip()
continue
if line.startswith('Default: '):
raw_output['default'] = line.split(': ', maxsplit=1)[1]
continue
if line.startswith('New profiles: '):
raw_output['new_profiles'] = line.split(': ', maxsplit=1)[1]
continue
if 'To' in line and 'Action' in line and 'From' in line:
rule_lines = True
continue
if rule_lines:
if '------' in line:
continue
# Split on action. Left of Action is 'to', right of Action is 'from'
rule_obj = {}
splitline = re.split(r'(ALLOW IN|ALLOW OUT|ALLOW FWD|DENY IN|DENY OUT|DENY FWD|LIMIT IN|LIMIT OUT|LIMIT FWD|REJECT IN|REJECT OUT|REJECT FWD|ALLOW|DENY|LIMIT|REJECT)', line)
to_line = splitline[0]
action_line = splitline[1]
action_list = action_line.split()
from_line = splitline[2]
action_direction = None
if len(action_list) == 1:
action = action_list[0]
elif len(action_list) == 2:
action = action_list[0]
action_direction = action_list[1]
rule_obj['action'] = action
rule_obj['action_direction'] = action_direction
rule_obj.update(_parse_to_from(to_line, 'to'))
rule_obj.update(_parse_to_from(from_line, 'from', rule_obj))
rules_list.append(rule_obj)
raw_output['rules'] = rules_list
if raw:
return raw_output
else:
return _process(raw_output)

364
jc/parsers/ufw_appinfo.py Normal file
View File

@@ -0,0 +1,364 @@
"""jc - JSON CLI output utility `ufw app info [application]` command output parser
Supports individual apps via `ufw app info [application]` and all apps list via `ufw app info all`.
Because `ufw` application definitions allow overlapping ports and port ranges, this parser preserves that behavior, but also provides `normalized` lists and ranges that remove duplicate ports and merge overlapping ranges.
Usage (cli):
$ ufw app info OpenSSH | jc --ufw-appinfo
or
$ jc ufw app info OpenSSH
Usage (module):
import jc.parsers.ufw_appinfo
result = jc.parsers.ufw_appinfo.parse(ufw_appinfo_command_output)
Schema:
[
{
"profile": string,
"title": string,
"description": string,
"tcp_list": [
integer
],
"tcp_ranges": [
{
"start": integer, # 'any' is converted to start/end: 0/65535
"end": integer
}
],
"udp_list": [
integer
],
"udp_ranges": [
{
"start": integer, # 'any' is converted to start/end: 0/65535
"end": integer
}
],
"normalized_tcp_list": [
integers # duplicates and overlapping are removed
],
"normalized_tcp_ranges": [
{
"start": integer, # 'any' is converted to start/end: 0/65535
"end": integers # overlapping are merged
}
],
"normalized_udp_list": [
integers # duplicates and overlapping are removed
],
"normalized_udp_ranges": [
{
"start": integer, # 'any' is converted to start/end: 0/65535
"end": integers # overlapping are merged
}
]
}
]
Examples:
$ ufw app info MSN | jc --ufw-appinfo -p
[
{
"profile": "MSN",
"title": "MSN Chat",
"description": "MSN chat protocol (with file transfer and voice)",
"tcp_list": [
1863,
6901
],
"udp_list": [
1863,
6901
],
"tcp_ranges": [
{
"start": 6891,
"end": 6900
}
],
"normalized_tcp_list": [
1863,
6901
],
"normalized_tcp_ranges": [
{
"start": 6891,
"end": 6900
}
],
"normalized_udp_list": [
1863,
6901
]
}
]
$ ufw app info MSN | jc --ufw-appinfo -p -r
[
{
"profile": "MSN",
"title": "MSN Chat",
"description": "MSN chat protocol (with file transfer and voice)",
"tcp_list": [
"1863",
"6901"
],
"udp_list": [
"1863",
"6901"
],
"tcp_ranges": [
{
"start": "6891",
"end": "6900"
}
]
}
]
"""
import jc.utils
class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.0'
description = '`ufw app info [application]` command parser'
author = 'Kelly Brazil'
author_email = 'kellyjonbrazil@gmail.com'
compatible = ['linux']
magic_commands = ['ufw app']
__version__ = info.version
def _process(proc_data):
"""
Final processing to conform to the schema.
Parameters:
proc_data: (List of Dictionaries) raw structured data to process
Returns:
List of Dictionaries. Structured to conform to the schema.
"""
for profile in proc_data:
# convert to ints
int_list = ['start', 'end']
if 'tcp_list' in profile:
profile['tcp_list'] = [int(p) for p in profile['tcp_list']]
if 'udp_list' in profile:
profile['udp_list'] = [int(p) for p in profile['udp_list']]
for protocol in ['tcp', 'udp']:
if protocol + '_ranges' in profile:
for i, item in enumerate(profile[protocol + '_ranges']):
for key in item:
if key in int_list:
profile[protocol + '_ranges'][i][key] = int(profile[protocol + '_ranges'][i][key])
# create normalized port lists and port ranges (remove duplicates and merge ranges)
# dump ranges into a set of 0 - 65535
# if items in the port list are in the set, then remove them
# iterate through the set to find gaps and create new ranges based on them
for protocol in ['tcp', 'udp']:
port_set = set()
if protocol + '_ranges' in profile:
for item in profile[protocol + '_ranges']:
port_set.update(range(item['start'], item['end'] + 1))
if protocol + '_list' in profile:
new_port_list = sorted(set([p for p in profile[protocol + '_list'] if p not in port_set]))
if new_port_list:
profile['normalized_' + protocol + '_list'] = new_port_list
new_port_ranges = []
state = 'findstart' # 'findstart' or 'findend'
for port in range(0, 65535 + 2):
if state == 'findstart':
port_range_obj = {}
if port in port_set:
port_range_obj['start'] = port
state = 'findend'
continue
if state == 'findend':
if port not in port_set:
port_range_obj['end'] = port - 1
new_port_ranges.append(port_range_obj)
state = 'findstart'
if new_port_ranges:
profile['normalized_' + protocol + '_ranges'] = new_port_ranges
return proc_data
def _parse_port_list(data, port_list=None):
"""return a list of port strings"""
# 1,2,3,4,5,6,7,8,9,10,9,30,80:90,8080:8090
# overlapping and repeated port numbers are allowed
if port_list is None:
port_list = []
data = data.split(',')
data_list = [p.strip() for p in data if ':' not in p and 'any' not in p]
port_list.extend(data_list)
return port_list
def _parse_port_range(data, range_list=None):
"""return a list of dictionaries"""
# 1,2,3,4,5,6,7,8,9,10,9,30,80:90,8080:8090
# overlapping port ranges are allowed
if range_list is None:
range_list = []
data = data.strip().split(',')
ranges = [p.strip() for p in data if ':' in p]
range_obj = {}
if 'any' in data:
range_list.append(
{
'start': 0,
'end': 65535
}
)
for range_ in ranges:
range_obj = {
'start': range_.split(':')[0],
'end': range_.split(':')[1]
}
range_list.append(range_obj)
return range_list
def parse(data, raw=False, quiet=False):
"""
Main text parsing function
Parameters:
data: (string) text data to parse
raw: (boolean) output preprocessed JSON if True
quiet: (boolean) suppress warning messages if True
Returns:
List of Dictionaries. Raw or processed structured data.
"""
if not quiet:
jc.utils.compatibility(__name__, info.compatible)
raw_output = []
item_obj = {}
if jc.utils.has_data(data):
ports = False
for line in filter(None, data.splitlines()):
if line.startswith('--'):
if item_obj:
raw_output.append(item_obj)
item_obj = {}
continue
if line.startswith('Profile:'):
item_obj['profile'] = line.split(': ')[1]
continue
if line.startswith('Title:'):
item_obj['title'] = line.split(': ')[1]
continue
if line.startswith('Description:'):
item_obj['description'] = line.split(': ')[1]
continue
if line.startswith('Port'):
ports = True
continue
if ports:
line_list = line.rsplit('/', maxsplit=1)
if len(line_list) == 2:
if line_list[1] == 'tcp':
tcp_prot_list = _parse_port_list(line_list[0])
if tcp_prot_list:
item_obj['tcp_list'] = tcp_prot_list
tcp_prot_range = _parse_port_range(line_list[0])
if tcp_prot_range:
item_obj['tcp_ranges'] = tcp_prot_range
elif line_list[1] == 'udp':
udp_prot_list = _parse_port_list(line_list[0])
if udp_prot_list:
item_obj['udp_list'] = udp_prot_list
udp_prot_range = _parse_port_range(line_list[0])
if udp_prot_range:
item_obj['udp_ranges'] = udp_prot_range
# 'any' case
else:
t_list = []
t_range = []
u_list = []
u_range = []
if 'tcp_list' in item_obj:
t_list = item_obj['tcp_list']
if 'tcp_ranges' in item_obj:
t_range = item_obj['tcp_ranges']
if 'udp_list' in item_obj:
u_list = item_obj['udp_list']
if 'udp_ranges' in item_obj:
u_range = item_obj['udp_ranges']
t_p_list = _parse_port_list(line, t_list)
if t_p_list:
item_obj['tcp_list'] = t_p_list
t_r_list = _parse_port_range(line, t_range)
if t_r_list:
item_obj['tcp_ranges'] = t_r_list
u_p_list = _parse_port_list(line, u_list)
if u_p_list:
item_obj['udp_list'] = u_p_list
u_r_list = _parse_port_range(line, u_range)
if u_r_list:
item_obj['udp_ranges'] = u_r_list
if item_obj:
raw_output.append(item_obj)
if raw:
return raw_output
else:
return _process(raw_output)

View File

@@ -43,6 +43,7 @@ Example:
}
"""
import jc.utils
from jc.exceptions import ParseError
class info():
@@ -60,10 +61,6 @@ class info():
__version__ = info.version
class ParseError(Exception):
pass
def _process(proc_data):
"""
Final processing to conform to the schema.

View File

@@ -65,19 +65,13 @@ Examples:
...
}
"""
import sys
import jc.utils
# check if xml library is installed and fail gracefully if it is not
try:
import xmltodict
except Exception:
jc.utils.error_message('The xmltodict library is not installed.')
sys.exit(1)
from jc.exceptions import LibraryNotInstalled
class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.4'
version = '1.5'
description = 'XML file parser'
author = 'Kelly Brazil'
author_email = 'kellyjonbrazil@gmail.com'
@@ -121,6 +115,12 @@ def parse(data, raw=False, quiet=False):
Dictionary. Raw or processed structured data.
"""
# check if xml library is installed and fail gracefully if it is not
try:
import xmltodict
except Exception:
raise LibraryNotInstalled('The xmltodict library is not installed.')
if not quiet:
jc.utils.compatibility(__name__, info.compatible)

View File

@@ -79,19 +79,13 @@ Examples:
}
]
"""
import sys
import jc.utils
# check if yaml library is installed and fail gracefully if it is not
try:
from ruamel.yaml import YAML
except Exception:
jc.utils.error_message('The ruamel.yaml library is not installed.')
sys.exit(1)
from jc.exceptions import LibraryNotInstalled
class info():
"""Provides parser metadata (version, author, etc.)"""
version = '1.4'
version = '1.5'
description = 'YAML file parser'
author = 'Kelly Brazil'
author_email = 'kellyjonbrazil@gmail.com'
@@ -135,6 +129,12 @@ def parse(data, raw=False, quiet=False):
List of Dictionaries representing the YAML documents.
"""
# check if yaml library is installed and fail gracefully if it is not
try:
from ruamel.yaml import YAML
except Exception:
raise LibraryNotInstalled('The ruamel.yaml library is not installed.')
if not quiet:
jc.utils.compatibility(__name__, info.compatible)

View File

@@ -85,7 +85,7 @@ def has_data(data):
def convert_to_int(value):
"""
Converts string input to integer by stripping all non-numeric characters
Converts string and float input to int. Strips all non-numeric characters from strings.
Parameters:
@@ -96,11 +96,12 @@ def convert_to_int(value):
integer/None Integer if successful conversion, otherwise None
"""
if isinstance(value, str):
str_val = re.sub(r'[^0-9\-\.]', '', value)
try:
return int(re.sub(r'[^0-9\-\.]', '', value))
except ValueError:
return int(str_val)
except (ValueError, TypeError):
try:
return int(convert_to_float(value))
return int(float(str_val))
except (ValueError, TypeError):
return None
@@ -113,7 +114,7 @@ def convert_to_int(value):
def convert_to_float(value):
"""
Converts string input to float by stripping all non-numeric characters
Converts string and int input to float. Strips all non-numeric characters from strings.
Parameters:
@@ -238,6 +239,9 @@ class timestamp:
}
utc_tz = False
# sometimes UTC is referenced as 'Coordinated Universal Time'. Convert to 'UTC'
data = data.replace('Coordinated Universal Time', 'UTC')
if 'UTC' in data:
utc_tz = True
if 'UTC+' in data or 'UTC-' in data:
@@ -253,6 +257,7 @@ class timestamp:
{'id': 1500, 'format': '%Y-%m-%d %H:%M', 'locale': None}, # en_US.UTF-8 local format (found in who cli output): 2021-03-23 00:14
{'id': 1600, 'format': '%m/%d/%Y %I:%M %p', 'locale': None}, # Windows english format (found in dir cli output): 12/07/2019 02:09 AM
{'id': 1700, 'format': '%m/%d/%Y, %I:%M:%S %p', 'locale': None}, # Windows english format wint non-UTC tz (found in systeminfo cli output): 3/22/2021, 1:15:51 PM (UTC-0600)
{'id': 1705, 'format': '%m/%d/%Y, %I:%M:%S %p %Z', 'locale': None}, # Windows english format with UTC tz (found in systeminfo cli output): 3/22/2021, 1:15:51 PM (UTC)
{'id': 1710, 'format': '%m/%d/%Y, %I:%M:%S %p UTC%z', 'locale': None}, # Windows english format with UTC tz (found in systeminfo cli output): 3/22/2021, 1:15:51 PM (UTC+0000)
{'id': 2000, 'format': '%a %d %b %Y %I:%M:%S %p %Z', 'locale': None}, # en_US.UTF-8 local format (found in upower cli output): Tue 23 Mar 2021 04:12:11 PM UTC
{'id': 3000, 'format': '%a %d %b %Y %I:%M:%S %p', 'locale': None}, # en_US.UTF-8 local format with non-UTC tz (found in upower cli output): Tue 23 Mar 2021 04:12:11 PM IST

Binary file not shown.

View File

@@ -1,3 +1,4 @@
#!/bin/bash
# system should be in "America/Los_Angeles" timezone for all tests to pass
python3 -m unittest -v

View File

@@ -5,7 +5,7 @@ with open('README.md', 'r') as f:
setuptools.setup(
name='jc',
version='1.15.2',
version='1.15.5',
author='Kelly Brazil',
author_email='kellyjonbrazil@gmail.com',
description='Converts the output of popular command-line tools and file-types to JSON.',

View File

@@ -4,12 +4,12 @@ jc \- JSONifies the output of many CLI tools and file-types
.SH SYNOPSIS
COMMAND | jc PARSER [OPTIONS]
or magic syntax:
or "Magic" syntax:
jc [OPTIONS] COMMAND
.SH DESCRIPTION
jc JSONifies the output of many CLI tools and file-types for easier parsing in scripts. jc accepts piped input from \fBSTDIN\fP and outputs a JSON representation of the previous command's output to \fBSTDOUT\fP. Alternatively, the "magic" syntax can be used by prepending jc to the command to be converted. Options can be passed to jc immediately before the command is given. (Note: command aliases are not supported).
jc JSONifies the output of many CLI tools and file-types for easier parsing in scripts. jc accepts piped input from \fBSTDIN\fP and outputs a JSON representation of the previous command's output to \fBSTDOUT\fP. Alternatively, the "Magic" syntax can be used by prepending jc to the command to be converted. Options can be passed to jc immediately before the command is given. (Note: "Magic" syntax does not support shell builtins or command aliases)
.SH OPTIONS
.B
@@ -62,6 +62,21 @@ raw JSON output
\fB-v\fP
version information
.SH EXIT CODES
Any fatal errors within jc will generate an exit code of \fB100\fP, otherwise the exit code will be \fB0\fP. When using the "Magic" syntax (e.g. \fBjc ifconfig eth0\fP), jc will store the exit code of the program being parsed and add it to the jc exit code. This way it is easier to determine if an error was from the parsed program or jc.
Consider the following examples using `ifconfig`:
.RS
ifconfig exit code = \fB0\fP, jc exit code = \fB0\fP, combined exit code = \fB0\fP (no errors)
ifconfig exit code = \fB1\fP, jc exit code = \fB0\fP, combined exit code = \fB1\fP (error in ifconfig)
ifconfig exit code = \fB0\fP, jc exit code = \fB100\fP, combined exit code = \fB100\fP (error in jc)
ifconfig exit code = \fB1\fP, jc exit code = \fB100\fP, combined exit code = \fB101\fP (error in both ifconfig and jc)
.RE
.SH ENVIRONMENT
You can specify custom colors via the \fBJC_COLORS\fP environment variable. The \fBJC_COLORS\fP environment variable takes four comma separated string values in the following format:
@@ -96,6 +111,19 @@ Local plugin filenames must be valid python module names, therefore must consist
Note: The application data directory follows the XDG Base Directory Specification
.SH CAVEATS
\fBLocale:\fP For best results set the \fBLANG\fP locale environment variable to \fBC\fP or \fBen_US.UTF-8\fP. For example, either by setting directly on the command-line:
\fB$ LANG=C date | jc --date\fP
or by exporting to the environment before running commands:
\fB$ export LANG=C\fP
\fBTimezones:\fP Some parsers have calculated epoch timestamp fields added to the output. Unless a timestamp field name has a \fB_utc\fP suffix it is considered naive. (i.e. based on the local timezone of the system the \fBjc\fP parser was run on).
If a UTC timezone can be detected in the text of the command output, the timestamp will be timezone aware and have a \fB_utc\fP suffix on the key name. (e.g. \fBepoch_utc\fP) No other timezones are supported for aware timestamps.
.SH EXAMPLES
Standard Syntax:
.RS

View File

@@ -62,26 +62,28 @@ The `jc` parsers can also be used as python modules. In this case the output wil
```
Two representations of the data are possible. The default representation uses a strict schema per parser and converts known numbers to int/float JSON values. Certain known values of `None` are converted to JSON `null`, known boolean values are converted, and, in some cases, additional semantic context fields are added.
> Note: Some parsers have calculated epoch timestamp fields added to the output. Unless a timestamp field name has a `_utc` suffix it is considered naive. (i.e. based on the local timezone of the system the `jc` parser was run on).
>
> If a UTC timezone can be detected in the text of the command output, the timestamp will be timezone aware and have a `_utc` suffix on the key name. (e.g. `epoch_utc`) No other timezones are supported for aware timestamps.
To access the raw, pre-processed JSON, use the `-r` cli option or the `raw=True` function parameter in `parse()`.
Schemas for each parser can be found at the documentation link beside each parser below.
Schemas for each parser can be found at the documentation link beside each [**Parser**](#parsers) below.
Release notes can be found [here](https://blog.kellybrazil.com/category/jc-news/).
## Why Would Anyone Do This!?
For more information on the motivations for this project, please see my [blog post](https://blog.kellybrazil.com/2019/11/26/bringing-the-unix-philosophy-to-the-21st-century/).
For more information on the motivations for this project, please see my blog post on [Bringing the Unix Philosophy to the 21st Century](https://blog.kellybrazil.com/2019/11/26/bringing-the-unix-philosophy-to-the-21st-century/).
See also:
- [libxo on FreeBSD](http://juniper.github.io/libxo/libxo-manual.html)
- [powershell](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-7)
- [blog: linux apps should have a json flag](https://thomashunter.name/posts/2012-06-06-linux-cli-apps-should-have-a-json-flag)
Use Cases:
- [Bash scripting](https://blog.kellybrazil.com/2021/04/12/practical-json-at-the-command-line/)
- [Ansible command output parsing](https://blog.kellybrazil.com/2020/08/30/parsing-command-output-in-ansible-with-jc/)
- [Saltstack command output parsing](https://blog.kellybrazil.com/2020/09/15/parsing-command-output-in-saltstack-with-jc/)
- [Nornir command output parsing](https://blog.kellybrazil.com/2020/12/09/parsing-command-output-in-nornir-with-jc/)
## Installation
There are several ways to get `jc`. You can install via `pip`; other OS package repositories like `apt-get`, `dnf`, `zypper`, `pacman`, `nix-env`, `guix`, `brew`, or `portsnap`; via DEB/RPM packages; or by downloading the correct binary for your architecture and running it anywhere on your filesystem.
There are several ways to get `jc`. You can install via `pip`; other OS package repositories like `apt-get`, `dnf`, `zypper`, `pacman`, `nix-env`, `guix`, `brew`, or `portsnap`; via DEB, RPM, and MSI packaged binaries for linux and Windows; or by downloading the correct binary for your architecture and running it anywhere on your filesystem.
### Pip (macOS, linux, unix, Windows)
```bash
@@ -102,7 +104,7 @@ pip3 install jc
| FreeBSD | `portsnap fetch update && cd /usr/ports/textproc/py-jc && make install clean` |
| Ansible filter plugin | `ansible-galaxy collection install community.general` |
> For more packages and binaries, see https://kellyjonbrazil.github.io/jc-packaging/.
> For more packages and binaries, see the [jc packaging](https://kellyjonbrazil.github.io/jc-packaging/) site.
## Usage
`jc` accepts piped input from `STDIN` and outputs a JSON representation of the previous command's output to `STDOUT`.
@@ -115,8 +117,6 @@ jc [OPTIONS] COMMAND
```
The JSON output can be compact (default) or pretty formatted with the `-p` option.
> Note: For best results set the `LANG` locale environment variable to `C`. For example, either by setting directly on the command-line: `$ LANG=C date | jc --date`, or by exporting to the environment before running commands: `$ export LANG=C`.
### Parsers
{% for parser in jc.parsers %}
- `{{ parser.argument }}` enables the {{ parser.description }} ([documentation](https://kellyjonbrazil.github.io/jc/docs/parsers/{{ parser.name }})){% endfor %}
@@ -131,6 +131,19 @@ The JSON output can be compact (default) or pretty formatted with the `-p` optio
- `-r` raw output. Provides a more literal JSON output, typically with string values and no additional semantic processing
- `-v` version information
### Exit Codes
Any fatal errors within `jc` will generate an exit code of `100`, otherwise the exit code will be `0`. When using the "magic" syntax (e.g. `jc ifconfig eth0`), `jc` will store the exit code of the program being parsed and add it to the `jc` exit code. This way it is easier to determine if an error was from the parsed program or `jc`.
Consider the following examples using `ifconfig`:
| `ifconfig` exit code | `jc` exit code | Combined exit code | Interpretation |
|----------------------|----------------|--------------------|------------------------------------|
| `0` | `0` | `0` | No errors |
| `1` | `0` | `1` | Error in `ifconfig` |
| `0` | `100` | `100` | Error in `jc` |
| `1` | `100` | `101` | Error in both `ifconfig` and `jc` |
### Setting Custom Colors via Environment Variable
You can specify custom colors via the `JC_COLORS` environment variable. The `JC_COLORS` environment variable takes four comma separated string values in the following format:
```bash
@@ -156,18 +169,41 @@ Custom local parser plugins may be placed in a `jc/jcparsers` folder in your loc
Local parser plugins are standard python module files. Use the [`jc/parsers/foo.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/foo.py) parser as a template and simply place a `.py` file in the `jcparsers` subfolder.
Local plugin filenames must be valid python module names, therefore must consist entirely of alphanumerics and start with a letter. Local plugins may override default plugins.
Local plugin filenames must be valid python module names, therefore must consist entirely of alphanumerics and start with a letter. Local plugins may override default parsers.
> Note: The application data directory follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
### Caveats
**Locale:**
For best results set the `LANG` locale environment variable to `C` or `en_US.UTF-8`. For example, either by setting directly on the command-line:
```
$ LANG=C date | jc --date
```
or by exporting to the environment before running commands:
```
$ export LANG=C
```
**Timezones:**
Some parsers have calculated epoch timestamp fields added to the output. Unless a timestamp field name has a `_utc` suffix it is considered naive. (i.e. based on the local timezone of the system the `jc` parser was run on).
If a UTC timezone can be detected in the text of the command output, the timestamp will be timezone aware and have a `_utc` suffix on the key name. (e.g. `epoch_utc`) No other timezones are supported for aware timestamps.
## Compatibility
Some parsers like `ls`, `ps`, `dig`, etc. will work on any platform. Other parsers that are platform-specific will generate a warning message if they are used on an unsupported platform. To see all parser information, including compatibility, run `jc -ap`.
Some parsers like `dig`, `xml`, `csv`, etc. will work on any platform. Other parsers that convert platform-specific output will generate a warning message if they are run on an unsupported platform. To see all parser information, including compatibility, run `jc -ap`.
You may still use a parser on an unsupported platform - for example, you may want to parse a file with linux `lsof` output on an macOS laptop. In that case you can suppress the warning message with the `-q` cli option or the `quiet=True` function parameter in `parse()`:
You may still use a parser on an unsupported platform - for example, you may want to parse a file with linux `lsof` output on an macOS or Windows laptop. In that case you can suppress the warning message with the `-q` cli option or the `quiet=True` function parameter in `parse()`:
macOS:
```bash
cat lsof.out | jc --lsof -q
```
or Windows:
```bash
type lsof.out | jc --lsof -q
```
Tested on:
- Centos 7.7
@@ -179,6 +215,8 @@ Tested on:
- NixOS
- FreeBSD12
- Windows 10
- Windows 2016 Server
- Windows 2019 Server
## Contributions
Feel free to add/improve code or parsers! You can use the [`jc/parsers/foo.py`](https://github.com/kellyjonbrazil/jc/blob/master/jc/parsers/foo.py) parser as a template and submit your parser with a pull request.

View File

@@ -0,0 +1 @@
{"destination_ip":"127.0.0.1","data_bytes":56,"pattern":null,"destination":"127.0.0.1","packets_transmitted":20,"packets_received":20,"packet_loss_percent":0.0,"duplicates":0,"time_ms":19070.0,"round_trip_ms_min":0.038,"round_trip_ms_avg":0.047,"round_trip_ms_max":0.08,"round_trip_ms_stddev":0.011,"responses":[{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":1,"ttl":64,"time_ms":0.038,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":2,"ttl":64,"time_ms":0.043,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":3,"ttl":64,"time_ms":0.044,"duplicate":false},{"type":"unparsable_line","unparsed_line":"64 bytes from 127.0.0.1: error - weird error"},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":5,"ttl":64,"time_ms":0.08,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":6,"ttl":64,"time_ms":0.043,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":7,"ttl":64,"time_ms":0.047,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":8,"ttl":64,"time_ms":0.04,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":9,"ttl":64,"time_ms":0.052,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":10,"ttl":64,"time_ms":0.044,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":11,"ttl":64,"time_ms":0.043,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":12,"ttl":64,"time_ms":0.043,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":13,"ttl":64,"time_ms":0.05,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":14,"ttl":64,"time_ms":0.045,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":15,"ttl":64,"time_ms":0.062,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":16,"ttl":64,"time_ms":0.046,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":17,"ttl":64,"time_ms":0.046,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":18,"ttl":64,"time_ms":0.045,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":19,"ttl":64,"time_ms":0.044,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"127.0.0.1","icmp_seq":20,"ttl":64,"time_ms":0.044,"duplicate":false}]}

View File

@@ -0,0 +1,27 @@
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.038 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.043 ms: some weird error
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.044 ms
64 bytes from 127.0.0.1: error - weird error
64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.080 ms
64 bytes from 127.0.0.1: icmp_seq=6 ttl=64 time=0.043 ms
this is a weird error message
64 bytes from 127.0.0.1: icmp_seq=7 ttl=64 time=0.047 ms
64 bytes from 127.0.0.1: icmp_seq=8 ttl=64 time=0.040 ms
64 bytes from 127.0.0.1: icmp_seq=9 ttl=64 time=0.052 ms
64 bytes from 127.0.0.1: icmp_seq=10 ttl=64 time=0.044 ms
64 bytes from 127.0.0.1: icmp_seq=11 ttl=64 time=0.043 ms
unparsable line
64 bytes from 127.0.0.1: icmp_seq=12 ttl=64 time=0.043 ms
64 bytes from 127.0.0.1: icmp_seq=13 ttl=64 time=0.050 ms
64 bytes from 127.0.0.1: icmp_seq=14 ttl=64 time=0.045 ms
64 bytes from 127.0.0.1: icmp_seq=15 ttl=64 time=0.062 ms
64 bytes from 127.0.0.1: icmp_seq=16 ttl=64 time=0.046 ms
64 bytes from 127.0.0.1: icmp_seq=17 ttl=64 time=0.046 ms
64 bytes from 127.0.0.1: icmp_seq=18 ttl=64 time=0.045 ms
64 bytes from 127.0.0.1: icmp_seq=19 ttl=64 time=0.044 ms
64 bytes from 127.0.0.1: icmp_seq=20 ttl=64 time=0.044 ms
--- 127.0.0.1 ping statistics ---
20 packets transmitted, 20 received, 0% packet loss, time 19070ms
rtt min/avg/max/mdev = 0.038/0.047/0.080/0.011 ms

View File

@@ -0,0 +1 @@
{"destination_ip":"2a04:4e42:600::323","data_bytes":56,"pattern":"0xabcd","destination":"2a04:4e42:600::323","packets_transmitted":20,"packets_received":19,"packet_loss_percent":5.0,"duplicates":0,"time_ms":19067.0,"round_trip_ms_min":27.064,"round_trip_ms_avg":33.626,"round_trip_ms_max":38.146,"round_trip_ms_stddev":3.803,"responses":[{"type":"unparsable_line","unparsed_line":"64 bytes from 2a04:4e42:600::323: strange error"},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":2,"ttl":59,"time_ms":28.4,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":3,"ttl":59,"time_ms":36.0,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":4,"ttl":59,"time_ms":28.5,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":5,"ttl":59,"time_ms":35.8,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":6,"ttl":59,"time_ms":34.4,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":7,"ttl":59,"time_ms":30.7,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":8,"ttl":59,"time_ms":28.5,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":9,"ttl":59,"time_ms":36.5,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":10,"ttl":59,"time_ms":36.3,"duplicate":false},{"type":"timeout","timestamp":null,"icmp_seq":11},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":12,"ttl":59,"time_ms":37.4,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":13,"ttl":59,"time_ms":30.7,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":14,"ttl":59,"time_ms":36.5,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":15,"ttl":59,"time_ms":35.4,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":16,"ttl":59,"time_ms":36.3,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":17,"ttl":59,"time_ms":37.5,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":18,"ttl":59,"time_ms":36.2,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":19,"ttl":59,"time_ms":27.0,"duplicate":false},{"type":"reply","timestamp":null,"bytes":64,"response_ip":"2a04:4e42:600::323","icmp_seq":20,"ttl":59,"time_ms":38.1,"duplicate":false}]}

View File

@@ -0,0 +1,27 @@
PATTERN: 0xabcd
PING 2a04:4e42:600::323(2a04:4e42:600::323) 56 data bytes
64 bytes from 2a04:4e42:600::323: strange error
64 bytes from 2a04:4e42:600::323: icmp_seq=2 ttl=59 time=28.4 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=3 ttl=59 time=36.0 ms
strange error here
64 bytes from 2a04:4e42:600::323: icmp_seq=4 ttl=59 time=28.5 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=5 ttl=59 time=35.8 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=6 ttl=59 time=34.4 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=7 ttl=59 time=30.7 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=8 ttl=59 time=28.5 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=9 ttl=59 time=36.5 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=10 ttl=59 time=36.3 ms
no answer yet for icmp_seq=11
64 bytes from 2a04:4e42:600::323: icmp_seq=12 ttl=59 time=37.4 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=13 ttl=59 time=30.7 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=14 ttl=59 time=36.5 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=15 ttl=59 time=35.4 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=16 ttl=59 time=36.3 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=17 ttl=59 time=37.5 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=18 ttl=59 time=36.2 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=19 ttl=59 time=27.0 ms
64 bytes from 2a04:4e42:600::323: icmp_seq=20 ttl=59 time=38.1 ms
--- 2a04:4e42:600::323 ping statistics ---
20 packets transmitted, 19 received, 5% packet loss, time 19067ms
rtt min/avg/max/mdev = 27.064/33.626/38.146/3.803 ms

View File

@@ -0,0 +1 @@
[{"profile":"MSN","title":"MSN Chat","description":"MSN chat protocol (with file transfer and voice)","tcp_list":[1863,6901],"udp_list":[1863,6901],"tcp_ranges":[{"start":6891,"end":6900}],"normalized_tcp_list":[1863,6901],"normalized_tcp_ranges":[{"start":6891,"end":6900}],"normalized_udp_list":[1863,6901]}]

View File

@@ -0,0 +1,9 @@
Profile: MSN
Title: MSN Chat
Description: MSN chat protocol (with file transfer and voice)
Ports:
1863
6891:6900/tcp
6901

View File

@@ -0,0 +1 @@
[{"profile":"TEST","title":"My test app","description":"a longer description of the test app here.","tcp_list":[1,2,3,4,5,6,7,8,9,10,9,8,7,30,53],"tcp_ranges":[{"start":80,"end":90},{"start":8080,"end":8090}],"udp_ranges":[{"start":50,"end":51},{"start":40,"end":60}],"udp_list":[53],"normalized_tcp_list":[1,2,3,4,5,6,7,8,9,10,30,53],"normalized_tcp_ranges":[{"start":80,"end":90},{"start":8080,"end":8090}],"normalized_udp_ranges":[{"start":40,"end":60}]}]

View File

@@ -0,0 +1,9 @@
Profile: TEST
Title: My test app
Description: a longer description of the test app here.
Ports:
1,2,3,4,5,6,7,8,9,10,9,8,7,30,80:90,8080:8090/tcp
50:51,40:60/udp
53

View File

@@ -0,0 +1 @@
[{"profile":"TEST2","title":"My test app2","description":"a longer description of the test app here.","tcp_ranges":[{"start":0,"end":65535}],"udp_ranges":[{"start":50,"end":51}],"tcp_list":[53],"udp_list":[53],"normalized_tcp_ranges":[{"start":0,"end":65535}],"normalized_udp_list":[53],"normalized_udp_ranges":[{"start":50,"end":51}]}]

View File

@@ -0,0 +1,9 @@
Profile: TEST2
Title: My test app2
Description: a longer description of the test app here.
Ports:
any/tcp
50:51/udp
53

View File

@@ -0,0 +1 @@
[{"profile":"TEST3","title":"My test app3","description":"test overlapping ports","tcp_list":[80,83,80,53],"tcp_ranges":[{"start":70,"end":90}],"udp_ranges":[{"start":50,"end":51}],"udp_list":[53],"normalized_tcp_list":[53],"normalized_tcp_ranges":[{"start":70,"end":90}],"normalized_udp_list":[53],"normalized_udp_ranges":[{"start":50,"end":51}]}]

View File

@@ -0,0 +1,9 @@
Profile: TEST3
Title: My test app3
Description: test overlapping ports
Ports:
80,83,80,70:90/tcp
50:51/udp
53

View File

@@ -0,0 +1 @@
{"status":"inactive","rules":[]}

View File

@@ -0,0 +1 @@
Status: inactive

File diff suppressed because one or more lines are too long

23
tests/fixtures/generic/ufw-numbered.out vendored Normal file
View File

@@ -0,0 +1,23 @@
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
[ 1] 22/tcp ALLOW IN Anywhere
[ 2] 22/tcp (v6) ALLOW OUT Anywhere (v6)
[ 3] 443/tcp DENY 192.168.0.1
[ 4] 443/udp DENY OUT 192.168.0.7 8080:8081
[ 5] 22/tcp ALLOW 192.168.0.0/24
[ 6] 22/udp ALLOW 192.168.0.0/24 8080:8081 on en0
[ 7] 22/tcp (v6) ALLOW IN 2405:204:7449:49fc:f09a:6f4a:bc93:1955/64 on en1
[ 8] 80 ALLOW IN Anywhere
[ 9] 8080 (v6) ALLOW IN Anywhere (v6)
[10] Apache Full ALLOW IN Anywhere
[11] Apache Full (v6) ALLOW IN Anywhere (v6)
[12] OpenSSH (v6) DENY IN Anywhere (v6)
[13] 10.10.10.10 8080 on enp34s0 ALLOW 127.0.0.1 8000
[14] 50200:50300/tcp (v6) ALLOW Anywhere (v6)
[15] Anywhere (v6) ALLOW IN 2405:204:7449:49fc:f09a:6f4a:bc93:1955

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
Status: active
To Action From
-- ------ ----
[ 1] 224.0.0.251 mDNS ALLOW IN Anywhere
[ 2] Anywhere ALLOW IN 123.123.123.123
[ 3] 25 ALLOW IN Anywhere
[ 4] 80 ALLOW IN Anywhere
[ 5] 443 ALLOW IN Anywhere
[ 6] 465 ALLOW IN Anywhere
[ 7] 993 ALLOW IN Anywhere
[ 8] 995 ALLOW IN Anywhere
[ 9] ff02::fb mDNS ALLOW IN Anywhere (v6)
[10] 25 (v6) ALLOW IN Anywhere (v6)
[11] 80 (v6) ALLOW IN Anywhere (v6)
[12] 443 (v6) ALLOW IN Anywhere (v6)
[13] 465 (v6) ALLOW IN Anywhere (v6)
[14] 993 (v6) ALLOW IN Anywhere (v6)
[15] 995 (v6) ALLOW IN Anywhere (v6)

1
tests/fixtures/generic/ufw.json vendored Normal file

File diff suppressed because one or more lines are too long

22
tests/fixtures/generic/ufw.out vendored Normal file
View File

@@ -0,0 +1,22 @@
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
22/tcp (v6) ALLOW OUT Anywhere (v6)
443/tcp DENY 192.168.0.1 # nice comment
443/udp DENY OUT 192.168.0.7 8080:8081
22/tcp ALLOW 192.168.0.0/24
22/udp ALLOW 192.168.0.0/24 8080:8081 on en0
22/tcp (v6) ALLOW FWD 2405:204:7449:49fc:f09a:6f4a:bc93:1955/64 on en1 #commenting this rule
80 ALLOW IN Anywhere
8080 (v6) REJECT IN Anywhere (v6)
Apache Full ALLOW IN Anywhere # a comment
Apache Full (v6) ALLOW IN Anywhere (v6)
OpenSSH (v6) DENY IN Anywhere (v6)
10.10.10.10 8080 on enp34s0 ALLOW 127.0.0.1 8000
50200:50300/tcp (v6) DENY FWD Anywhere (v6)
Anywhere (v6) LIMIT 2405:204:7449:49fc:f09a:6f4a:bc93:1955 # this is a comment

View File

@@ -0,0 +1 @@
[{"answer":[{"name":"cnn.com.","class":"IN","type":"A","ttl":47,"data":"151.101.65.67"},{"name":"cnn.com.","class":"IN","type":"A","ttl":47,"data":"151.101.193.67"},{"name":"cnn.com.","class":"IN","type":"A","ttl":47,"data":"151.101.129.67"},{"name":"cnn.com.","class":"IN","type":"A","ttl":47,"data":"151.101.1.67"}]}]

View File

@@ -0,0 +1,4 @@
cnn.com. 47 IN A 151.101.65.67
cnn.com. 47 IN A 151.101.193.67
cnn.com. 47 IN A 151.101.129.67
cnn.com. 47 IN A 151.101.1.67

View File

@@ -0,0 +1 @@
{"destination_ip":"192.168.1.220","data_bytes":56,"pattern":null,"destination":"192.168.1.220","packets_transmitted":8,"packets_received":0,"packet_loss_percent":100.0,"duplicates":0,"responses":[{"type":"timeout","icmp_seq":0,"duplicate":false},{"type":"timeout","icmp_seq":1,"duplicate":false},{"type":"unparsable_line","unparsed_line":"92 bytes from fgt1.attlocal.net (192.168.1.220) Destination Network Unreachable"},{"type":"destination_host_unreachable","bytes":92,"response_ip":"192.168.1.220","vr":4,"hl":5,"tos":0,"len":21504,"id":11887,"flg":0,"off":0,"ttl":63,"pro":1,"cks":51248,"src":"192.168.1.221","dst":"192.168.1.220"},{"type":"unparsable_line","unparsed_line":"92 bytes from fgt1.attlocal.net (192.168.1.220) Weird error message"},{"type":"timeout","icmp_seq":2,"duplicate":false},{"type":"timeout","icmp_seq":3,"duplicate":false},{"type":"timeout","icmp_seq":4,"duplicate":false},{"type":"destination_host_unreachable","bytes":92,"response_ip":"192.168.1.220","vr":4,"hl":5,"tos":0,"len":21504,"id":40674,"flg":0,"off":0,"ttl":63,"pro":1,"cks":22461,"src":"192.168.1.221","dst":"192.168.1.220"},{"type":"destination_host_unreachable","bytes":92,"response_ip":"192.168.1.220","vr":4,"hl":5,"tos":0,"len":21504,"id":31035,"flg":0,"off":0,"ttl":63,"pro":1,"cks":32100,"src":"192.168.1.221","dst":"192.168.1.220"},{"type":"destination_host_unreachable","bytes":92,"response_ip":"192.168.1.220","vr":4,"hl":5,"tos":0,"len":21504,"id":53536,"flg":0,"off":0,"ttl":63,"pro":1,"cks":9599,"src":"192.168.1.221","dst":"192.168.1.220"},{"type":"timeout","icmp_seq":5,"duplicate":false},{"type":"timeout","icmp_seq":6,"duplicate":false}]}

View File

@@ -0,0 +1,35 @@
PING 192.168.1.220 (192.168.1.220): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
92 bytes from fgt1.attlocal.net (192.168.1.220): Destination Network Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 567b 0 0000 3f 01 a024 192.168.1.221 192.168.1.220
92 bytes from fgt1.attlocal.net (192.168.1.220): Destination Host Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 2e6f 0 0000 3f 01 c830 192.168.1.221 192.168.1.220
92 bytes from fgt1.attlocal.net (192.168.1.220): Weird error message
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 a1ed 0 0000 3f 01 54b2 192.168.1.221 192.168.1.220
Request timeout for icmp_seq 2
Request timeout for icmp_seq 3
Request timeout for icmp_seq 4
92 bytes from fgt1.attlocal.net (192.168.1.220): Destination Host Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 9ee2 0 0000 3f 01 57bd 192.168.1.221 192.168.1.220
92 bytes from fgt1.attlocal.net (192.168.1.220): Destination Host Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 793b 0 0000 3f 01 7d64 192.168.1.221 192.168.1.220
92 bytes from fgt1.attlocal.net (192.168.1.220): Destination Host Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 d120 0 0000 3f 01 257f 192.168.1.221 192.168.1.220
Request timeout for icmp_seq 5
Request timeout for icmp_seq 6
--- 192.168.1.220 ping statistics ---
8 packets transmitted, 0 packets received, 100.0% packet loss

View File

@@ -0,0 +1 @@
{"destination_ip":"192.168.1.220","data_bytes":56,"pattern":null,"destination":"192.168.1.220","packets_transmitted":8,"packets_received":0,"packet_loss_percent":100.0,"duplicates":0,"responses":[{"type":"timeout","icmp_seq":0,"duplicate":false},{"type":"timeout","icmp_seq":1,"duplicate":false},{"type":"destination_host_unreachable","bytes":92,"response_ip":"192.168.1.220","vr":4,"hl":5,"tos":0,"len":21504,"id":22139,"flg":0,"off":0,"ttl":63,"pro":1,"cks":40996,"src":"192.168.1.221","dst":"192.168.1.220"},{"type":"destination_host_unreachable","bytes":92,"response_ip":"192.168.1.220","vr":4,"hl":5,"tos":0,"len":21504,"id":11887,"flg":0,"off":0,"ttl":63,"pro":1,"cks":51248,"src":"192.168.1.221","dst":"192.168.1.220"},{"type":"destination_host_unreachable","bytes":92,"response_ip":"192.168.1.220","vr":4,"hl":5,"tos":0,"len":21504,"id":41453,"flg":0,"off":0,"ttl":63,"pro":1,"cks":21682,"src":"192.168.1.221","dst":"192.168.1.220"},{"type":"timeout","icmp_seq":2,"duplicate":false},{"type":"timeout","icmp_seq":3,"duplicate":false},{"type":"timeout","icmp_seq":4,"duplicate":false},{"type":"destination_host_unreachable","bytes":92,"response_ip":"192.168.1.220","vr":4,"hl":5,"tos":0,"len":21504,"id":40674,"flg":0,"off":0,"ttl":63,"pro":1,"cks":22461,"src":"192.168.1.221","dst":"192.168.1.220"},{"type":"destination_host_unreachable","bytes":92,"response_ip":"192.168.1.220","vr":4,"hl":5,"tos":0,"len":21504,"id":31035,"flg":0,"off":0,"ttl":63,"pro":1,"cks":32100,"src":"192.168.1.221","dst":"192.168.1.220"},{"type":"destination_host_unreachable","bytes":92,"response_ip":"192.168.1.220","vr":4,"hl":5,"tos":0,"len":21504,"id":53536,"flg":0,"off":0,"ttl":63,"pro":1,"cks":9599,"src":"192.168.1.221","dst":"192.168.1.220"},{"type":"timeout","icmp_seq":5,"duplicate":false},{"type":"timeout","icmp_seq":6,"duplicate":false}]}

View File

@@ -0,0 +1,35 @@
PING 192.168.1.220 (192.168.1.220): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1
92 bytes from fgt1.attlocal.net (192.168.1.220): Destination Host Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 567b 0 0000 3f 01 a024 192.168.1.221 192.168.1.220
92 bytes from fgt1.attlocal.net (192.168.1.220): Destination Host Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 2e6f 0 0000 3f 01 c830 192.168.1.221 192.168.1.220
92 bytes from fgt1.attlocal.net (192.168.1.220): Destination Host Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 a1ed 0 0000 3f 01 54b2 192.168.1.221 192.168.1.220
Request timeout for icmp_seq 2
Request timeout for icmp_seq 3
Request timeout for icmp_seq 4
92 bytes from fgt1.attlocal.net (192.168.1.220): Destination Host Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 9ee2 0 0000 3f 01 57bd 192.168.1.221 192.168.1.220
92 bytes from fgt1.attlocal.net (192.168.1.220): Destination Host Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 793b 0 0000 3f 01 7d64 192.168.1.221 192.168.1.220
92 bytes from fgt1.attlocal.net (192.168.1.220): Destination Host Unreachable
Vr HL TOS Len ID Flg off TTL Pro cks Src Dst
4 5 00 5400 d120 0 0000 3f 01 257f 192.168.1.221 192.168.1.220
Request timeout for icmp_seq 5
Request timeout for icmp_seq 6
--- 192.168.1.220 ping statistics ---
8 packets transmitted, 0 packets received, 100.0% packet loss

View File

@@ -0,0 +1 @@
{"source_ip":"::1","destination_ip":"::1","data_bytes":56,"pattern":null,"destination":"::1","packets_transmitted":3,"packets_received":3,"packet_loss_percent":0.0,"duplicates":0,"round_trip_ms_min":0.071,"round_trip_ms_avg":0.115,"round_trip_ms_max":0.153,"round_trip_ms_stddev":0.034,"responses":[{"type":"reply","bytes":16,"response_ip":"::1","icmp_seq":0,"ttl":64,"time_ms":0.071,"duplicate":false},{"type":"unparsable_line","unparsed_line":"16 bytes from ::1 strange error"},{"type":"reply","bytes":16,"response_ip":"::1","icmp_seq":2,"ttl":64,"time_ms":0.122,"duplicate":false}]}

View File

@@ -0,0 +1,9 @@
PING6(56=40+8+8 bytes) ::1 --> ::1
16 bytes from ::1, icmp_seq=0 hlim=64 time=0.071 ms
16 bytes from ::1, strange error
weird error message
16 bytes from ::1, icmp_seq=2 hlim=64 time=0.122 ms
--- ::1 ping6 statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 0.071/0.115/0.153/0.034 ms

View File

@@ -0,0 +1 @@
[{"profile":"MSN","title":"MSN Chat","description":"MSN chat protocol (with file transfer and voice)","tcp_list":[1863,6901],"udp_list":[1863,6901],"tcp_ranges":[{"start":6891,"end":6900}],"normalized_tcp_list":[1863,6901],"normalized_tcp_ranges":[{"start":6891,"end":6900}],"normalized_udp_list":[1863,6901]},{"profile":"OpenSSH","title":"Secure shell server, an rshd replacement","description":"OpenSSH is a free implementation of the Secure Shell protocol.","tcp_list":[22],"normalized_tcp_list":[22]},{"profile":"TEST","title":"My test app","description":"a longer description of the test app here.","tcp_list":[1,2,3,4,5,6,7,8,9,10,30,53],"tcp_ranges":[{"start":80,"end":90},{"start":8080,"end":8090}],"udp_ranges":[{"start":50,"end":51}],"udp_list":[53],"normalized_tcp_list":[1,2,3,4,5,6,7,8,9,10,30,53],"normalized_tcp_ranges":[{"start":80,"end":90},{"start":8080,"end":8090}],"normalized_udp_list":[53],"normalized_udp_ranges":[{"start":50,"end":51}]},{"profile":"TEST2","title":"My test app2","description":"a longer description of the test app here.","tcp_ranges":[{"start":0,"end":65535}],"udp_ranges":[{"start":50,"end":51}],"tcp_list":[53],"udp_list":[53],"normalized_tcp_ranges":[{"start":0,"end":65535}],"normalized_udp_list":[53],"normalized_udp_ranges":[{"start":50,"end":51}]},{"profile":"TEST3","title":"My test app3","description":"test overlapping ports","tcp_list":[80,83,80,53],"tcp_ranges":[{"start":70,"end":90}],"udp_ranges":[{"start":50,"end":51}],"udp_list":[53],"normalized_tcp_list":[53],"normalized_tcp_ranges":[{"start":70,"end":90}],"normalized_udp_list":[53],"normalized_udp_ranges":[{"start":50,"end":51}]}]

View File

@@ -0,0 +1,51 @@
Profile: MSN
Title: MSN Chat
Description: MSN chat protocol (with file transfer and voice)
Ports:
1863
6891:6900/tcp
6901
--
Profile: OpenSSH
Title: Secure shell server, an rshd replacement
Description: OpenSSH is a free implementation of the Secure Shell protocol.
Port:
22/tcp
--
Profile: TEST
Title: My test app
Description: a longer description of the test app here.
Ports:
1,2,3,4,5,6,7,8,9,10,30,80:90,8080:8090/tcp
50:51/udp
53
--
Profile: TEST2
Title: My test app2
Description: a longer description of the test app here.
Ports:
any/tcp
50:51/udp
53
--
Profile: TEST3
Title: My test app3
Description: test overlapping ports
Ports:
80,83,80,70:90/tcp
50:51/udp
53

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
Status: active
To Action From
-- ------ ----
[ 1] 22 ALLOW IN Anywhere
[ 2] 80,443/tcp ALLOW IN Anywhere
[ 3] 80 on eth0 ALLOW IN Anywhere # test
[ 4] 10.0.0.1/ipv6 ALLOW IN 10.4.0.0/16/ipv6
[ 5] 10.0.0.1/esp ALLOW IN Anywhere
[ 6] 10.0.0.1/esp ALLOW IN 10.4.0.0/16/esp
[ 7] 10.0.0.1/ah ALLOW IN Anywhere
[ 8] 10.0.0.1/ah ALLOW IN 10.4.0.0/16/ah
[ 9] 100:200,300:400/tcp ALLOW IN Anywhere
[10] 1,2,100:200,300:400/udp ALLOW IN Anywhere
[11] 22 (v6) ALLOW IN Anywhere (v6)
[12] 80,443/tcp (v6) ALLOW IN Anywhere (v6)
[13] 80 (v6) on eth0 ALLOW IN Anywhere (v6) # test
[14] 100:200,300:400/tcp (v6) ALLOW IN Anywhere (v6)
[15] 1,2,100:200,300:400/udp (v6) ALLOW IN Anywhere (v6)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22 ALLOW IN Anywhere
80,443/tcp ALLOW IN Anywhere
80 on eth0 ALLOW IN Anywhere # test
10.0.0.1/ipv6 ALLOW IN 10.4.0.0/16/ipv6
10.0.0.1/esp ALLOW IN Anywhere
10.0.0.1/esp ALLOW IN 10.4.0.0/16/esp
10.0.0.1/ah ALLOW IN Anywhere
10.0.0.1/ah ALLOW IN 10.4.0.0/16/ah
100:200,300:400/tcp ALLOW IN Anywhere
1,2,100:200,300:400/udp ALLOW IN Anywhere
22 (v6) ALLOW IN Anywhere (v6)
80,443/tcp (v6) ALLOW IN Anywhere (v6)
80 (v6) on eth0 ALLOW IN Anywhere (v6) # test
100:200,300:400/tcp (v6) ALLOW IN Anywhere (v6)
1,2,100:200,300:400/udp (v6) ALLOW IN Anywhere (v6)

View File

@@ -5,31 +5,31 @@ import jc.cli
class MyTests(unittest.TestCase):
def test_cli_generate_magic_command(self):
def test_cli_magic_parser(self):
commands = {
'jc -p systemctl list-sockets': 'systemctl list-sockets | jc --systemctl-ls -p',
'jc -p systemctl list-unit-files': 'systemctl list-unit-files | jc --systemctl-luf -p',
'jc -p pip list': 'pip list | jc --pip-list -p',
'jc -p pip3 list': 'pip3 list | jc --pip-list -p',
'jc -p pip show jc': 'pip show jc | jc --pip-show -p',
'jc -p pip3 show jc': 'pip3 show jc | jc --pip-show -p',
'jc -prd last': 'last | jc --last -prd',
'jc -prd lastb': 'lastb | jc --last -prd',
'jc -p airport -I': 'airport -I | jc --airport -p',
'jc -p -r airport -I': 'airport -I | jc --airport -pr',
'jc -prd airport -I': 'airport -I | jc --airport -prd',
'jc -p nonexistent command': 'nonexistent command',
'jc -ap': None,
'jc -a arp -a': None,
'jc -v': None,
'jc -h': None,
'jc -h --arp': None,
'jc -h arp': None,
'jc -h arp -a': None
'jc -p systemctl list-sockets': (True, ['systemctl', 'list-sockets'], '--systemctl-ls', ['p']),
'jc -p systemctl list-unit-files': (True, ['systemctl', 'list-unit-files'], '--systemctl-luf', ['p']),
'jc -p pip list': (True, ['pip', 'list'], '--pip-list', ['p']),
'jc -p pip3 list': (True, ['pip3', 'list'], '--pip-list', ['p']),
'jc -p pip show jc': (True, ['pip', 'show', 'jc'], '--pip-show', ['p']),
'jc -p pip3 show jc': (True, ['pip3', 'show', 'jc'], '--pip-show', ['p']),
'jc -prd last': (True, ['last'], '--last', ['p', 'r', 'd']),
'jc -prdd lastb': (True, ['lastb'], '--last', ['p', 'r', 'd', 'd']),
'jc -p airport -I': (True, ['airport', '-I'], '--airport', ['p']),
'jc -p -r airport -I': (True, ['airport', '-I'], '--airport', ['p', 'r']),
'jc -prd airport -I': (True, ['airport', '-I'], '--airport', ['p', 'r', 'd']),
'jc -p nonexistent command': (False, ['nonexistent', 'command'], None, ['p']),
'jc -ap': (False, None, None, []),
'jc -a arp -a': (False, None, None, []),
'jc -v': (False, None, None, []),
'jc -h': (False, None, None, []),
'jc -h --arp': (False, None, None, []),
'jc -h arp': (False, None, None, []),
'jc -h arp -a': (False, None, None, [])
}
for command, expected_command in commands.items():
self.assertEqual(jc.cli.generate_magic_command(command.split(' '))[1], expected_command)
self.assertEqual(jc.cli.magic_parser(command.split(' ')), expected_command)
def test_cli_set_env_colors(self):
if pygments.__version__.startswith('2.3.'):

View File

@@ -55,10 +55,12 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/dig-axfr.out'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_dig_axfr = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/dig-noall-answer.out'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_dig_noall_answer = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/dig-answer-spaces.out'), 'r', encoding='utf-8') as f:
self.generic_dig_answer_spaces = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/dig-additional.out'), 'r', encoding='utf-8') as f:
self.generic_dig_additional = f.read()
@@ -123,10 +125,12 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/dig-axfr.json'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_dig_axfr_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/dig-noall-answer.json'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_dig_noall_answer_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/dig-answer-spaces.json'), 'r', encoding='utf-8') as f:
self.generic_dig_answer_spaces_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/dig-additional.json'), 'r', encoding='utf-8') as f:
self.generic_dig_additional_json = json.loads(f.read())
@@ -241,6 +245,12 @@ class MyTests(unittest.TestCase):
"""
self.assertEqual(jc.parsers.dig.parse(self.osx_10_14_6_dig_axfr, quiet=True), self.osx_10_14_6_dig_axfr_json)
def test_dig_noall_answer_osx_10_14_6(self):
"""
Test 'dig +noall +answer' on OSX 10.14.6
"""
self.assertEqual(jc.parsers.dig.parse(self.osx_10_14_6_dig_noall_answer, quiet=True), self.osx_10_14_6_dig_noall_answer_json)
def test_dig_answer_spaces(self):
"""
Test 'dig' with spaces in the answer data (e.g. TXT responses)

View File

@@ -30,6 +30,9 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/centos-7.7/ping6-ip-O-p.out'), 'r', encoding='utf-8') as f:
self.centos_7_7_ping6_ip_O_p = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/centos-7.7/ping6-ip-O-p-unparsable.out'), 'r', encoding='utf-8') as f:
self.centos_7_7_ping6_ip_O_p_unparsable = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/centos-7.7/ping6-ip-O-D-p.out'), 'r', encoding='utf-8') as f:
self.centos_7_7_ping6_ip_O_D_p = f.read()
@@ -45,6 +48,9 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/centos-7.7/ping6-ip-dup.out'), 'r', encoding='utf-8') as f:
self.centos_7_7_ping6_ip_dup = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/centos-7.7/ping-ip-O-unparsedlines.out'), 'r', encoding='utf-8') as f:
self.centos_7_7_ping_ip_O_unparsedlines = f.read()
# ubuntu
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-18.04/ping-ip-O.out'), 'r', encoding='utf-8') as f:
self.ubuntu_18_4_ping_ip_O = f.read()
@@ -157,6 +163,12 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping-ip.out'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping_ip = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping-ip-unreachable.out'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping_ip_unreachable = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping-ip-unknown-errors.out'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping_ip_unknown_errors = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping6-hostname-p.out'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping6_hostname_p = f.read()
@@ -175,6 +187,9 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping6-ip.out'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping6_ip = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping6-ip-unparsable.out'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping6_ip_unparsable = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping-ip-dup.out'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping_ip_dup = f.read()
@@ -209,6 +224,9 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/centos-7.7/ping6-ip-O-p.json'), 'r', encoding='utf-8') as f:
self.centos_7_7_ping6_ip_O_p_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/centos-7.7/ping6-ip-O-p-unparsable.json'), 'r', encoding='utf-8') as f:
self.centos_7_7_ping6_ip_O_p_unparsable_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/centos-7.7/ping6-ip-O-D-p.json'), 'r', encoding='utf-8') as f:
self.centos_7_7_ping6_ip_O_D_p_json = json.loads(f.read())
@@ -224,6 +242,9 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/centos-7.7/ping6-ip-dup.json'), 'r', encoding='utf-8') as f:
self.centos_7_7_ping6_ip_dup_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/centos-7.7/ping-ip-O-unparsedlines.json'), 'r', encoding='utf-8') as f:
self.centos_7_7_ping_ip_O_unparsedlines_json = json.loads(f.read())
# ubunutu
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-18.04/ping-ip-O.json'), 'r', encoding='utf-8') as f:
self.ubuntu_18_4_ping_ip_O_json = json.loads(f.read())
@@ -336,6 +357,12 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping-ip.json'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping_ip_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping-ip-unreachable.json'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping_ip_unreachable_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping-ip-unknown-errors.json'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping_ip_unknown_errors_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping6-hostname-p.json'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping6_hostname_p_json = json.loads(f.read())
@@ -354,6 +381,9 @@ class MyTests(unittest.TestCase):
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping6-ip.json'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping6_ip_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping6-ip-unparsable.json'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping6_ip_unparsable_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/osx-10.14.6/ping-ip-dup.json'), 'r', encoding='utf-8') as f:
self.osx_10_14_6_ping_ip_dup_json = json.loads(f.read())
@@ -409,6 +439,12 @@ class MyTests(unittest.TestCase):
"""
self.assertEqual(jc.parsers.ping.parse(self.centos_7_7_ping6_ip_O_p, quiet=True), self.centos_7_7_ping6_ip_O_p_json)
def test_ping6_ip_O_p_unparsable_centos_7_7(self):
"""
Test 'ping6 <ip> -O -p' with unparsable lines on Centos 7.7
"""
self.assertEqual(jc.parsers.ping.parse(self.centos_7_7_ping6_ip_O_p_unparsable, quiet=True), self.centos_7_7_ping6_ip_O_p_unparsable_json)
def test_ping6_ip_O_D_p_centos_7_7(self):
"""
Test 'ping6 <ip> -O -D -p' on Centos 7.7
@@ -439,6 +475,12 @@ class MyTests(unittest.TestCase):
"""
self.assertEqual(jc.parsers.ping.parse(self.centos_7_7_ping6_ip_dup, quiet=True), self.centos_7_7_ping6_ip_dup_json)
def test_ping_ip_O_unparsedlines_centos_7_7(self):
"""
Test 'ping <ip> -O' on Centos 7.7 with unparsable lines and error messages
"""
self.assertEqual(jc.parsers.ping.parse(self.centos_7_7_ping_ip_O_unparsedlines, quiet=True), self.centos_7_7_ping_ip_O_unparsedlines_json)
def test_ping_ip_O_ubuntu_18_4(self):
"""
Test 'ping <ip> -O' on Ubuntu 18.4
@@ -651,10 +693,22 @@ class MyTests(unittest.TestCase):
def test_ping_ip_osx_10_14_6(self):
"""
Test 'ping6 <ip>' on osx 10.14.6
Test 'ping <ip>' on osx 10.14.6
"""
self.assertEqual(jc.parsers.ping.parse(self.osx_10_14_6_ping_ip, quiet=True), self.osx_10_14_6_ping_ip_json)
def test_ping_ip_unreachable_osx_10_14_6(self):
"""
Test 'ping <ip>' with host unreachable error on osx 10.14.6
"""
self.assertEqual(jc.parsers.ping.parse(self.osx_10_14_6_ping_ip_unreachable, quiet=True), self.osx_10_14_6_ping_ip_unreachable_json)
def test_ping_ip_unknown_errors_osx_10_14_6(self):
"""
Test 'ping <ip>' with unknown/unparsable errors on osx 10.14.6
"""
self.assertEqual(jc.parsers.ping.parse(self.osx_10_14_6_ping_ip_unknown_errors, quiet=True), self.osx_10_14_6_ping_ip_unknown_errors_json)
def test_ping6_hostname_p_osx_10_14_6(self):
"""
Test 'ping6 <hostname> -p' on osx 10.14.6
@@ -691,6 +745,12 @@ class MyTests(unittest.TestCase):
"""
self.assertEqual(jc.parsers.ping.parse(self.osx_10_14_6_ping6_ip, quiet=True), self.osx_10_14_6_ping6_ip_json)
def test_ping6_ip_unparsable_osx_10_14_6(self):
"""
Test 'ping6 <ip>' with unparsable lines on osx 10.14.6
"""
self.assertEqual(jc.parsers.ping.parse(self.osx_10_14_6_ping6_ip_unparsable, quiet=True), self.osx_10_14_6_ping6_ip_unparsable_json)
def test_ping_ip_dup_osx_10_14_6(self):
"""
Test 'ping <ip>' to broadcast IP to get duplicate replies on osx 10.14.6

94
tests/test_ufw.py Normal file
View File

@@ -0,0 +1,94 @@
import os
import json
import unittest
import jc.parsers.ufw
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
class MyTests(unittest.TestCase):
def setUp(self):
# input
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-18.04/ufw-verbose.out'), 'r', encoding='utf-8') as f:
self.ubuntu_18_04_ufw_verbose = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-18.04/ufw-numbered.out'), 'r', encoding='utf-8') as f:
self.ubuntu_18_04_ufw_numbered = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw.out'), 'r', encoding='utf-8') as f:
self.generic_ufw = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-numbered.out'), 'r', encoding='utf-8') as f:
self.generic_ufw_numbered = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-numbered2.out'), 'r', encoding='utf-8') as f:
self.generic_ufw_numbered2 = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-inactive.out'), 'r', encoding='utf-8') as f:
self.generic_ufw_inactive = f.read()
# output
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-18.04/ufw-verbose.json'), 'r', encoding='utf-8') as f:
self.ubuntu_18_04_ufw_verbose_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-18.04/ufw-numbered.json'), 'r', encoding='utf-8') as f:
self.ubuntu_18_04_ufw_numbered_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw.json'), 'r', encoding='utf-8') as f:
self.generic_ufw_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-numbered.json'), 'r', encoding='utf-8') as f:
self.generic_ufw_numbered_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-numbered2.json'), 'r', encoding='utf-8') as f:
self.generic_ufw_numbered2_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-inactive.json'), 'r', encoding='utf-8') as f:
self.generic_ufw_inactive_json = json.loads(f.read())
def test_ufw_nodata(self):
"""
Test 'ufw' with no data
"""
self.assertEqual(jc.parsers.ufw.parse('', quiet=True), {})
def test_ufw_ubuntu_18_04_verbose(self):
"""
Test 'ufw status verbose' on Ubuntu 18.04
"""
self.assertEqual(jc.parsers.ufw.parse(self.ubuntu_18_04_ufw_verbose, quiet=True), self.ubuntu_18_04_ufw_verbose_json)
def test_ufw_ubuntu_18_04_numbered(self):
"""
Test 'ufw status numbered' on Ubuntu 18.04
"""
self.assertEqual(jc.parsers.ufw.parse(self.ubuntu_18_04_ufw_numbered, quiet=True), self.ubuntu_18_04_ufw_numbered_json)
def test_ufw_generic_verbose(self):
"""
Test 'ufw status verbose' sample
"""
self.assertEqual(jc.parsers.ufw.parse(self.generic_ufw, quiet=True), self.generic_ufw_json)
def test_ufw_generic_verbose_numbered(self):
"""
Test 'ufw status verbose numbered' sample
"""
self.assertEqual(jc.parsers.ufw.parse(self.generic_ufw_numbered, quiet=True), self.generic_ufw_numbered_json)
def test_ufw_generic_verbose_numbered2(self):
"""
Test 'ufw status verbose numbered' sample
"""
self.assertEqual(jc.parsers.ufw.parse(self.generic_ufw_numbered2, quiet=True), self.generic_ufw_numbered2_json)
def test_ufw_generic_inactive(self):
"""
Test 'ufw status' when firewall is inactive
"""
self.assertEqual(jc.parsers.ufw.parse(self.generic_ufw_inactive, quiet=True), self.generic_ufw_inactive_json)
if __name__ == '__main__':
unittest.main()

82
tests/test_ufw_appinfo.py Normal file
View File

@@ -0,0 +1,82 @@
import os
import json
import unittest
import jc.parsers.ufw_appinfo
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
class MyTests(unittest.TestCase):
def setUp(self):
# input
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-18.04/ufw-appinfo-all.out'), 'r', encoding='utf-8') as f:
self.ubuntu_18_04_ufw_appinfo_all = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-appinfo-test.out'), 'r', encoding='utf-8') as f:
self.generic_ufw_appinfo_test = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-appinfo-test2.out'), 'r', encoding='utf-8') as f:
self.generic_ufw_appinfo_test2 = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-appinfo-test3.out'), 'r', encoding='utf-8') as f:
self.generic_ufw_appinfo_test3 = f.read()
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-appinfo-msn.out'), 'r', encoding='utf-8') as f:
self.generic_ufw_appinfo_msn = f.read()
# output
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/ubuntu-18.04/ufw-appinfo-all.json'), 'r', encoding='utf-8') as f:
self.ubuntu_18_04_ufw_appinfo_all_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-appinfo-test.json'), 'r', encoding='utf-8') as f:
self.generic_ufw_appinfo_test_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-appinfo-test2.json'), 'r', encoding='utf-8') as f:
self.generic_ufw_appinfo_test2_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-appinfo-test3.json'), 'r', encoding='utf-8') as f:
self.generic_ufw_appinfo_test3_json = json.loads(f.read())
with open(os.path.join(THIS_DIR, os.pardir, 'tests/fixtures/generic/ufw-appinfo-msn.json'), 'r', encoding='utf-8') as f:
self.generic_ufw_appinfo_msn_json = json.loads(f.read())
def test_ufw_appinfo_nodata(self):
"""
Test 'ufw_appinfo' with no data
"""
self.assertEqual(jc.parsers.ufw_appinfo.parse('', quiet=True), [])
def test_ufw_appinfo_ubuntu_18_04_all(self):
"""
Test 'ufw app info all' on Ubuntu 18.04
"""
self.assertEqual(jc.parsers.ufw_appinfo.parse(self.ubuntu_18_04_ufw_appinfo_all, quiet=True), self.ubuntu_18_04_ufw_appinfo_all_json)
def test_ufw_appinfo_generic_test(self):
"""
Test 'ufw app info [application]' sample
"""
self.assertEqual(jc.parsers.ufw_appinfo.parse(self.generic_ufw_appinfo_test, quiet=True), self.generic_ufw_appinfo_test_json)
def test_ufw_appinfo_generic_test2(self):
"""
Test 'ufw app info [application]' sample
"""
self.assertEqual(jc.parsers.ufw_appinfo.parse(self.generic_ufw_appinfo_test2, quiet=True), self.generic_ufw_appinfo_test2_json)
def test_ufw_appinfo_generic_test3(self):
"""
Test 'ufw app info [application]' sample
"""
self.assertEqual(jc.parsers.ufw_appinfo.parse(self.generic_ufw_appinfo_test3, quiet=True), self.generic_ufw_appinfo_test3_json)
def test_ufw_appinfo_generic_msn(self):
"""
Test 'ufw app info MSN' sample
"""
self.assertEqual(jc.parsers.ufw_appinfo.parse(self.generic_ufw_appinfo_msn, quiet=True), self.generic_ufw_appinfo_msn_json)
if __name__ == '__main__':
unittest.main()

View File

@@ -17,6 +17,10 @@ class MyTests(unittest.TestCase):
# Windows english format wint non-UTC tz (found in systeminfo cli output)
'3/22/2021, 1:15:51 PM (UTC-0600)': {'string': '3/22/2021, 1:15:51 PM (UTC-0600)', 'format': 1700, 'naive': 1616444151, 'utc': None},
# Windows english format with UTC tz (found in systeminfo cli output)
'3/22/2021, 1:15:51 PM (UTC)': {'string': '3/22/2021, 1:15:51 PM (UTC)', 'format': 1705, 'naive': 1616444151, 'utc': 1616418951},
# Windows english format with UTC tz in long-form (found in systeminfo cli output)
'3/22/2021, 1:15:51 PM (Coordinated Universal Time)': {'string': '3/22/2021, 1:15:51 PM (Coordinated Universal Time)', 'format': 1705, 'naive': 1616444151, 'utc': 1616418951},
# Windows english format with UTC tz (found in systeminfo cli output)
'3/22/2021, 1:15:51 PM (UTC+0000)': {'string': '3/22/2021, 1:15:51 PM (UTC+0000)', 'format': 1710, 'naive': 1616444151, 'utc': 1616418951},
# en_US.UTF-8 local format (found in upower cli output)
'Tue 23 Mar 2021 04:12:11 PM UTC': {'string': 'Tue 23 Mar 2021 04:12:11 PM UTC', 'format': 2000, 'naive': 1616541131, 'utc': 1616515931},
@@ -45,7 +49,7 @@ class MyTests(unittest.TestCase):
for input_string, expected_output in datetime_map.items():
self.assertEqual(jc.utils.timestamp(input_string).__dict__, expected_output)
def test_convert_to_int(self):
def test_utils_convert_to_int(self):
io_map = {
None: None,
True: 1,
@@ -72,7 +76,7 @@ class MyTests(unittest.TestCase):
for input_string, expected_output in io_map.items():
self.assertEqual(jc.utils.convert_to_int(input_string), expected_output)
def test_convert_to_float(self):
def test_utils_convert_to_float(self):
io_map = {
None: None,
True: 1.0,
@@ -99,7 +103,7 @@ class MyTests(unittest.TestCase):
for input_string, expected_output in io_map.items():
self.assertEqual(jc.utils.convert_to_float(input_string), expected_output)
def test_convert_to_bool(self):
def test_utils_convert_to_bool(self):
io_map = {
None: False,
True: True,