├── .gitchangelog.rc ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── dockerfiles ├── Dockerfile.kismet-app ├── Dockerfile.kismet-app-master ├── Dockerfile.kismet-rest_2.7 ├── Dockerfile.kismet-rest_3.7 └── Dockerfile.kismet-rest_ubu16 ├── docs ├── Makefile └── source │ ├── abstractions.rst │ ├── alerts.rst │ ├── conf.py │ ├── datasources.rst │ ├── devices.rst │ ├── gps.rst │ ├── index.rst │ ├── legacy.rst │ ├── messages.rst │ └── system.rst ├── kismet_rest ├── __init__.py ├── alerts.py ├── base_interface.py ├── datasources.py ├── devices.py ├── exceptions.py ├── gps.py ├── legacy.py ├── logger.py ├── messages.py ├── packetchain.py ├── system.py └── utility.py ├── run_all_tests.sh ├── setup.py └── tests ├── __init__.py ├── integration ├── test_integration_alerts.py ├── test_integration_base_interface.py ├── test_integration_datasources.py ├── test_integration_devices.py ├── test_integration_legacy.py ├── test_integration_messages.py └── test_integration_system.py └── unit ├── __init__.py ├── test_unit_logger.py └── test_unit_utility.py /.gitchangelog.rc: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | ## 5 | ## Format 6 | ## 7 | ## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] 8 | ## 9 | ## Description 10 | ## 11 | ## ACTION is one of 'chg', 'fix', 'new' 12 | ## 13 | ## Is WHAT the change is about. 14 | ## 15 | ## 'chg' is for refactor, small improvement, cosmetic changes... 16 | ## 'fix' is for bug fixes 17 | ## 'new' is for new features, big improvement 18 | ## 19 | ## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' 20 | ## 21 | ## Is WHO is concerned by the change. 22 | ## 23 | ## 'dev' is for developpers (API changes, refactors...) 24 | ## 'usr' is for final users (UI changes) 25 | ## 'pkg' is for packagers (packaging changes) 26 | ## 'test' is for testers (test only related changes) 27 | ## 'doc' is for doc guys (doc only changes) 28 | ## 29 | ## COMMIT_MSG is ... well ... the commit message itself. 30 | ## 31 | ## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic' 32 | ## 33 | ## They are preceded with a '!' or a '@' (prefer the former, as the 34 | ## latter is wrongly interpreted in github.) Commonly used tags are: 35 | ## 36 | ## 'refactor' is obviously for refactoring code only 37 | ## 'minor' is for a very meaningless change (a typo, adding a comment) 38 | ## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) 39 | ## 'wip' is for partial functionality but complete subfunctionality. 40 | ## 41 | ## Example: 42 | ## 43 | ## new: usr: support of bazaar implemented 44 | ## chg: re-indentend some lines !cosmetic 45 | ## new: dev: updated code to be compatible with last version of killer lib. 46 | ## fix: pkg: updated year of licence coverage. 47 | ## new: test: added a bunch of test around user usability of feature X. 48 | ## fix: typo in spelling my name in comment. !minor 49 | ## 50 | ## Please note that multi-line commit message are supported, and only the 51 | ## first line will be considered as the "summary" of the commit message. So 52 | ## tags, and other rules only applies to the summary. The body of the commit 53 | ## message will be displayed in the changelog without reformatting. 54 | 55 | 56 | ## This is where we determine the current version, used for labeling the 'unreleased version' 57 | 58 | def read(fname): 59 | return open(os.path.join(os.path.dirname('__file__'), fname)).read() 60 | 61 | def get_version(): 62 | raw_init_file = read("kismet_rest/__init__.py") 63 | rx_compiled = re.compile(r"\s*__version__\s*=\s*\"(\S+)\"") 64 | ver = rx_compiled.search(raw_init_file).group(1) 65 | return ver 66 | 67 | ## 68 | ## ``ignore_regexps`` is a line of regexps 69 | ## 70 | ## Any commit having its full commit message matching any regexp listed here 71 | ## will be ignored and won't be reported in the changelog. 72 | ## 73 | ignore_regexps = [ 74 | r'@minor', r'!minor', 75 | r'@cosmetic', r'!cosmetic', 76 | r'@refactor', r'!refactor', 77 | r'@wip', r'!wip', 78 | r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:', 79 | # r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:', 80 | r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$', 81 | ] 82 | 83 | 84 | ## ``section_regexps`` is a list of 2-tuples associating a string label and a 85 | ## list of regexp 86 | ## 87 | ## Commit messages will be classified in sections thanks to this. Section 88 | ## titles are the label, and a commit is classified under this section if any 89 | ## of the regexps associated is matching. 90 | ## 91 | section_regexps = [ 92 | ('New', [ 93 | r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 94 | ]), 95 | ('Changes', [ 96 | r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 97 | ]), 98 | ('Fix', [ 99 | r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', 100 | ]), 101 | 102 | ('Other', None ## Match all lines 103 | ), 104 | 105 | ] 106 | 107 | 108 | ## ``body_process`` is a callable 109 | ## 110 | ## This callable will be given the original body and result will 111 | ## be used in the changelog. 112 | ## 113 | ## Available constructs are: 114 | ## 115 | ## - any python callable that take one txt argument and return txt argument. 116 | ## 117 | ## - ReSub(pattern, replacement): will apply regexp substitution. 118 | ## 119 | ## - Indent(chars=" "): will indent the text with the prefix 120 | ## Please remember that template engines gets also to modify the text and 121 | ## will usually indent themselves the text if needed. 122 | ## 123 | ## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns 124 | ## 125 | ## - noop: do nothing 126 | ## 127 | ## - ucfirst: ensure the first letter is uppercase. 128 | ## (usually used in the ``subject_process`` pipeline) 129 | ## 130 | ## - final_dot: ensure text finishes with a dot 131 | ## (usually used in the ``subject_process`` pipeline) 132 | ## 133 | ## - strip: remove any spaces before or after the content of the string 134 | ## 135 | ## Additionally, you can `pipe` the provided filters, for instance: 136 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ") 137 | #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') 138 | #body_process = noop 139 | body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip 140 | 141 | 142 | ## ``subject_process`` is a callable 143 | ## 144 | ## This callable will be given the original subject and result will 145 | ## be used in the changelog. 146 | ## 147 | ## Available constructs are those listed in ``body_process`` doc. 148 | subject_process = (strip | 149 | ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') | 150 | ucfirst | final_dot) 151 | 152 | 153 | ## ``tag_filter_regexp`` is a regexp 154 | ## 155 | ## Tags that will be used for the changelog must match this regexp. 156 | ## 157 | tag_filter_regexp = r'\d+(\.|-)\d+(\.|-)\d+$' 158 | 159 | 160 | ## ``unreleased_version_label`` is a string 161 | ## 162 | ## This label will be used as the changelog Title of the last set of changes 163 | ## between last valid tag and HEAD if any. 164 | # unreleased_version_label = "%%version%% (unreleased)" 165 | unreleased_version_label = "v%s"% get_version() 166 | 167 | ## ``output_engine`` is a callable 168 | ## 169 | ## This will change the output format of the generated changelog file 170 | ## 171 | ## Available choices are: 172 | ## 173 | ## - rest_py 174 | ## 175 | ## Legacy pure python engine, outputs ReSTructured text. 176 | ## This is the default. 177 | ## 178 | ## - mustache() 179 | ## 180 | ## Template name could be any of the available templates in 181 | ## ``templates/mustache/*.tpl``. 182 | ## Requires python package ``pystache``. 183 | ## Examples: 184 | ## - mustache("markdown") 185 | ## - mustache("restructuredtext") 186 | ## 187 | ## - makotemplate() 188 | ## 189 | ## Template name could be any of the available templates in 190 | ## ``templates/mako/*.tpl``. 191 | ## Requires python package ``mako``. 192 | ## Examples: 193 | ## - makotemplate("restructuredtext") 194 | ## 195 | #output_engine = rest_py 196 | #output_engine = mustache("restructuredtext") 197 | #output_engine = mustache("markdown") 198 | #output_engine = makotemplate("restructuredtext") 199 | 200 | 201 | ## ``include_merge`` is a boolean 202 | ## 203 | ## This option tells git-log whether to include merge commits in the log. 204 | ## The default is to include them. 205 | include_merge = False 206 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python compiled code 2 | *.pyc 3 | /*.egg-info/ 4 | /build/ 5 | /dist/ 6 | docs/build/ 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | python: 2 | version: 3.7 3 | install: 4 | - method: pip 5 | path: . 6 | system_packages: true 7 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | v2020.05.01 5 | ----------- 6 | 7 | Changes 8 | ~~~~~~~ 9 | - Add support for datasource open/close [ohad83] 10 | - Switch to itjson endpoints [Mike Kershaw / Dragorn] 11 | 12 | v2019.05.02 13 | ----------- 14 | 15 | Changes 16 | ~~~~~~~ 17 | - Support Python 3.5. [ashmastaflash] 18 | - Add MANIFEST.in. [ashmastaflash] 19 | 20 | 21 | 2019.05.01 (2019-05-20) 22 | ----------------------- 23 | 24 | New 25 | ~~~ 26 | - Refactor complete. [ashmastaflash] 27 | 28 | Closes #1 29 | 30 | Changes 31 | ~~~~~~~ 32 | - Add developer notes to README.rst. [ashmastaflash] 33 | - Add configs for gitchangelog and rtd. [ashmastaflash] 34 | 35 | Other 36 | ~~~~~ 37 | - Update docs. [Mike Kershaw / Dragorn] 38 | - Start extracting module. [Mike Kershaw / Dragorn] 39 | - Started repo. [Mike Kershaw / Dragorn] 40 | 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ** UNLESS OTHERWISE NOTED IN THE FILE OR DIRECTORY, KISMET 2 | IS RELEASED UNDER THE GPL2 LICENSE 3 | 4 | SPECIFIC SUBCOMPONENTS AND FILES MAY BE UNDER LESS RESTRICTIVE 5 | LICENSES. THESE DIFFERENCES ARE NOTED IN THE FILE OR IN THE 6 | LICENSE FILE OF THOSE SUBDIRECTORIES ** 7 | 8 | 9 | GNU GENERAL PUBLIC LICENSE 10 | Version 2, June 1991 11 | 12 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 13 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 14 | Everyone is permitted to copy and distribute verbatim copies 15 | of this license document, but changing it is not allowed. 16 | 17 | Preamble 18 | 19 | The licenses for most software are designed to take away your 20 | freedom to share and change it. By contrast, the GNU General Public 21 | License is intended to guarantee your freedom to share and change free 22 | software--to make sure the software is free for all its users. This 23 | General Public License applies to most of the Free Software 24 | Foundation's software and to any other program whose authors commit to 25 | using it. (Some other Free Software Foundation software is covered by 26 | the GNU Lesser General Public License instead.) You can apply it to 27 | your programs, too. 28 | 29 | When we speak of free software, we are referring to freedom, not 30 | price. Our General Public Licenses are designed to make sure that you 31 | have the freedom to distribute copies of free software (and charge for 32 | this service if you wish), that you receive source code or can get it 33 | if you want it, that you can change the software or use pieces of it 34 | in new free programs; and that you know you can do these things. 35 | 36 | To protect your rights, we need to make restrictions that forbid 37 | anyone to deny you these rights or to ask you to surrender the rights. 38 | These restrictions translate to certain responsibilities for you if you 39 | distribute copies of the software, or if you modify it. 40 | 41 | For example, if you distribute copies of such a program, whether 42 | gratis or for a fee, you must give the recipients all the rights that 43 | you have. You must make sure that they, too, receive or can get the 44 | source code. And you must show them these terms so they know their 45 | rights. 46 | 47 | We protect your rights with two steps: (1) copyright the software, and 48 | (2) offer you this license which gives you legal permission to copy, 49 | distribute and/or modify the software. 50 | 51 | Also, for each author's protection and ours, we want to make certain 52 | that everyone understands that there is no warranty for this free 53 | software. If the software is modified by someone else and passed on, we 54 | want its recipients to know that what they have is not the original, so 55 | that any problems introduced by others will not reflect on the original 56 | authors' reputations. 57 | 58 | Finally, any free program is threatened constantly by software 59 | patents. We wish to avoid the danger that redistributors of a free 60 | program will individually obtain patent licenses, in effect making the 61 | program proprietary. To prevent this, we have made it clear that any 62 | patent must be licensed for everyone's free use or not licensed at all. 63 | 64 | The precise terms and conditions for copying, distribution and 65 | modification follow. 66 | 67 | GNU GENERAL PUBLIC LICENSE 68 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 69 | 70 | 0. This License applies to any program or other work which contains 71 | a notice placed by the copyright holder saying it may be distributed 72 | under the terms of this General Public License. The "Program", below, 73 | refers to any such program or work, and a "work based on the Program" 74 | means either the Program or any derivative work under copyright law: 75 | that is to say, a work containing the Program or a portion of it, 76 | either verbatim or with modifications and/or translated into another 77 | language. (Hereinafter, translation is included without limitation in 78 | the term "modification".) Each licensee is addressed as "you". 79 | 80 | Activities other than copying, distribution and modification are not 81 | covered by this License; they are outside its scope. The act of 82 | running the Program is not restricted, and the output from the Program 83 | is covered only if its contents constitute a work based on the 84 | Program (independent of having been made by running the Program). 85 | Whether that is true depends on what the Program does. 86 | 87 | 1. You may copy and distribute verbatim copies of the Program's 88 | source code as you receive it, in any medium, provided that you 89 | conspicuously and appropriately publish on each copy an appropriate 90 | copyright notice and disclaimer of warranty; keep intact all the 91 | notices that refer to this License and to the absence of any warranty; 92 | and give any other recipients of the Program a copy of this License 93 | along with the Program. 94 | 95 | You may charge a fee for the physical act of transferring a copy, and 96 | you may at your option offer warranty protection in exchange for a fee. 97 | 98 | 2. You may modify your copy or copies of the Program or any portion 99 | of it, thus forming a work based on the Program, and copy and 100 | distribute such modifications or work under the terms of Section 1 101 | above, provided that you also meet all of these conditions: 102 | 103 | a) You must cause the modified files to carry prominent notices 104 | stating that you changed the files and the date of any change. 105 | 106 | b) You must cause any work that you distribute or publish, that in 107 | whole or in part contains or is derived from the Program or any 108 | part thereof, to be licensed as a whole at no charge to all third 109 | parties under the terms of this License. 110 | 111 | c) If the modified program normally reads commands interactively 112 | when run, you must cause it, when started running for such 113 | interactive use in the most ordinary way, to print or display an 114 | announcement including an appropriate copyright notice and a 115 | notice that there is no warranty (or else, saying that you provide 116 | a warranty) and that users may redistribute the program under 117 | these conditions, and telling the user how to view a copy of this 118 | License. (Exception: if the Program itself is interactive but 119 | does not normally print such an announcement, your work based on 120 | the Program is not required to print an announcement.) 121 | 122 | These requirements apply to the modified work as a whole. If 123 | identifiable sections of that work are not derived from the Program, 124 | and can be reasonably considered independent and separate works in 125 | themselves, then this License, and its terms, do not apply to those 126 | sections when you distribute them as separate works. But when you 127 | distribute the same sections as part of a whole which is a work based 128 | on the Program, the distribution of the whole must be on the terms of 129 | this License, whose permissions for other licensees extend to the 130 | entire whole, and thus to each and every part regardless of who wrote it. 131 | 132 | Thus, it is not the intent of this section to claim rights or contest 133 | your rights to work written entirely by you; rather, the intent is to 134 | exercise the right to control the distribution of derivative or 135 | collective works based on the Program. 136 | 137 | In addition, mere aggregation of another work not based on the Program 138 | with the Program (or with a work based on the Program) on a volume of 139 | a storage or distribution medium does not bring the other work under 140 | the scope of this License. 141 | 142 | 3. You may copy and distribute the Program (or a work based on it, 143 | under Section 2) in object code or executable form under the terms of 144 | Sections 1 and 2 above provided that you also do one of the following: 145 | 146 | a) Accompany it with the complete corresponding machine-readable 147 | source code, which must be distributed under the terms of Sections 148 | 1 and 2 above on a medium customarily used for software interchange; or, 149 | 150 | b) Accompany it with a written offer, valid for at least three 151 | years, to give any third party, for a charge no more than your 152 | cost of physically performing source distribution, a complete 153 | machine-readable copy of the corresponding source code, to be 154 | distributed under the terms of Sections 1 and 2 above on a medium 155 | customarily used for software interchange; or, 156 | 157 | c) Accompany it with the information you received as to the offer 158 | to distribute corresponding source code. (This alternative is 159 | allowed only for noncommercial distribution and only if you 160 | received the program in object code or executable form with such 161 | an offer, in accord with Subsection b above.) 162 | 163 | The source code for a work means the preferred form of the work for 164 | making modifications to it. For an executable work, complete source 165 | code means all the source code for all modules it contains, plus any 166 | associated interface definition files, plus the scripts used to 167 | control compilation and installation of the executable. However, as a 168 | special exception, the source code distributed need not include 169 | anything that is normally distributed (in either source or binary 170 | form) with the major components (compiler, kernel, and so on) of the 171 | operating system on which the executable runs, unless that component 172 | itself accompanies the executable. 173 | 174 | If distribution of executable or object code is made by offering 175 | access to copy from a designated place, then offering equivalent 176 | access to copy the source code from the same place counts as 177 | distribution of the source code, even though third parties are not 178 | compelled to copy the source along with the object code. 179 | 180 | 4. You may not copy, modify, sublicense, or distribute the Program 181 | except as expressly provided under this License. Any attempt 182 | otherwise to copy, modify, sublicense or distribute the Program is 183 | void, and will automatically terminate your rights under this License. 184 | However, parties who have received copies, or rights, from you under 185 | this License will not have their licenses terminated so long as such 186 | parties remain in full compliance. 187 | 188 | 5. You are not required to accept this License, since you have not 189 | signed it. However, nothing else grants you permission to modify or 190 | distribute the Program or its derivative works. These actions are 191 | prohibited by law if you do not accept this License. Therefore, by 192 | modifying or distributing the Program (or any work based on the 193 | Program), you indicate your acceptance of this License to do so, and 194 | all its terms and conditions for copying, distributing or modifying 195 | the Program or works based on it. 196 | 197 | 6. Each time you redistribute the Program (or any work based on the 198 | Program), the recipient automatically receives a license from the 199 | original licensor to copy, distribute or modify the Program subject to 200 | these terms and conditions. You may not impose any further 201 | restrictions on the recipients' exercise of the rights granted herein. 202 | You are not responsible for enforcing compliance by third parties to 203 | this License. 204 | 205 | 7. If, as a consequence of a court judgment or allegation of patent 206 | infringement or for any other reason (not limited to patent issues), 207 | conditions are imposed on you (whether by court order, agreement or 208 | otherwise) that contradict the conditions of this License, they do not 209 | excuse you from the conditions of this License. If you cannot 210 | distribute so as to satisfy simultaneously your obligations under this 211 | License and any other pertinent obligations, then as a consequence you 212 | may not distribute the Program at all. For example, if a patent 213 | license would not permit royalty-free redistribution of the Program by 214 | all those who receive copies directly or indirectly through you, then 215 | the only way you could satisfy both it and this License would be to 216 | refrain entirely from distribution of the Program. 217 | 218 | If any portion of this section is held invalid or unenforceable under 219 | any particular circumstance, the balance of the section is intended to 220 | apply and the section as a whole is intended to apply in other 221 | circumstances. 222 | 223 | It is not the purpose of this section to induce you to infringe any 224 | patents or other property right claims or to contest validity of any 225 | such claims; this section has the sole purpose of protecting the 226 | integrity of the free software distribution system, which is 227 | implemented by public license practices. Many people have made 228 | generous contributions to the wide range of software distributed 229 | through that system in reliance on consistent application of that 230 | system; it is up to the author/donor to decide if he or she is willing 231 | to distribute software through any other system and a licensee cannot 232 | impose that choice. 233 | 234 | This section is intended to make thoroughly clear what is believed to 235 | be a consequence of the rest of this License. 236 | 237 | 8. If the distribution and/or use of the Program is restricted in 238 | certain countries either by patents or by copyrighted interfaces, the 239 | original copyright holder who places the Program under this License 240 | may add an explicit geographical distribution limitation excluding 241 | those countries, so that distribution is permitted only in or among 242 | countries not thus excluded. In such case, this License incorporates 243 | the limitation as if written in the body of this License. 244 | 245 | 9. The Free Software Foundation may publish revised and/or new versions 246 | of the General Public License from time to time. Such new versions will 247 | be similar in spirit to the present version, but may differ in detail to 248 | address new problems or concerns. 249 | 250 | Each version is given a distinguishing version number. If the Program 251 | specifies a version number of this License which applies to it and "any 252 | later version", you have the option of following the terms and conditions 253 | either of that version or of any later version published by the Free 254 | Software Foundation. If the Program does not specify a version number of 255 | this License, you may choose any version ever published by the Free Software 256 | Foundation. 257 | 258 | 10. If you wish to incorporate parts of the Program into other free 259 | programs whose distribution conditions are different, write to the author 260 | to ask for permission. For software which is copyrighted by the Free 261 | Software Foundation, write to the Free Software Foundation; we sometimes 262 | make exceptions for this. Our decision will be guided by the two goals 263 | of preserving the free status of all derivatives of our free software and 264 | of promoting the sharing and reuse of software generally. 265 | 266 | NO WARRANTY 267 | 268 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 269 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 270 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 271 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 272 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 273 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 274 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 275 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 276 | REPAIR OR CORRECTION. 277 | 278 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 279 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 280 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 281 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 282 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 283 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 284 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 285 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 286 | POSSIBILITY OF SUCH DAMAGES. 287 | 288 | END OF TERMS AND CONDITIONS 289 | 290 | How to Apply These Terms to Your New Programs 291 | 292 | If you develop a new program, and you want it to be of the greatest 293 | possible use to the public, the best way to achieve this is to make it 294 | free software which everyone can redistribute and change under these terms. 295 | 296 | To do so, attach the following notices to the program. It is safest 297 | to attach them to the start of each source file to most effectively 298 | convey the exclusion of warranty; and each file should have at least 299 | the "copyright" line and a pointer to where the full notice is found. 300 | 301 | 302 | Copyright (C) 303 | 304 | This program is free software; you can redistribute it and/or modify 305 | it under the terms of the GNU General Public License as published by 306 | the Free Software Foundation; either version 2 of the License, or 307 | (at your option) any later version. 308 | 309 | This program is distributed in the hope that it will be useful, 310 | but WITHOUT ANY WARRANTY; without even the implied warranty of 311 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 312 | GNU General Public License for more details. 313 | 314 | You should have received a copy of the GNU General Public License along 315 | with this program; if not, write to the Free Software Foundation, Inc., 316 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 317 | 318 | Also add information on how to contact you by electronic and paper mail. 319 | 320 | If the program is interactive, make it output a short notice like this 321 | when it starts in an interactive mode: 322 | 323 | Gnomovision version 69, Copyright (C) year name of author 324 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 325 | This is free software, and you are welcome to redistribute it 326 | under certain conditions; type `show c' for details. 327 | 328 | The hypothetical commands `show w' and `show c' should show the appropriate 329 | parts of the General Public License. Of course, the commands you use may 330 | be called something other than `show w' and `show c'; they could even be 331 | mouse-clicks or menu items--whatever suits your program. 332 | 333 | You should also get your employer (if you work as a programmer) or your 334 | school, if any, to sign a "copyright disclaimer" for the program, if 335 | necessary. Here is a sample; alter the names: 336 | 337 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 338 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 339 | 340 | , 1 April 1989 341 | Ty Coon, President of Vice 342 | 343 | This General Public License does not permit incorporating your program into 344 | proprietary programs. If your program is a subroutine library, you may 345 | consider it more useful to permit linking proprietary applications with the 346 | library. If this is what you want to do, use the GNU Lesser General 347 | Public License instead of this License. 348 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | kismet_rest 2 | =========== 3 | 4 | Python wrapper for Kismet REST interface. 5 | 6 | .. image:: https://readthedocs.org/projects/kismet-rest/badge/?version=latest 7 | :target: https://kismet-rest.readthedocs.io/en/latest/?badge=latest 8 | :alt: Documentation Status 9 | 10 | 11 | Installing from PyPI 12 | ---------------------- 13 | 14 | :: 15 | 16 | pip install kismet_rest 17 | 18 | 19 | Installing from source 20 | ---------------------- 21 | 22 | :: 23 | 24 | git clone https://github.com/kismetwireless/python-kismet-rest 25 | cd python-kismet-rest && pip install . 26 | 27 | 28 | Usage examples 29 | -------------- 30 | 31 | 32 | Legacy functionality (KismetConnector): 33 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 34 | 35 | 36 | :: 37 | 38 | import kismet_rest 39 | conn = kismet_rest.KismetConnector(username="my_user", password="my_pass") 40 | for device in conn.device_summary(): 41 | pprint.pprint(device) 42 | 43 | 44 | Alerts since 2019-01-01: 45 | ~~~~~~~~~~~~~~~~~~~~~~~~ 46 | 47 | :: 48 | 49 | import kismet_rest 50 | alerts = kismet_rest.Alerts() 51 | for alert in alerts.all(ts_sec=1546300800): 52 | print(alert) 53 | 54 | 55 | Devices last observed since 2019-01-01: 56 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | 58 | :: 59 | 60 | import kismet_rest 61 | devices = kismet_rest.Devices() 62 | for device in devices.all(ts=1546300800): 63 | print(device) 64 | 65 | 66 | Developer notes: 67 | ---------------- 68 | 69 | * Formatting commit messages: 70 | * Correctly-formatted commit messages will be organized in CHANGELOG.rst 71 | * Commit messages are formatted like this ``type: audience: message !tag`` 72 | * Type is for the type of change (``new``, ``chg``) 73 | * Audience is for the audience of the commit note(``usr``,``test``,``doc``) 74 | * The message part is pretty self-explanatory. 75 | * The optional tag allows you to flag a commit for exclusion from CHANGELOG.rst.(``minor`` or ``wip``) 76 | * A commit message like this: ``new: usr: Made a new widget.`` will appear in CHANGELOG.rst, under the appropriate release, under the "New" section. 77 | * More info on message formatting: https://github.com/vaab/gitchangelog 78 | * Updating CHANGELOG.rst: 79 | * Install gitchangelog: ``pip3 install gitchangelog`` 80 | * Make sure that ``__version__`` is correct in ``kismet_rest/__init__.py`` 81 | * Build the new changelog: ``gitchangelog > CHANGELOG.rst`` 82 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile.kismet-app: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y \ 7 | apt-transport-https \ 8 | gnupg \wget && \ 9 | wget -O - https://www.kismetwireless.net/repos/kismet-release.gpg.key | apt-key add - && \ 10 | echo 'deb https://www.kismetwireless.net/repos/apt/release/bionic bionic main' | tee /etc/apt/sources.list.d/kismet.list && \ 11 | apt-get update && \ 12 | apt-get install -y \ 13 | kismet-core-debug 14 | # kismet-capture-linux-bluetooth \ 15 | # kismet-capture-linux-wifi \ 16 | # kismet-capture-nrf-mousejack \ 17 | # python-kismetcapturertl433 \ 18 | # python-kismetexternal \ 19 | # strace 20 | 21 | EXPOSE 2501 22 | 23 | RUN echo "httpd_username=admin" >> /etc/kismet/kismet.conf 24 | RUN echo "httpd_password=passwordy" >> /etc/kismet/kismet.conf 25 | 26 | CMD kismet -c /export/kismet.pcap: 27 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile.kismet-app-master: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | WORKDIR /source 6 | 7 | RUN apt-get update && \ 8 | apt-get install -y \ 9 | build-essential \ 10 | git \ 11 | libmicrohttpd-dev \ 12 | pkg-config \ 13 | zlib1g-dev \ 14 | libnl-3-dev \ 15 | libnl-genl-3-dev \ 16 | libcap-dev \ 17 | libpcap-dev \ 18 | libnm-dev \ 19 | libdw-dev \ 20 | libsqlite3-dev \ 21 | libprotobuf-dev \ 22 | libprotobuf-c-dev \ 23 | protobuf-compiler \ 24 | protobuf-c-compiler \ 25 | libsensors4-dev \ 26 | python \ 27 | python-setuptools \ 28 | python-protobuf \ 29 | python-requests \ 30 | librtlsdr0 \ 31 | python-usb \ 32 | python-paho-mqtt \ 33 | libusb-1.0-0-dev 34 | 35 | RUN git clone https://www.kismetwireless.net/git/kismet.git 36 | 37 | WORKDIR /source/kismet 38 | 39 | RUN ./configure 40 | 41 | # RUN make suidinstall 42 | RUN make && make install 43 | 44 | # RUN usermod -aG kismet $USER 45 | 46 | EXPOSE 2501 47 | 48 | RUN find / -name kismet.conf 49 | 50 | RUN echo "httpd_username=admin" >> /usr/local/etc/kismet.conf 51 | RUN echo "httpd_password=passwordy" >> /usr/local/etc/kismet.conf 52 | 53 | CMD kismet -c /export/kismet.pcap: 54 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile.kismet-rest_2.7: -------------------------------------------------------------------------------- 1 | ARG PY_VER=2.7 2 | FROM python:${PY_VER} 3 | 4 | RUN pip install pytest pytest-coverage 5 | 6 | COPY . /src/ 7 | 8 | WORKDIR /src/ 9 | 10 | RUN pip install -e . 11 | 12 | CMD py.test --cov=kismet_rest --cov-report=term-missing 13 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile.kismet-rest_3.7: -------------------------------------------------------------------------------- 1 | ARG PY_VER=3.7 2 | FROM python:${PY_VER} 3 | 4 | RUN pip install pytest pytest-coverage 5 | 6 | COPY . /src/ 7 | 8 | WORKDIR /src/ 9 | 10 | RUN pip install -e . 11 | 12 | CMD py.test --cov=kismet_rest --cov-report=term-missing 13 | -------------------------------------------------------------------------------- /dockerfiles/Dockerfile.kismet-rest_ubu16: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y \ 5 | python3 \ 6 | python3-pip 7 | 8 | RUN pip3 install pytest pytest-coverage 9 | 10 | COPY . /src/ 11 | 12 | WORKDIR /src/ 13 | 14 | RUN pip3 install -e . 15 | 16 | CMD python3 -m pytest --cov=kismet_rest --cov-report=term-missing 17 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 20 | -------------------------------------------------------------------------------- /docs/source/abstractions.rst: -------------------------------------------------------------------------------- 1 | .. kismetdb documentation master file, created by 2 | sphinx-quickstart on Mon Jan 21 22:01:16 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Abstractions 7 | ============ 8 | 9 | This library presents endpoints as Python objects. 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: Tables: 14 | 15 | alerts 16 | datasources 17 | devices 18 | gps 19 | messages 20 | system 21 | -------------------------------------------------------------------------------- /docs/source/alerts.rst: -------------------------------------------------------------------------------- 1 | Alerts 2 | ====== 3 | 4 | .. toctree:: 5 | 6 | .. autoclass:: kismet_rest.Alerts 7 | :members: all, define, raise_alert 8 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import re 17 | # import sys 18 | # sys.path.insert(0, os.path.abspath('.')) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = u'kismet_rest' 24 | copyright = u'2019, Mike Kershaw / Dragorn' 25 | author = u'Mike Kershaw / Dragorn' 26 | 27 | 28 | def read_file(fname): 29 | """Return file contents as a string.""" 30 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 31 | 32 | 33 | def get_version(): 34 | """Return version from init file.""" 35 | raw_init_file = read_file("../../{}/__init__.py".format(project)) 36 | rx_compiled = re.compile(r"\s*__version__\s*=\s*\"(\S+)\"") 37 | ver = rx_compiled.search(raw_init_file).group(1) 38 | return ver 39 | 40 | 41 | # The short X.Y version 42 | version = get_version() 43 | # The full version, including alpha/beta/rc tags 44 | release = get_version() 45 | 46 | 47 | # -- General configuration --------------------------------------------------- 48 | 49 | # If your documentation needs a minimal Sphinx version, state it here. 50 | # 51 | # needs_sphinx = '1.0' 52 | 53 | # Add any Sphinx extension module names here, as strings. They can be 54 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 55 | # ones. 56 | extensions = [ 57 | 'sphinx.ext.autodoc', 58 | 'sphinx.ext.viewcode', 59 | 'sphinx.ext.napoleon' 60 | ] 61 | 62 | # Add any paths that contain templates here, relative to this directory. 63 | templates_path = ['_templates'] 64 | 65 | # The suffix(es) of source filenames. 66 | # You can specify multiple suffix as a list of string: 67 | # 68 | # source_suffix = ['.rst', '.md'] 69 | source_suffix = '.rst' 70 | 71 | # The master toctree document. 72 | master_doc = 'index' 73 | 74 | # The language for content autogenerated by Sphinx. Refer to documentation 75 | # for a list of supported languages. 76 | # 77 | # This is also used if you do content translation via gettext catalogs. 78 | # Usually you set "language" from the command line for these cases. 79 | language = None 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | # This pattern also affects html_static_path and html_extra_path. 84 | exclude_patterns = [] 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = None 88 | 89 | 90 | # -- Options for HTML output ------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | # 95 | html_theme = 'alabaster' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | # 101 | # html_theme_options = {} 102 | 103 | # Add any paths that contain custom static files (such as style sheets) here, 104 | # relative to this directory. They are copied after the builtin static files, 105 | # so a file named "default.css" will overwrite the builtin "default.css". 106 | html_static_path = ['_static'] 107 | 108 | # Custom sidebar templates, must be a dictionary that maps document names 109 | # to template names. 110 | # 111 | # The default sidebars (for documents that don't match any pattern) are 112 | # defined by theme itself. Builtin themes are using these templates by 113 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 114 | # 'searchbox.html']``. 115 | # 116 | # html_sidebars = {} 117 | 118 | 119 | # -- Options for HTMLHelp output --------------------------------------------- 120 | 121 | # Output file base name for HTML help builder. 122 | htmlhelp_basename = 'kismet_restdoc' 123 | 124 | 125 | # -- Options for LaTeX output ------------------------------------------------ 126 | 127 | latex_elements = { 128 | # The paper size ('letterpaper' or 'a4paper'). 129 | # 130 | # 'papersize': 'letterpaper', 131 | 132 | # The font size ('10pt', '11pt' or '12pt'). 133 | # 134 | # 'pointsize': '10pt', 135 | 136 | # Additional stuff for the LaTeX preamble. 137 | # 138 | # 'preamble': '', 139 | 140 | # Latex figure (float) alignment 141 | # 142 | # 'figure_align': 'htbp', 143 | } 144 | 145 | # Grouping the document tree into LaTeX files. List of tuples 146 | # (source start file, target name, title, 147 | # author, documentclass [howto, manual, or own class]). 148 | latex_documents = [ 149 | (master_doc, 'kismet_rest.tex', u'kismet\\_rest Documentation', 150 | u'Mike Kershaw / Dragorn', 'manual'), 151 | ] 152 | 153 | 154 | # -- Options for manual page output ------------------------------------------ 155 | 156 | # One entry per manual page. List of tuples 157 | # (source start file, name, description, authors, manual section). 158 | man_pages = [ 159 | (master_doc, 'kismet_rest', u'kismet_rest Documentation', 160 | [author], 1) 161 | ] 162 | 163 | 164 | # -- Options for Texinfo output ---------------------------------------------- 165 | 166 | # Grouping the document tree into Texinfo files. List of tuples 167 | # (source start file, target name, title, author, 168 | # dir menu entry, description, category) 169 | texinfo_documents = [ 170 | (master_doc, 'kismet_rest', u'kismet_rest Documentation', 171 | author, 'kismet_rest', 'One line description of project.', 172 | 'Miscellaneous'), 173 | ] 174 | 175 | 176 | # -- Options for Epub output ------------------------------------------------- 177 | 178 | # Bibliographic Dublin Core info. 179 | epub_title = project 180 | 181 | # The unique identifier of the text. This can be a ISBN number 182 | # or the project homepage. 183 | # 184 | # epub_identifier = '' 185 | 186 | # A unique identification for the text. 187 | # 188 | # epub_uid = '' 189 | 190 | # A list of files that should not be packed into the epub file. 191 | epub_exclude_files = ['search.html'] 192 | 193 | 194 | # -- Extension configuration ------------------------------------------------- 195 | -------------------------------------------------------------------------------- /docs/source/datasources.rst: -------------------------------------------------------------------------------- 1 | Datasources 2 | =========== 3 | 4 | .. toctree:: 5 | 6 | .. autoclass:: kismet_rest.Datasources 7 | :members: all, interfaces, set_channel, set_hop_rate, 8 | set_hop_channels, set_hop, add, pause, resume 9 | -------------------------------------------------------------------------------- /docs/source/devices.rst: -------------------------------------------------------------------------------- 1 | Devices 2 | ======= 3 | 4 | .. toctree:: 5 | 6 | .. autoclass:: kismet_rest.Devices 7 | :members: all, by_mac, by_key, dot11_clients_of, dot11_access_points 8 | -------------------------------------------------------------------------------- /docs/source/gps.rst: -------------------------------------------------------------------------------- 1 | GPS 2 | === 3 | 4 | .. toctree:: 5 | 6 | .. autoclass:: kismet_rest.GPS 7 | :members: current_location 8 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. kismet_rest documentation master file, created by 2 | sphinx-quickstart on Sun Apr 14 23:17:41 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. kismet_rest 7 | =========== 8 | 9 | .. include:: ../../README.rst 10 | 11 | 12 | 13 | Table of Contents 14 | ================= 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | 19 | abstractions 20 | legacy 21 | 22 | 23 | .. include:: ../../CHANGELOG.rst 24 | 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | -------------------------------------------------------------------------------- /docs/source/legacy.rst: -------------------------------------------------------------------------------- 1 | Legacy 2 | ======= 3 | 4 | .. toctree:: 5 | 6 | .. autoclass:: kismet_rest.KismetConnector 7 | :members: system_status, device_summary, device_list, 8 | device_summary_since, smart_summary_since, smart_device_list, 9 | device_list_by_mac, dot11_clients_of, dot11_access_points, device, 10 | device_field, device_by_key, device_by_mac, datasources, 11 | datasource_list_interfaces, config_datasource_set_channel, 12 | config_datasource_set_hop_rate, config_datasource_set_hop_channels, 13 | config_datasource_set_hop, add_datasource, define_alert, raise_alert, 14 | alerts, messages, location 15 | -------------------------------------------------------------------------------- /docs/source/messages.rst: -------------------------------------------------------------------------------- 1 | Messages 2 | ======== 3 | 4 | .. toctree:: 5 | 6 | .. autoclass:: kismet_rest.Messages 7 | :members: all 8 | -------------------------------------------------------------------------------- /docs/source/system.rst: -------------------------------------------------------------------------------- 1 | System 2 | ====== 3 | 4 | .. toctree:: 5 | 6 | .. autoclass:: kismet_rest.System 7 | :members: get_status, get_system_time 8 | -------------------------------------------------------------------------------- /kismet_rest/__init__.py: -------------------------------------------------------------------------------- 1 | """Kismet REST interface module. 2 | 3 | (c) 2018-2023 Mike Kershaw / Dragorn 4 | Licensed under GPL2 or above 5 | """ 6 | 7 | from .exceptions import KismetConnectorException # NOQA 8 | from .exceptions import KismetLoginException # NOQA 9 | from .exceptions import KismetRequestException # NOQA 10 | from .exceptions import KismetConnectionError # NOQA 11 | 12 | from .alerts import Alerts # NOQA 13 | from .base_interface import BaseInterface # NOQA 14 | from .datasources import Datasources # NOQA 15 | from .devices import Devices # NOQA 16 | from .gps import GPS # NOQA 17 | from .logger import Logger # NOQA 18 | from .legacy import KismetConnector # NOQA 19 | from .messages import Messages # NOQA 20 | from .packetchain import Packetchain # NOQA 21 | # from .packets import Packets # NOQA 22 | from .system import System # NOQA 23 | from .utility import Utility # NOQA 24 | 25 | __version__ = "2023.01.01" 26 | -------------------------------------------------------------------------------- /kismet_rest/alerts.py: -------------------------------------------------------------------------------- 1 | """Alerts abstraction.""" 2 | 3 | from .base_interface import BaseInterface 4 | 5 | 6 | class Alerts(BaseInterface): 7 | """Alerts abstraction.""" 8 | 9 | kwargs_defaults = {"ts_sec": 0, "ts_usec": 0} 10 | url_template = "alerts/last-time/{ts_sec}.{ts_usec}/alerts.itjson" 11 | 12 | def all(self, callback=None, callback_args=None, **kwargs): 13 | """Yield all alerts, one at a time. 14 | 15 | If callback is set, nothing will be returned. 16 | 17 | Args: 18 | callback: Callback function. 19 | callback_args: Arguments for callback. 20 | 21 | Keyword args: 22 | ts_sec (int): Starting timestamp in seconds since Epoch. 23 | ts_usec (int): Microseconds for starting timestamp. 24 | 25 | Yield: 26 | dict: Alert json, or None if callback is set. 27 | """ 28 | callback_settings = {} 29 | if callback: 30 | callback_settings["callback"] = callback 31 | if callback_args: 32 | callback_settings["callback_args"] = callback_args 33 | query_args = self.kwargs_defaults.copy() 34 | query_args.update(kwargs) 35 | url = self.url_template.format(**query_args) 36 | for result in self.interact_yield("GET", url, **callback_settings): 37 | yield result 38 | 39 | def define(self, name, description, rate="10/min", burst="1/sec", 40 | phyname=None): 41 | """Define an alert. 42 | 43 | LOGIN REQUIRED 44 | 45 | Define a new alert. This alert can then be triggered on external 46 | conditions via raise_alert(...) 47 | 48 | Phyname is optional, and links the alert to a specific PHY type. 49 | 50 | Rate and Burst are optional rate and burst limits. 51 | 52 | Args: 53 | name (str): Name of alert. 54 | description (str): Description of alert. 55 | rate (str): Rate limit. Defaults to ``10/min``. 56 | burst (str): Burst limit. Defaults to ``1/sec``. 57 | phyname (str): Name of PHY. Defaults to None. 58 | 59 | Return: 60 | bool: True for success, False for failed request. 61 | """ 62 | cmd = {"name": name, 63 | "description": description, 64 | "throttle": rate, 65 | "burst": burst} 66 | if phyname is not None: 67 | cmd["phyname"] = phyname 68 | url = "alerts/definitions/define_alert.cmd" 69 | return self.interact("POST", url, payload=cmd, only_status=True) 70 | 71 | def raise_alert(self, name, text, bssid=None, source=None, dest=None, 72 | other=None, channel=None): 73 | """Raise an alert in Kismet. 74 | 75 | Trigger an alert; the alert can be one defined via define_alert(...) or 76 | an alert built into the system. 77 | 78 | The alert name and content of the alert are required, all other fields 79 | are optional. 80 | 81 | Args: 82 | name (str): Name of alert. 83 | text (str): Descriptive text for alert. 84 | bssid (str): BSSID to filter for. 85 | source (str): ... 86 | dest (str): ... 87 | other (str): ... 88 | channel (str): Channel to filter for. 89 | """ 90 | 91 | cmd = {"name": name, 92 | "text": text} 93 | if bssid is not None: 94 | cmd["bssid"] = bssid 95 | if source is not None: 96 | cmd["source"] = source 97 | if dest is not None: 98 | cmd["dest"] = dest 99 | if other is not None: 100 | cmd["other"] = other 101 | if channel is not None: 102 | cmd["channel"] = channel 103 | return self.interact("POST", "alerts/raise_alert.cmd", payload=cmd, 104 | only_status=True) 105 | -------------------------------------------------------------------------------- /kismet_rest/base_interface.py: -------------------------------------------------------------------------------- 1 | """Base interface. All API interaction, at a low level, happens here.""" 2 | 3 | import json 4 | import os 5 | import sys 6 | from urllib3.util.retry import Retry 7 | 8 | import requests 9 | from requests.adapters import HTTPAdapter 10 | 11 | from .logger import Logger 12 | from .exceptions import KismetLoginException 13 | from .exceptions import KismetRequestException 14 | from .exceptions import KismetConnectionError 15 | from .utility import Utility 16 | 17 | if sys.version_info[0] < 3: 18 | from urlparse import urlparse 19 | else: 20 | from urllib.parse import urlparse 21 | 22 | 23 | class BaseInterface(object): 24 | """Initialize with optional keyword arguments to override default settings. 25 | 26 | For compatibility with the original implementation, we support arguments 27 | and keyword arguments for host_uri and session_cache. 28 | 29 | Args: 30 | host_uri (str): URI for Kismet host. This setting is made available as 31 | an argument and a keyword argument. If both are set, the keyword 32 | argument takes precedence. 33 | session_cache (str): Path for storing session cache information. This 34 | setting is made available as an argument and a keyword argument. 35 | if the keyword argument is set, it takes precedence over the 36 | argument. 37 | Keyword Args: 38 | host_uri (str): URI for Kismet REST API. Defaults to 39 | ``http://127.0.0.1:2501``. If Kismet is behind a reverse proxy, add 40 | the base path to this url as well: ``https://my.proxy.com/kismet/`` 41 | username (str): Username for administrative interaction with Kismet 42 | REST interface. 43 | password (str): Password corresponding to ``username``. 44 | session_cache (str): Path for storing session cache information. 45 | Defaults to `~/.pykismet_session`. 46 | debug (bool): Set to True to enble debug logging. 47 | """ 48 | 49 | permitted_kwargs = ["host_uri", "username", "password", 50 | "session_cache", "debug", "apikey"] 51 | 52 | def __init__(self, host_uri='http://127.0.0.1:2501', 53 | sessioncache_path='~/.pykismet_session', **kwargs): 54 | """Initialize using legacy args or (new style) kwargs.""" 55 | self.logger = Logger() 56 | self.max_retries = 5 57 | self.retry_statuses = [500] 58 | self.host_uri = host_uri 59 | self.username = None 60 | self.password = "nopass" 61 | self.apikey = None 62 | self.session_cache = sessioncache_path 63 | self.debug = False 64 | # Set the default path for storing sessions 65 | # self.sessioncache_path = None 66 | self.set_attributes_from_dict(kwargs) 67 | self.session = requests.Session() 68 | self.set_session_cache(self.session_cache) 69 | self.create_client() 70 | if self.debug: 71 | self.logger.set_debug() 72 | if self.username: 73 | self.set_login(self.username, self.password) 74 | if self.apikey: 75 | self.set_apikey(self.apikey) 76 | self.is_py35 = sys.version_info[0] == 3 and sys.version_info[1] == 5 77 | 78 | def set_attributes_from_dict(self, kwa): 79 | """Set instance attributes from dictionary if on whitelist.""" 80 | for kwarg, val in kwa.items(): 81 | if kwarg in self.permitted_kwargs: 82 | setattr(self, kwarg, val) 83 | 84 | def create_client(self): 85 | """Build client object for interaction with Kismet REST API. 86 | 87 | This client implements connection re-use and exponential back-off. 88 | """ 89 | self.client = requests.Session() 90 | self.retries = Retry(total=self.max_retries, 91 | status_forcelist=self.retry_statuses, 92 | backoff_factor=1) 93 | self.http_adapter = HTTPAdapter(pool_connections=1, 94 | max_retries=self.retries) 95 | parsed_uri = urlparse(self.host_uri) 96 | proto = parsed_uri.scheme if parsed_uri.scheme else "http" 97 | host = parsed_uri.netloc 98 | self.session_mount = "{}://{}".format(proto, host) 99 | self.client.mount(self.session_mount, self.http_adapter) 100 | 101 | def log_init(self): 102 | """Initialize logging.""" 103 | lib_ver = Utility.get_lib_version() 104 | kis_ver = self.get_kismet_version() 105 | msg = "Initialized kismetrest v{}, Kismet version {}".format(lib_ver, 106 | kis_ver) 107 | self.logger.debug(msg) 108 | 109 | def get_kismet_version(self): 110 | """Return version of Kismet, as reported by Kismet REST interface.""" 111 | try: 112 | kismet_version = self.interact("GET", "system/status.json") 113 | except (requests.exceptions.ConnectionError) as err: 114 | msg = "Unable to connecto to Kismet: {}".format(err) 115 | raise KismetConnectionError(msg) 116 | return kismet_version 117 | 118 | def interact(self, verb, url_path, stream=False, **kwargs): 119 | """Wrap all low-level API interaction. 120 | 121 | Args: 122 | verb (str): ``GET`` or ``POST``. 123 | url_path (str): Path part of URL. 124 | stream (bool): Process as a stream, meaning that this function 125 | becomes a generator which yields results one at a time. This 126 | enables a lower memory footprint end-to-end, but requires a 127 | somewhat different approach to interacting with this function. 128 | 129 | Keyword Args: 130 | payload (dict): Dictionary with POST payload. 131 | only_status (bool): Only return boolean to represent success or 132 | failure of operation. 133 | callback (function): Callback to be used for each JSON object. 134 | callback_args (list): List of arguments for callback. 135 | 136 | Return: 137 | dict: JSON from API. String returned if return_string is set. 138 | """ 139 | only_status = bool("only_status" in kwargs 140 | and kwargs["only_status"] is True) 141 | payload = kwargs["payload"] if "payload" in kwargs else {} 142 | full_url = Utility.build_full_url(self.host_uri, url_path) 143 | if verb == "GET": 144 | self.logger.debug("interact: GET against {} " 145 | "stream={}".format(full_url, stream)) 146 | response = self.session.get(full_url, stream=stream) 147 | elif verb == "POST": 148 | if payload: 149 | postdata = json.dumps(payload) 150 | else: 151 | postdata = "{}" 152 | 153 | formatted_payload = {"json": postdata} 154 | self.logger.debug("interact: POST against {} " 155 | "with {} stream={}".format(full_url, 156 | formatted_payload, 157 | stream)) 158 | response = self.session.post(full_url, data=formatted_payload, 159 | stream=stream) 160 | 161 | else: 162 | self.logger.error("HTTP verb {} not yet supported!".format(verb)) 163 | 164 | # Application error 165 | if response.status_code == 500: 166 | msg = "Kismet 500 Error response from {}: {}".format(url_path, 167 | response.text) 168 | self.logger.error(msg) 169 | raise KismetLoginException(msg, response.status_code) 170 | 171 | # Invalid request 172 | if response.status_code == 400: 173 | msg = "Kismet 400 Error response from {}: {}".format(url_path, 174 | response.text) 175 | self.logger.error(msg) 176 | raise KismetRequestException(msg, response.status_code) 177 | 178 | # login required 179 | if response.status_code == 401: 180 | msg = "Login required for {}".format(url_path) 181 | self.logger.error(msg) 182 | raise KismetLoginException(msg, response.status_code) 183 | 184 | # Did we succeed? 185 | if not response.status_code == 200: 186 | msg = "Request failed {} {}".format(url_path, response.status_code) 187 | self.logger.error(msg) 188 | raise KismetRequestException(msg, response.status_code) 189 | 190 | if only_status: 191 | return bool(response) # We can test for good resp codes like this. 192 | if not stream: 193 | retval = self.process_response_bulk(response, **kwargs) 194 | self.update_session() 195 | return retval 196 | return [result for result in 197 | self.process_response_stream(response, **kwargs)] 198 | 199 | def interact_yield(self, verb, url_path, **kwargs): 200 | """Wrap all low-level API interaction. 201 | 202 | Args: 203 | verb (str): ``GET`` or ``POST``. 204 | url_path (str): Path part of URL. 205 | 206 | Keyword Args: 207 | payload (dict): Dictionary with POST payload. 208 | callback (function): Callback to be used for each JSON object. 209 | callback_args (list): List of arguments for callback. 210 | 211 | Yield: 212 | dict: JSON from API. String returned if return_string is set. 213 | """ 214 | payload = kwargs["payload"] if "payload" in kwargs else {} 215 | full_url = Utility.build_full_url(self.host_uri, url_path) 216 | if verb == "GET": 217 | self.logger.debug("interact_yield: GET {}".format(full_url)) 218 | response = self.session.get(full_url, stream=True) 219 | elif verb == "POST": 220 | if payload: 221 | postdata = json.dumps(payload) 222 | else: 223 | postdata = "{}" 224 | 225 | formatted_payload = {"json": postdata} 226 | self.logger.debug("interact_yield: POST against {} " 227 | "with {}".format(full_url, formatted_payload)) 228 | response = self.session.post(full_url, data=formatted_payload, 229 | stream=True) 230 | 231 | else: 232 | self.logger.error("HTTP verb {} not yet supported!".format(verb)) 233 | 234 | # Application error 235 | if response.status_code == 500: 236 | msg = "Kismet 500 Error response from {}: {}".format(url_path, 237 | response.text) 238 | self.logger.error(msg) 239 | raise KismetLoginException(msg, response.status_code) 240 | 241 | # Invalid request 242 | if response.status_code == 400: 243 | msg = "Kismet 400 Error response from {}: {}".format(url_path, 244 | response.text) 245 | self.logger.error(msg) 246 | raise KismetRequestException(msg, response.status_code) 247 | 248 | # login required 249 | if response.status_code == 401: 250 | msg = "Login required for {}".format(url_path) 251 | self.logger.error(msg) 252 | raise KismetLoginException(msg, response.status_code) 253 | 254 | # Did we succeed? 255 | if not response.status_code == 200: 256 | msg = "Request failed {} {}".format(url_path, response.status_code) 257 | self.logger.error(msg) 258 | raise KismetRequestException(msg, response.status_code) 259 | for result in self.process_response_stream(response, **kwargs): 260 | yield result 261 | 262 | def process_response_stream(self, response, **kwargs): 263 | """Process API response as a stream.""" 264 | response.encoding = 'utf-8' 265 | if "callback" in kwargs: 266 | callback_args = (kwargs["callback_args"] 267 | if "callback_args" in kwargs 268 | else []) 269 | for item in response.iter_lines(decode_unicode=True): 270 | if callback_args: 271 | kwargs["callback"](json.loads(item), *callback_args) 272 | continue 273 | kwargs["callback"](json.loads(item)) 274 | return 275 | for result in response.iter_lines(decode_unicode=True): 276 | yield json.loads(result) 277 | 278 | def process_response_bulk(self, response, **kwargs): 279 | """Process API response as a single bulk interaction.""" 280 | response.encoding = 'utf-8' 281 | if "callback" in kwargs: 282 | callback_args = (kwargs["callback_args"] 283 | if "callback_args" in kwargs 284 | else []) 285 | for item in response.json(): 286 | if callback_args: 287 | kwargs["callback"](item, *callback_args) 288 | continue 289 | kwargs["callback"](item) 290 | return None 291 | return response.json() 292 | 293 | def set_session_cache(self, path): 294 | """Set a cache file for HTTP sessions. 295 | 296 | Args: 297 | path (str): Path to session cache file. 298 | 299 | """ 300 | self.sessioncache_path = os.path.expanduser(path) 301 | # If we already have a session cache file here, load it 302 | if os.path.isfile(self.sessioncache_path): 303 | try: 304 | lcachef = open(self.sessioncache_path, "r") 305 | cookie = lcachef.read() 306 | # Add the session cookie 307 | requests.utils.add_dict_to_cookiejar( 308 | self.session.cookies, {"KISMET": cookie}) 309 | lcachef.close() 310 | except Exception as exc: 311 | if self.debug: 312 | print("Failed to read session cache:", exc) 313 | 314 | def update_session(self): 315 | """Update the session key. 316 | 317 | Internal utility function for extracting an updated session key, if one 318 | is present, from the connection. Typically called after fetching any 319 | URI. 320 | """ 321 | try: 322 | c_dict = requests.utils.dict_from_cookiejar(self.session.cookies) 323 | cookie = c_dict["KISMET"] 324 | if cookie: 325 | lcachef = open(self.sessioncache_path, "w") 326 | lcachef.write(cookie) 327 | lcachef.close() 328 | except KeyError: 329 | pass 330 | except Exception as exc: 331 | self.logger.error("DEBUG - Failed to save session: {}".format(exc)) 332 | 333 | def set_login(self, username, password): 334 | """Set login credentials.""" 335 | self.session.auth = (username, password) 336 | 337 | def set_apikey(self, apikey): 338 | """Add API key to cookies.""" 339 | requests.utils.add_dict_to_cookiejar( 340 | self.session.cookies, {"KISMET": apikey}) 341 | 342 | def set_debug(self): 343 | """Set debug mode for more verbose output.""" 344 | self.logger.set_debug() 345 | 346 | def check_session(self): 347 | """Confirm session validity. 348 | 349 | Checks if a session is valid / session is logged in 350 | """ 351 | response = self.session.get("%s/session/check_session" % self.host_uri) 352 | if not response.status_code == 200: 353 | return False 354 | self.update_session() 355 | return True 356 | 357 | def login(self): 358 | """Login to Kismet REST interface. 359 | 360 | Logs in (and caches login credentials). Required for administrative 361 | behavior. 362 | """ 363 | response = self.session.get("%s/session/check_session" % self.host_uri) 364 | if not response.status_code == 200: 365 | msg = "login(): Invalid session: {}".format(response.text) 366 | self.logger.debug(msg) 367 | return False 368 | self.update_session() 369 | return True 370 | -------------------------------------------------------------------------------- /kismet_rest/datasources.py: -------------------------------------------------------------------------------- 1 | """Datasources abstraction.""" 2 | 3 | from .base_interface import BaseInterface 4 | 5 | 6 | class Datasources(BaseInterface): 7 | """Datasources abstraction.""" 8 | 9 | kwargs_defaults = {} 10 | url_template = "datasource/all_sources.itjson" 11 | 12 | def all(self, callback=None, callback_args=None): 13 | """Yield all datasources, one at a time. 14 | 15 | If callback is set, nothing will be returned. 16 | 17 | Args: 18 | callback: Callback function. 19 | callback_args: Arguments for callback. 20 | 21 | Yield: 22 | dict: Datasource json, or None if callback is set. 23 | """ 24 | callback_settings = {} 25 | if callback: 26 | callback_settings["callback"] = callback 27 | if callback_args: 28 | callback_settings["callback_args"] = callback_args 29 | url = self.url_template 30 | for result in self.interact_yield("GET", url, **callback_settings): 31 | yield result 32 | 33 | def interfaces(self, callback=None, callback_args=None): 34 | """Yield all interfaces, one at a time. 35 | 36 | If callback is set, nothing will be returned. 37 | 38 | Args: 39 | callback: Callback function. 40 | callback_args: Arguments for callback. 41 | 42 | Yield: 43 | dict: Datasource json, or None if callback is set. 44 | """ 45 | callback_settings = {} 46 | if callback: 47 | callback_settings["callback"] = callback 48 | if callback_args: 49 | callback_settings["callback_args"] = callback_args 50 | url = "datasource/list_interfaces.itjson" 51 | for result in self.interact_yield("GET", url, **callback_settings): 52 | yield result 53 | 54 | def set_channel(self, uuid, channel): 55 | """Return ``True`` if operation was successful, ``False`` otherwise. 56 | 57 | Locks an data source to an 802.11 channel or frequency. Channel 58 | may be complex channel such as "6HT40+". 59 | 60 | Requires valid login. 61 | 62 | """ 63 | cmd = {"channel": channel} 64 | url = "datasource/by-uuid/{}/set_channel.cmd".format(uuid) 65 | return self.interact("POST", url, payload=cmd, only_status=True) 66 | 67 | def set_hop_rate(self, uuid, rate): 68 | """Set the hop rate of a specific data source by UUID. 69 | 70 | Configures the hopping rate of a data source, while not changing the 71 | channels used for hopping. 72 | 73 | Requires valid login 74 | """ 75 | cmd = {"rate": rate} 76 | url = "datasource/by-uuid/{}/set_channel.cmd".format(uuid) 77 | return self.interact("POST", url, payload=cmd, only_status=True) 78 | 79 | def set_hop_channels(self, uuid, rate, channels): 80 | """Set datasource hopping rate by UUID. 81 | 82 | Configures a data source for hopping at 'rate' over a vector of 83 | channels. 84 | 85 | Requires valid login 86 | """ 87 | cmd = {"rate": rate, 88 | "channels": channels} 89 | url = "datasource/by-uuid/{}/set_channel.cmd".format(uuid) 90 | return self.interact("POST", url, payload=cmd, only_status=True) 91 | 92 | def set_hop(self, uuid): 93 | """Configure a source for hopping. 94 | 95 | Uses existing source hop / channel list / etc attributes. 96 | 97 | Requires valid login 98 | """ 99 | cmd = {"hop": True} 100 | url = "datasource/by-uuid/{}/set_hop.cmd".format(uuid) 101 | return self.interact("POST", url, payload=cmd, only_status=True) 102 | 103 | def add(self, source): 104 | """Add a new source to Kismet. 105 | 106 | source is a standard source definition. 107 | 108 | Requires valid login. 109 | 110 | Return: 111 | bool: Success 112 | """ 113 | cmd = {"definition": source} 114 | return self.interact("POST", "datasource/add_source.cmd", 115 | only_status=True, payload=cmd) 116 | 117 | def pause(self, source): 118 | """Pause source. 119 | 120 | Args: 121 | source (str): UUID of source to pause. 122 | 123 | Return: 124 | bool: Success 125 | 126 | """ 127 | url = "/datasource/by-uuid/{}/pause_source.cmd".format(source) 128 | return self.interact("GET", url, only_status=True) 129 | 130 | def resume(self, source): 131 | """Resume paused source. 132 | 133 | Args: 134 | source (str): UUID of source to resume. 135 | 136 | Return: 137 | bool: Success 138 | 139 | """ 140 | url = "/datasource/by-uuid/{}/resume_source.cmd".format(source) 141 | return self.interact("GET", url, only_status=True) 142 | 143 | def close(self, uuid): 144 | """Close source. A closed source will no longer be processed, and will remain closed unless reopened. 145 | 146 | Args: 147 | uuid (str): UUID of source to close. 148 | 149 | Return: 150 | bool: Success 151 | 152 | """ 153 | url = "/datasource/by-uuid/{}/close_source.cmd".format(uuid) 154 | return self.interact("GET", url, only_status=True) 155 | 156 | def open(self, uuid): 157 | """Reopen a closed source. 158 | 159 | Args: 160 | uuid (str): UUID of source to open. 161 | 162 | Return: 163 | bool: Success 164 | 165 | """ 166 | url = "/datasource/by-uuid/{}/open_source.cmd".format(uuid) 167 | return self.interact("GET", url, only_status=True) 168 | 169 | -------------------------------------------------------------------------------- /kismet_rest/devices.py: -------------------------------------------------------------------------------- 1 | """Devices abstraction.""" 2 | 3 | from .base_interface import BaseInterface 4 | 5 | 6 | class Devices(BaseInterface): 7 | """Devices abstraction.""" 8 | 9 | kwargs_defaults = {"ts": 0} 10 | url_template = "devices/last-time/{ts}/devices.itjson" 11 | 12 | def all(self, callback=None, callback_args=None, **kwargs): 13 | """Yield all devices, one at a time. 14 | 15 | If callback is set, nothing will be returned. 16 | 17 | Args: 18 | callback: Callback function. 19 | callback_args: Arguments for callback. 20 | 21 | Keyword args: 22 | ts (int): Starting last-seen timestamp in seconds since Epoch. 23 | 24 | Yield: 25 | dict: Device json, or None if callback is set. 26 | """ 27 | callback_settings = {} 28 | if callback: 29 | callback_settings["callback"] = callback 30 | if callback_args: 31 | callback_settings["callback_args"] = callback_args 32 | query_args = self.kwargs_defaults.copy() 33 | query_args.update(kwargs) 34 | url = self.url_template.format(**query_args) 35 | for result in self.interact_yield("POST", url, **callback_settings): 36 | yield result 37 | 38 | def by_mac(self, callback=None, callback_args=None, **kwargs): 39 | """Yield devices matching provided MAC addresses or masked MAC groups. 40 | 41 | Args: 42 | callback: Callback function. 43 | callback_args: Arguments for callback. 44 | 45 | Keyword args: 46 | devices (list): List of device MACs or MAC masks. 47 | fields (list): List of fields to return. 48 | 49 | Yield: 50 | dict: Device json, or None if callback is set. 51 | """ 52 | call_settings = {} 53 | if callback: 54 | call_settings["callback"] = callback 55 | if callback_args: 56 | call_settings["callback_args"] = callback_args 57 | valid_kwargs = ["fields", "devices"] 58 | call_settings["payload"] = {kword: kwargs[kword] 59 | for kword in valid_kwargs 60 | if kword in kwargs} 61 | url = "devices/multimac/devices.itjson" 62 | for result in self.interact_yield("POST", url, **call_settings): 63 | yield result 64 | 65 | def by_key(self, device_key, field=None, fields=None): 66 | """Return a dictionary representing one device, identified by ``key``. 67 | 68 | Fetch a complete device record by the Kismet key (unique key per Kismet 69 | session) or fetch a specific sub-field by path. 70 | 71 | Return: 72 | dict: Dictionary object describing one device. 73 | """ 74 | url = "devices/by-key/{}/device.json".format(device_key) 75 | if not fields and field: 76 | url = "{}/{}".format(url, field) 77 | elif fields: 78 | payload = {"fields": fields} 79 | return self.interact("POST", url, payload=payload) 80 | return self.interact("POST", url) 81 | 82 | def dot11_clients_of(self, ap_id, callback=None, callback_args=None, 83 | **kwargs): 84 | """List clients of an 802.11 AP. 85 | 86 | List devices which are clients of a given 802.11 access point, using 87 | the /phy/phy80211/clients-of endpoint. 88 | 89 | Returned devices can be summarized/simplified by the fields list. 90 | 91 | If a callback is given, it will be called for each device in the 92 | result. If no callback is provided, the results will be yielded. 93 | 94 | Args: 95 | ap_id (str): ID of AP to return clients for. 96 | callback: Callback function. 97 | callback_args: Arguments for callback. 98 | 99 | Yield: 100 | dict: Dictionary describing a client of the identified AP. 101 | """ 102 | call_settings = {} 103 | if callback: 104 | call_settings["callback"] = callback 105 | if callback_args: 106 | call_settings["callback_args"] = callback_args 107 | valid_kwargs = ["fields"] 108 | call_settings["payload"] = {kword: kwargs[kword] 109 | for kword in valid_kwargs 110 | if kword in kwargs} 111 | url = "phy/phy80211/clients-of/{}/clients.itjson".format(ap_id) 112 | for result in self.interact_yield("POST", url, **call_settings): 113 | yield result 114 | 115 | def dot11_access_points(self, callback=None, callback_args=None, **kwargs): 116 | """Return a list of dot11 access points. 117 | 118 | List devices which are considered to be 802.11 access points, using the 119 | /devices/views/phydot11_accesspoints/ view 120 | 121 | Returned devices can be summarized/simplified by the fields list. 122 | 123 | If a timestamp is given, only devices modified more recently than the 124 | timestamp (and matching any other conditions) will be returned. 125 | 126 | If a regex is given, only devices matching the regex (and any other 127 | conditions) will be returned. 128 | 129 | If a callback is given, it will be called for each device in the 130 | result. If no callback is provided, the results will be yielded as 131 | dictionary objects. 132 | 133 | Args: 134 | callback (obj): Callback for processing individual results. 135 | cbargs (list): List of arguments for callback. 136 | 137 | Keyword args: 138 | last_time (int): Unix epoch timestamp 139 | regex (str): Regular expression for filtering results. 140 | fields (list): Fields for filtering. 141 | 142 | Yield: 143 | dict: Dictionary-type objects which describe access points. 144 | Keys describing access points: 145 | ``dot11.device``, 146 | ``kismet.device.base.basic_crypt_set``, 147 | ``kismet.device.base.basic_type_set``, 148 | ``kismet.device.base.channel``, 149 | ``kismet.device.base.commonname``, 150 | ``kismet.device.base.crypt``, 151 | ``kismet.device.base.datasize``, 152 | ``kismet.device.base.datasize.rrd``, 153 | ``kismet.device.base.first_time``, 154 | ``kismet.device.base.freq_khz_map``, 155 | ``kismet.device.base.frequency``, 156 | ``kismet.device.base.key``, 157 | ``kismet.device.base.last_time``, 158 | ``kismet.device.base.macaddr``, 159 | ``kismet.device.base.manuf``, 160 | ``kismet.device.base.mod_time``, 161 | ``kismet.device.base.name``, 162 | ``kismet.device.base.num_alerts``, 163 | ``kismet.device.base.packet.bin.250``, 164 | ``kismet.device.base.packet.bin.500``, 165 | ``kismet.device.base.packets.crypt``, 166 | ``kismet.device.base.packets.data``, 167 | ``kismet.device.base.packets.error``, 168 | ``kismet.device.base.packets.filtered``, 169 | ``kismet.device.base.packets.llc``, 170 | ``kismet.device.base.packets.rrd``, 171 | ``kismet.device.base.packets.rx``, 172 | ``kismet.device.base.packets.total``, 173 | ``kismet.device.base.packets.tx``, 174 | ``kismet.device.base.phyname``, 175 | ``kismet.device.base.seenby``, 176 | ``kismet.device.base.server_uuid``, 177 | ``kismet.device.base.signal``, 178 | ``kismet.device.base.tags``, 179 | ``kismet.device.base.type``. 180 | """ 181 | valid_kwargs = ["last_time", "regex", "fields"] 182 | url = "devices/views/phydot11_accesspoints/devices.itjson" 183 | call_settings = {} 184 | if callback: 185 | call_settings["callback"] = callback 186 | if callback_args: 187 | call_settings["callback_args"] = callback_args 188 | call_settings["payload"] = {kword: kwargs[kword] 189 | for kword in valid_kwargs 190 | if kword in kwargs} 191 | for result in self.interact_yield("POST", url, **call_settings): 192 | yield result 193 | -------------------------------------------------------------------------------- /kismet_rest/exceptions.py: -------------------------------------------------------------------------------- 1 | """All exceptions for the kismet-rest library are defined here.""" 2 | 3 | 4 | class KismetConnectorException(Exception): 5 | """General class.""" 6 | 7 | 8 | 9 | class KismetLoginException(KismetConnectorException): 10 | """Authentication-related exception.""" 11 | def __init__(self, message, rcode): 12 | super(Exception, self).__init__(message) 13 | self.rcode = rcode 14 | 15 | 16 | class KismetRequestException(KismetConnectorException): 17 | """Request-related exception.""" 18 | def __init__(self, message, rcode): 19 | super(Exception, self).__init__(message) 20 | self.rcode = rcode 21 | 22 | 23 | class KismetConnectionError(KismetConnectorException): 24 | """Connection-related exception.""" 25 | def __init__(self, message, rcode=None): 26 | super(Exception, self).__init__(message) 27 | self.rcode = rcode 28 | 29 | class KismetServiceError(KismetConnectorException): 30 | """Server-side application errors.""" 31 | def __init__(self, message, rcode): 32 | super(Exception, self).__init__(message) 33 | self.rcode = rcode 34 | -------------------------------------------------------------------------------- /kismet_rest/gps.py: -------------------------------------------------------------------------------- 1 | """GPS abstraction.""" 2 | 3 | from .base_interface import BaseInterface 4 | 5 | 6 | class GPS(BaseInterface): 7 | """GPS abstraction.""" 8 | 9 | def current_location(self): 10 | """Return the gps location. 11 | 12 | Return: 13 | dict: Dictionary object describing current location of Kismet 14 | server. Keys represented in output: 15 | ``kismet.common.location.lat``, 16 | ``kismet.common.location.lon``, 17 | ``kismet.common.location.alt``, 18 | ``kismet.common.location.heading``, 19 | ``kismet.common.location.speed``, 20 | ``kismet.common.location.time_sec``, 21 | ``kismet.common.location.time_usec``, 22 | ``kismet.common.location.fix``, 23 | ``kismet.common.location.valid`` 24 | """ 25 | return self.interact("GET", "gps/location.json") 26 | -------------------------------------------------------------------------------- /kismet_rest/legacy.py: -------------------------------------------------------------------------------- 1 | """Kismet REST interface module. 2 | 3 | (c) 2018 Mike Kershaw / Dragorn 4 | Licensed under GPL2 or above. 5 | """ 6 | 7 | from .base_interface import BaseInterface 8 | 9 | """ 10 | The field simplification and pathing options are best described in the 11 | developer docs for Kismet under docs/dev/webui_rest.md ; basically, they 12 | allow for selecting specific fields from the tree and returning ONLY those 13 | fields, instead of the entire object. 14 | 15 | This will increase the speed of searches of large sets of data, and decrease 16 | the time it takes for Kismet to return them. 17 | 18 | Whenever possible this API will use the 'itjson' format for multiple returned 19 | objects - this places a JSON object for each element in an array/vector 20 | response as a complete JSON record followed by a newline; this allows for 21 | parsing the JSON response without allocating the entire vector object in memory 22 | first, and enables streamed-base parsing of very large responses. 23 | 24 | Field Simplification Specification: 25 | 26 | Several endpoints in Kismet take a field filtering object. These 27 | use a common specification: 28 | 29 | [ 30 | field1, 31 | ... 32 | fieldN 33 | ] 34 | 35 | where a field may be a single-element string, consisting of a 36 | field name -or- a field path, such as: 37 | 'kismet.device.base.channel' 38 | 'kismet.device.base.signal/kismet.common.signal.last_signal_dbm' 39 | 40 | OR a field may be a two-value array, consisting of a field name or 41 | path, and a target name the field will be aliased to: 42 | 43 | ['kismet.device.base.channel', 'base.channel'] 44 | ['kismet.device.base.signal/kismet.common.signal.last_signal_dbm', 45 | 'last.signal'] 46 | 47 | The fields in the returned device will be inserted as their final 48 | name - that is, from the first above example, the device will contain 49 | 'kismet.device.base.channel' and 'kismet.common.signal.last_signal_dbm' 50 | and from the second example: 51 | 'base.channel' and 'last.signal' 52 | 53 | Filter Specification: 54 | 55 | Several endpoints in Kismet take a regex object. These use a common 56 | specification: 57 | 58 | [ 59 | [ multifield, regex ], 60 | ... 61 | [ multifield, regex ] 62 | ] 63 | 64 | Multifield is a field path specification which will automatically expand 65 | value-maps and vectors found in the path. For example, the multifield 66 | path: 67 | 'dot11.device/dot11.device.advertised_ssid_map/dot11.advertisedssid.ssid' 68 | 69 | would apply to all 'dot11.advertisedssid.ssid' fields in the ssid_map 70 | automatically. 71 | 72 | Regex is a basic string containing a regular expression, compatible with 73 | PCRE. 74 | 75 | To match on SSIDs: 76 | 77 | regex = [ 78 | ['dot11.device/dot11.device.advertised_ssid_map/dot11.advertisedssid.ssid', 79 | '^SomePrefix.*' ] 80 | ] 81 | 82 | A device is included in the results if it matches any of the regular 83 | expressions. 84 | 85 | """ 86 | 87 | 88 | class KismetConnector(BaseInterface): 89 | """Kismet rest API.""" 90 | 91 | def system_status(self): 92 | """Return system status. 93 | 94 | Note: This is superseded by :py:meth:`kismet_rest.System.get_status` 95 | """ 96 | return self.interact("GET", "system/status.json") 97 | 98 | def device_summary(self, callback=None, cbargs=None): 99 | """Return a summary of all devices. 100 | 101 | Note: This is superseded by :py:meth:`kismet_rest.Devices.all` 102 | 103 | Deprecated API - now referenced as device_list(..) 104 | """ 105 | return self.device_list(callback, cbargs) 106 | 107 | def device_list(self, callback=None, cbargs=None): 108 | """Return all fields of all devices. 109 | 110 | Note: This is superseded by :py:meth:`kismet_rest.Devices.all` 111 | 112 | This may be extremely memory and CPU intensive and should be avoided. 113 | Memory use can be reduced by providing a callback, which will be 114 | invoked for each device. 115 | 116 | In general THIS API SHOULD BE AVOIDED. There are several potentially 117 | serious repercussions in querying all fields of all devices in a very 118 | high device count environment. 119 | 120 | It is strongly recommended that you use smart_device_list(...) 121 | """ 122 | kwargs = {} 123 | url = "/devices/all_devices.itjson" 124 | if callback: 125 | kwargs = {"callback": callback, 126 | "callback_args": cbargs} 127 | return self.interact("GET", url, True, **kwargs) 128 | 129 | def device_summary_since(self, ts=0, fields=None, callback=None, 130 | cbargs=None): 131 | """ 132 | device_summary_since(ts, [fields, callback, cbargs]) -> 133 | device summary list 134 | 135 | Note: This is superseded by :py:meth:`kismet_rest.Devices.all` 136 | 137 | Deprecated API - now referenced as smart_device_list(...) 138 | 139 | Return object containing summary of devices added or changed since ts 140 | and ts info 141 | """ 142 | return self.smart_device_list(ts=ts, fields=fields, callback=callback, 143 | cbargs=cbargs) 144 | 145 | def smart_summary_since(self, ts=0, fields=None, regex=None, callback=None, 146 | cbargs=None): 147 | """ 148 | smart_summary_since([ts, fields, regex, callback, cbargs]) -> 149 | device summary list 150 | 151 | Note: This is superseded by :py:meth:`kismet_rest.Devices.all` 152 | 153 | Deprecated API - now referenced as smart_device_list(...) 154 | """ 155 | return self.smart_device_list(ts=ts, fields=fields, regex=regex, 156 | callback=callback, cbargs=cbargs) 157 | 158 | def smart_device_list(self, ts=0, fields=None, regex=None, callback=None, 159 | cbargs=None): 160 | """Return a list of devices. 161 | 162 | Note: This is superseded by :py:meth:`kismet_rest.Devices.all` 163 | 164 | Perform a 'smart' device list. The device list can be manipulated in 165 | several ways: 166 | 167 | 1. Devices active since last timestamp. By setting the 'ts' 168 | parameter, only devices which have been active since that 169 | timestamp will be returned. 170 | 2. Devices which match a regex, as defined by the regex spec above 171 | 3. Devices can be simplified to reduce the amount of work being 172 | done and number of fields being returned. 173 | 174 | If a callback is given, it will be called for each device in the 175 | result. If no callback is provided, the results will be returned as a 176 | vector. 177 | 178 | Args: 179 | ts (int): Unix epoch timestamp. 180 | fields (list): List of field names for matching. 181 | regex (str): Regular expression for field matching. 182 | callback (obj): Callback for processing search results. 183 | cbargs (list): List of arguments for callback. 184 | 185 | Returns: 186 | list: List of dictionary-type objects, which describe devices 187 | observed by Kismet. Dictionary keys are: 188 | ``dot11.device``, 189 | ``kismet.device.base.basic_crypt_set``, 190 | ``kismet.device.base.basic_type_set``, 191 | ``kismet.device.base.channel``, 192 | ``kismet.device.base.commonname``, 193 | ``kismet.device.base.crypt``, 194 | ``kismet.device.base.datasize``, 195 | ``kismet.device.base.datasize.rrd``, 196 | ``kismet.device.base.first_time``, 197 | ``kismet.device.base.freq_khz_map``, 198 | ``kismet.device.base.frequency``, 199 | ``kismet.device.base.key``, 200 | ``kismet.device.base.last_time``, 201 | ``kismet.device.base.macaddr``, 202 | ``kismet.device.base.manuf``, 203 | ``kismet.device.base.mod_time``, 204 | ``kismet.device.base.name``, 205 | ``kismet.device.base.num_alerts``, 206 | ``kismet.device.base.packet.bin.1000``, 207 | ``kismet.device.base.packet.bin.250``, 208 | ``kismet.device.base.packet.bin.500``, 209 | ``kismet.device.base.packets.crypt``, 210 | ``kismet.device.base.packets.data``, 211 | ``kismet.device.base.packets.error``, 212 | ``kismet.device.base.packets.filtered``, 213 | ``kismet.device.base.packets.llc``, 214 | ``kismet.device.base.packets.rrd``, 215 | ``kismet.device.base.packets.rx``, 216 | ``kismet.device.base.packets.total``, 217 | ``kismet.device.base.packets.tx``, 218 | ``kismet.device.base.phyname``, 219 | ``kismet.device.base.seenby``, 220 | ``kismet.device.base.server_uuid``, 221 | ``kismet.device.base.signal``, 222 | ``kismet.device.base.tags``, 223 | ``kismet.device.base.type``. 224 | """ 225 | 226 | cmd = {} 227 | 228 | if fields: 229 | cmd["fields"] = fields 230 | 231 | if regex: 232 | cmd["regex"] = regex 233 | 234 | url = "devices/last-time/{}/devices.itjson".format(ts) 235 | kwargs = {"payload": cmd} 236 | if callback: 237 | kwargs = {"callback": callback, 238 | "callback_args": cbargs, 239 | "payload": cmd} 240 | print(url) 241 | return self.interact("POST", url, True, **kwargs) 242 | 243 | def device_list_by_mac(self, maclist, fields=None, callback=None, 244 | cbargs=None): 245 | """List devices matching MAC addresses in maclist. 246 | 247 | Note: This method is deprecated. 248 | 249 | Use :py:meth:`kismet_rest.Devices.yield_by_mac` instead. 250 | 251 | MAC addresses may be 252 | complete MACs or masked MAC groups 253 | ("AA:BB:CC:00:00:00/FF:FF:FF:00:00:00"). 254 | 255 | Returned devices can be summarized/simplified by the fields list. 256 | 257 | If a callback is given, it will be called for each device in the 258 | result. If no callback is provided, the results will be returned as a 259 | vector. 260 | """ 261 | cmd = {} 262 | url = "devices/multimac/devices.itjson" 263 | if fields is not None: 264 | cmd["fields"] = fields 265 | 266 | cmd["devices"] = maclist 267 | 268 | if callback: 269 | return [result for result in 270 | self.interact_yield("POST", url, payload=cmd, 271 | callback=callback, 272 | callback_args=cbargs, stream=True)] 273 | return [result for result in 274 | self.interact_yield("POST", url, payload=cmd, stream=True)] 275 | 276 | def dot11_clients_of(self, apkey, fields=None, callback=None, cbargs=None): 277 | """List clients of 802.11 AP. 278 | 279 | Note: This is superseded by 280 | :py:meth:`kismet_rest.Devices.dot11_clients_of` 281 | 282 | List devices which are clients of a given 802.11 access point, using 283 | the /phy/phy80211/clients-of endpoint. 284 | 285 | Returned devices can be summarized/simplified by the fields list. 286 | 287 | If a callback is given, it will be called for each device in the 288 | result. If no callback is provided, the results will be returned as a 289 | vector. 290 | """ 291 | cmd = {} 292 | 293 | if fields is not None: 294 | cmd["fields"] = fields 295 | url = "phy/phy80211/clients-of/{}/clients.itjson".format(apkey) 296 | if callback: 297 | return [result for result in 298 | self.interact_yield("POST", url, payload=cmd, 299 | callback=callback, 300 | callback_args=cbargs, stream=True)] 301 | return [result for result in 302 | self.interact_yield("POST", url, payload=cmd, stream=True)] 303 | 304 | def dot11_access_points(self, tstamp=None, regex=None, fields=None, 305 | callback=None, cbargs=None): 306 | """Return a list of dot11 access points. 307 | 308 | Note: This is superseded by 309 | :py:meth:`kismet_rest.Devices.dot11_access_points` 310 | 311 | List devices which are considered to be 802.11 access points, using the 312 | /devices/views/phydot11_accesspoints/ view 313 | 314 | Returned devices can be summarized/simplified by the fields list. 315 | 316 | If a timestamp is given, only devices modified more recently than the 317 | timestamp (and matching any other conditions) will be returned. 318 | 319 | If a regex is given, only devices matching the regex (and any other 320 | conditions) will be returned. 321 | 322 | If a callback is given, it will be called for each device in the 323 | result. If no callback is provided, the results will be returned as a 324 | vector. 325 | 326 | Args: 327 | ts (int): Unix epoch timestamp 328 | regex (str): Regular expression for filtering results. 329 | fields (list): Fields for filtering. 330 | callback (obj): Callback for processing individual results. 331 | cbargs (list): List of arguments for callback. 332 | 333 | Return: 334 | list: List of dictionary-type objects which describe access points. 335 | Keys describing access points: 336 | ``dot11.device``, 337 | ``kismet.device.base.basic_crypt_set``, 338 | ``kismet.device.base.basic_type_set``, 339 | ``kismet.device.base.channel``, 340 | ``kismet.device.base.commonname``, 341 | ``kismet.device.base.crypt``, 342 | ``kismet.device.base.datasize``, 343 | ``kismet.device.base.datasize.rrd``, 344 | ``kismet.device.base.first_time``, 345 | ``kismet.device.base.freq_khz_map``, 346 | ``kismet.device.base.frequency``, 347 | ``kismet.device.base.key``, 348 | ``kismet.device.base.last_time``, 349 | ``kismet.device.base.macaddr``, 350 | ``kismet.device.base.manuf``, 351 | ``kismet.device.base.mod_time``, 352 | ``kismet.device.base.name``, 353 | ``kismet.device.base.num_alerts``, 354 | ``kismet.device.base.packet.bin.250``, 355 | ``kismet.device.base.packet.bin.500``, 356 | ``kismet.device.base.packets.crypt``, 357 | ``kismet.device.base.packets.data``, 358 | ``kismet.device.base.packets.error``, 359 | ``kismet.device.base.packets.filtered``, 360 | ``kismet.device.base.packets.llc``, 361 | ``kismet.device.base.packets.rrd``, 362 | ``kismet.device.base.packets.rx``, 363 | ``kismet.device.base.packets.total``, 364 | ``kismet.device.base.packets.tx``, 365 | ``kismet.device.base.phyname``, 366 | ``kismet.device.base.seenby``, 367 | ``kismet.device.base.server_uuid``, 368 | ``kismet.device.base.signal``, 369 | ``kismet.device.base.tags``, 370 | ``kismet.device.base.type``. 371 | """ 372 | cmd = {} 373 | 374 | if tstamp is not None: 375 | cmd["last_time"] = tstamp 376 | 377 | if regex is not None: 378 | cmd["regex"] = regex 379 | 380 | if fields is not None: 381 | cmd["fields"] = fields 382 | url = "devices/views/phydot11_accesspoints/devices.itjson" 383 | if callback: 384 | return [result for result in 385 | self.interact_yield("POST", url, payload=cmd, 386 | callback=callback, 387 | callback_args=cbargs, stream=True)] 388 | return [result for result in 389 | self.interact_yield("POST", url, payload=cmd, stream=True)] 390 | 391 | def device(self, key, field=None, fields=None): 392 | """Wrap device_by_key. 393 | 394 | Deprecated. 395 | """ 396 | return self.device_by_key(key, field, fields) 397 | 398 | def device_field(self, key, field): 399 | """Wrap device_by_key. 400 | 401 | Deprecated, prefer device_by_key with field. 402 | """ 403 | return self.device_by_key(key, field=field) 404 | 405 | def device_by_key(self, key, field=None, fields=None): 406 | """Return a dictionary representing one device, identified by ``key``. 407 | 408 | Note: This is superseded by 409 | :py:meth:`kismet_rest.Devices.get_by_key` 410 | 411 | Fetch a complete device record by the Kismet key (unique key per Kismet 412 | session) or fetch a specific sub-field by path. 413 | 414 | If a field simplification set is passed in 'fields', perform a 415 | simplification on the result 416 | """ 417 | if fields is None: 418 | if field is not None: 419 | field = "/" + field 420 | else: 421 | field = "" 422 | url = "devices/by-key/{}/device.json{}".format(key, field) 423 | result = self.interact("GET", url) 424 | else: 425 | payload = {"fields": fields} 426 | url = "devices/by-key/{}/device.json".format(key) 427 | result = self.interact("POST", url, payload=payload) 428 | return result 429 | 430 | def device_by_mac(self, mac, fields=None): 431 | """Return a list of all devices matching ``mac``. 432 | 433 | Deprecated. 434 | Use :py:meth:`kismet_rest.Devices.yield_by_mac` instead. 435 | 436 | Return a vector of all devices in all phy types matching the supplied 437 | MAC address; typically this will return a vector of a single device, 438 | but MAC addresses could overlap between phy types. 439 | 440 | If a field simplification set is passed in 'fields', perform a 441 | simplification on the result 442 | """ 443 | if fields: 444 | cmd = {"fields": fields} 445 | url = "devices/by-mac/{}/devices.json".format(mac) 446 | return self.interact("POST", url, payload=cmd, stream=False) 447 | url = "devices/by-mac/{}/devices.json".format(mac) 448 | return self.interact("POST", url, stream=False) 449 | 450 | def datasources(self): 451 | """Return a list of data sources. 452 | 453 | Deprecated. 454 | Use :py:meth:`kismet_rest.Datasources.all` instead. 455 | 456 | Return: 457 | list: List of dictionary-type objects, which describe data sources. 458 | Dictionary keys are: 459 | ``kismet.datasource.capture_interface``, 460 | ``kismet.datasource.channel``, 461 | ``kismet.datasource.channels``, 462 | ``kismet.datasource.definition``, 463 | ``kismet.datasource.dlt``, 464 | ``kismet.datasource.error``, 465 | ``kismet.datasource.error_reason``, 466 | ``kismet.datasource.hardware``, 467 | ``kismet.datasource.hop_channels``, 468 | ``kismet.datasource.hop_offset``, 469 | ``kismet.datasource.hopping``, 470 | ``kismet.datasource.hop_rate``, 471 | ``kismet.datasource.hop_shuffle``, 472 | ``kismet.datasource.hop_shuffle_skip``, 473 | ``kismet.datasource.hop_split``, 474 | ``kismet.datasource.info.amp_gain``, 475 | ``kismet.datasource.info.amp_type``, 476 | ``kismet.datasource.info.antenna_beamwidth``, 477 | ``kismet.datasource.info.antenna_gain``, 478 | ``kismet.datasource.info.antenna_orientation``, 479 | ``kismet.datasource.info.antenna_type``, 480 | ``kismet.datasource.interface``, 481 | ``kismet.datasource.ipc_binary``, 482 | ``kismet.datasource.ipc_pid``, 483 | ``kismet.datasource.linktype_override``, 484 | ``kismet.datasource.name``, 485 | ``kismet.datasource.num_error_packets``, 486 | ``kismet.datasource.num_packets``, 487 | ``kismet.datasource.packets_rrd``, 488 | ``kismet.datasource.passive``, 489 | ``kismet.datasource.paused``, 490 | ``kismet.datasource.remote``, 491 | ``kismet.datasource.retry``, 492 | ``kismet.datasource.retry_attempts``, 493 | ``kismet.datasource.running``, 494 | ``kismet.datasource.source_key``, 495 | ``kismet.datasource.source_number``, 496 | ``kismet.datasource.total_retry_attempts``, 497 | ``kismet.datasource.type_driver``, 498 | ``kismet.datasource.uuid``, 499 | ``kismet.datasource.warning``. 500 | 501 | """ 502 | return self.interact("GET", "datasource/all_sources.json") 503 | 504 | def datasource_list_interfaces(self): 505 | """Return a list of all available interfaces. 506 | 507 | Deprecated. 508 | Use :py:meth:`kismet_rest.Datasources.yield_interfaces` instead. 509 | """ 510 | return self.interact("GET", "datasource/list_interfaces.json") 511 | 512 | def config_datasource_set_channel(self, uuid, channel): 513 | """Return ``True`` if operation was successful, ``False`` otherwise. 514 | 515 | Deprecated. 516 | Use :py:meth:`kismet_rest.Datasources.set_channel` instead. 517 | 518 | Locks an data source to an 802.11 channel or frequency. Channel 519 | may be complex channel such as "6HT40+". 520 | 521 | Requires valid login. 522 | 523 | """ 524 | cmd = {"channel": channel} 525 | url = "datasource/by-uuid/{}/set_channel.cmd".format(uuid) 526 | return self.interact("POST", url, payload=cmd, only_status=True) 527 | 528 | def config_datasource_set_hop_rate(self, uuid, rate): 529 | """Set the hop rate of a specific data source by UUID. 530 | 531 | Deprecated. 532 | Use :py:meth:`kismet_rest.Datasources.set_hop_rate` instead. 533 | 534 | Configures the hopping rate of a data source, while not changing the 535 | channels used for hopping. 536 | 537 | Requires valid login 538 | """ 539 | cmd = {"rate": rate} 540 | url = "datasource/by-uuid/{}/set_channel.cmd".format(uuid) 541 | return self.interact("POST", url, payload=cmd, only_status=True) 542 | 543 | def config_datasource_set_hop_channels(self, uuid, rate, channels): 544 | """Set datasource hopping rate by UUID. 545 | 546 | Deprecated. 547 | Use :py:meth:`kismet_rest.Datasources.set_hop_channels` instead. 548 | 549 | Configures a data source for hopping at 'rate' over a vector of 550 | channels. 551 | 552 | Requires valid login 553 | """ 554 | cmd = {"rate": rate, 555 | "channels": channels} 556 | url = "datasource/by-uuid/{}/set_channel.cmd".format(uuid) 557 | return self.interact("POST", url, payload=cmd, only_status=True) 558 | 559 | def config_datasource_set_hop(self, uuid): 560 | """Configure a source for hopping. 561 | 562 | Deprecated. 563 | Use :py:meth:`kismet_rest.Datasources.set_hop` instead. 564 | 565 | Uses existing source hop / channel list / etc attributes. 566 | 567 | Requires valid login 568 | """ 569 | cmd = {"hop": True} 570 | url = "datasource/by-uuid/{}/set_hop.cmd".format(uuid) 571 | return self.interact("POST", url, payload=cmd, only_status=True) 572 | 573 | def add_datasource(self, source): 574 | """Add a new source to Kismet. 575 | 576 | Deprecated. 577 | Use :py:meth:`kismet_rest.Datasources.add` instead. 578 | 579 | source is a standard source definition. 580 | 581 | Requires valid login. 582 | 583 | Return: 584 | bool: Success 585 | """ 586 | cmd = {"definition": source} 587 | 588 | return self.interact("POST", "datasource/add_source.cmd", 589 | only_status=True, payload=cmd) 590 | 591 | def define_alert(self, name, description, rate="10/min", burst="1/sec", 592 | phyname=None): 593 | """ 594 | define_alert(name, description, rate, burst) -> Boolean 595 | 596 | Deprecated. 597 | Use :py:meth:`kismet_rest.Alerts.define` instead. 598 | 599 | LOGIN REQUIRED 600 | 601 | Define a new alert. This alert can then be triggered on external 602 | conditions via raise_alert(...) 603 | 604 | Phyname is optional, and links the alert to a specific PHY type. 605 | 606 | Rate and Burst are optional rate and burst limits. 607 | """ 608 | cmd = {"name": name, 609 | "description": description, 610 | "throttle": rate, 611 | "burst": burst} 612 | if phyname is not None: 613 | cmd["phyname"] = phyname 614 | url = "alerts/definitions/define_alert.cmd" 615 | return self.interact("POST", url, payload=cmd, only_status=True) 616 | 617 | def raise_alert(self, name, text, bssid=None, source=None, dest=None, 618 | other=None, channel=None): 619 | """Raise an alert in Kismet. 620 | 621 | Deprecated. 622 | Use :py:meth:`kismet_rest.Alerts.raise` instead. 623 | 624 | LOGIN REQUIRED 625 | 626 | Trigger an alert; the alert can be one defined via define_alert(...) or 627 | an alert built into the system. 628 | 629 | The alert name and content of the alert are required, all other fields 630 | are optional. 631 | 632 | Args: 633 | name (str): Name of alert. 634 | text (str): Descriptive text for alert. 635 | bssid (str): BSSID to filter for. 636 | source (str): ... 637 | dest (str): ... 638 | other (str): ... 639 | channel (str): Channel to filter for. 640 | """ 641 | 642 | cmd = {"name": name, 643 | "text": text} 644 | if bssid is not None: 645 | cmd["bssid"] = bssid 646 | if source is not None: 647 | cmd["source"] = source 648 | if dest is not None: 649 | cmd["dest"] = dest 650 | if other is not None: 651 | cmd["other"] = other 652 | if channel is not None: 653 | cmd["channel"] = channel 654 | return self.interact("POST", "alerts/raise_alert.cmd", payload=cmd, 655 | only_status=True) 656 | 657 | def alerts(self, ts_sec=0, ts_usec=0): 658 | """Return alert object. 659 | 660 | Deprecated. 661 | Use :py:meth:`kismet_rest.Alerts.all` instead. 662 | 663 | Fetch alert object, containing metadata and list of alerts, optionally 664 | filtered to alerts since a given timestamp 665 | 666 | Args: 667 | ts_sec (int): Timestamp seconds (Unix epoch) 668 | ts_usec (int): Timestamp microseconds 669 | 670 | Return: 671 | dict: Dictionary containing metadata and a list of alerts. Keys 672 | represented in output: ``'kismet.alert.timestamp``, 673 | ``kismet.alert.list``. 674 | """ 675 | url = "alerts/last-time/{}.{}/alerts.json".format(ts_sec, ts_usec) 676 | return self.interact("GET", url) 677 | 678 | def messages(self, ts_sec=0, ts_usec=0): 679 | """Return message object. 680 | 681 | Deprecated. 682 | Use :py:meth:`kismet_rest.Messages.all` instead. 683 | 684 | Fetch message object, containing metadata and list of messages, 685 | optionally filtered to messages since a given timestamp 686 | 687 | Args: 688 | ts_sec (int): Timestamp seconds (Unix epoch) 689 | ts_usec (int): Timestamp microseconds 690 | 691 | Return: 692 | dict: Dictionary containing metadata and a list of messages. 693 | Top-level keys: ``kismet.messagebus.timestamp``, 694 | ``kismet.messagebus.list`` 695 | """ 696 | url = "messagebus/last-time/{}.{}/messages.json".format(ts_sec, 697 | ts_usec) 698 | return self.interact("GET", url) 699 | 700 | def location(self): 701 | """Return the gps location. 702 | 703 | Deprecated. 704 | Use :py:meth:`kismet_rest.GPS.current_location` instead. 705 | 706 | Return: 707 | dict: Dictionary object describing current location of Kismet 708 | server. Keys represented in output: 709 | ``kismet.common.location.lat``, 710 | ``kismet.common.location.lon``, 711 | ``kismet.common.location.alt``, 712 | ``kismet.common.location.heading``, 713 | ``kismet.common.location.speed``, 714 | ``kismet.common.location.time_sec``, 715 | ``kismet.common.location.time_usec``, 716 | ``kismet.common.location.fix``, 717 | ``kismet.common.location.valid`` 718 | """ 719 | return self.interact("GET", "gps/location.json") 720 | 721 | 722 | if __name__ == "__main__": 723 | print(KismetConnector().system_status()) 724 | -------------------------------------------------------------------------------- /kismet_rest/logger.py: -------------------------------------------------------------------------------- 1 | """All logging happens via this interface.""" 2 | import logging 3 | import os 4 | 5 | 6 | class Logger(object): 7 | """All logging happens here.""" 8 | 9 | def __init__(self): 10 | """If ${DEBUG} env var is set to "True", level will be set to debug.""" 11 | self.logger = logging.getLogger(__name__) 12 | msg_format = "%(asctime)-15s %(levelname)s %(name)s %(message)s" 13 | logging.basicConfig(format=msg_format) 14 | if os.getenv("DEBUG", "") in ["True", "true"]: 15 | self.set_debug() 16 | else: 17 | self.set_info() 18 | 19 | def set_debug(self): 20 | """Set logging to debug.""" 21 | self.logger.setLevel(logging.DEBUG) 22 | 23 | def set_info(self): 24 | """Set logging to info.""" 25 | self.logger.setLevel(logging.INFO) 26 | 27 | def critical(self, message): 28 | """Log a critical message.""" 29 | self.logger.critical(message) 30 | 31 | def error(self, message): 32 | """Log an error message.""" 33 | self.logger.error(message) 34 | 35 | def warn(self, message): 36 | """Log a warning message.""" 37 | self.logger.warning(message) 38 | 39 | def info(self, message): 40 | """Log an info message.""" 41 | self.logger.info(message) 42 | 43 | def debug(self, message): 44 | """Log a debug message.""" 45 | self.logger.debug(message) 46 | -------------------------------------------------------------------------------- /kismet_rest/messages.py: -------------------------------------------------------------------------------- 1 | """Messages abstraction.""" 2 | 3 | from .base_interface import BaseInterface 4 | 5 | 6 | class Messages(BaseInterface): 7 | """Messages abstraction.""" 8 | 9 | kwargs_defaults = {"ts_sec": 0, "ts_usec": 0} 10 | url_template = "messagebus/last-time/{ts_sec}.{ts_usec}/messages.json" 11 | 12 | def all(self, callback=None, callback_args=None, **kwargs): 13 | """Yield all messages, one at a time. 14 | 15 | If callback is set, nothing will be returned. 16 | 17 | Args: 18 | callback: Callback function. 19 | callback_args: Arguments for callback. 20 | 21 | Keyword args: 22 | ts_sec (int): Seconds since epoch for first message retrieved. 23 | ts_usec (int): Microseconds modifier for ts_sec query argument. 24 | 25 | Yield: 26 | dict: Message json, or None if callback is set. 27 | """ 28 | callback_settings = {} 29 | if callback: 30 | callback_settings["callback"] = callback 31 | if callback_args: 32 | callback_settings["callback_args"] = callback_args 33 | query_args = self.kwargs_defaults.copy() 34 | query_args.update(kwargs) 35 | url = self.url_template.format(**query_args) 36 | for result in self.interact_yield("GET", url, **callback_settings): 37 | yield result 38 | -------------------------------------------------------------------------------- /kismet_rest/packetchain.py: -------------------------------------------------------------------------------- 1 | """Packet processing endpoint.""" 2 | import datetime 3 | 4 | from .base_interface import BaseInterface 5 | from .exceptions import KismetServiceError 6 | 7 | 8 | class Packetchain(BaseInterface): 9 | """Wrap all interaction with /packetchain/ endpoint.""" 10 | 11 | def _order_rrd(data, rrdtype, timestamp): 12 | """Re-order a RRD ring 13 | 14 | Args: 15 | data (array) RRD data ring 16 | 17 | rrdtype (string) rrd type 18 | 19 | timestamp (number) serialization timestamp 20 | """ 21 | 22 | ts_slot = 0 23 | 24 | if rrdtype == "minute": 25 | ts_slot = (timestamp % len(data)) 26 | elif rrdtype == "hour": 27 | ts_slot = int(timestamp / 60) % len(data) 28 | elif rrdtype == "day": 29 | ts_slot = int(timestamp / 3600) % len(data) 30 | else: 31 | msg = "Unknown rrd type {}".format(rrdtype) 32 | raise KismetServiceError(msg, -1) 33 | 34 | return data[ts_slot + 1:] + data[:ts_slot + 1] 35 | 36 | def get_packet_stats(self, category, timeline): 37 | """Get the packet statistics for a given category and timeline. 38 | 39 | Args: 40 | category (str or array): Category to get packets from; 41 | processed 42 | dropped 43 | queued 44 | peak 45 | dupe 46 | packets 47 | 48 | timeline (str): Timeline to fetch packets from; 49 | minute 50 | hour 51 | day 52 | 53 | Return: 54 | Array of RRD data or array of arrays of RRD data if multiple 55 | categories are requested 56 | """ 57 | 58 | categories = { 59 | "processed": "kismet.packetchain.processed_packets_rrd", 60 | "dropped": "kismet.packetchain.dropped_packets_rrd", 61 | "queued": "kismet.packetchain.queued_packets_rrd", 62 | "peak": "kismet.packetchain.peak_packets_rrd", 63 | "dupe": "kismet.packetchain.dupe_packets_rrd", 64 | "packets": "kismet.packetchain.packets_rrd", 65 | } 66 | 67 | times = { 68 | "minute": "kismet.common.rrd.minute_vec", 69 | "hour": "kismet.common.rrd.hour_vec", 70 | "day": "kismet.common.rrd.day_vec", 71 | } 72 | 73 | if isinstance(category, list): 74 | for c in category: 75 | if not c in categories: 76 | msg = "Invalid category: {}".format(c) 77 | raise KismetServiceError(msg, -1) 78 | else: 79 | if not category in categories: 80 | msg = "Invalid category: {}".format(category) 81 | raise KismetServiceError(msg, -1) 82 | 83 | if not timeline in times: 84 | msg = "Invalid timeline: {}".format(timeline) 85 | raise KismetServiceError(msg, -1) 86 | 87 | fields = [] 88 | 89 | if isinstance(category, list): 90 | for c in category: 91 | f1 = "{}/kismet.common.rrd.serial_time".format(categories[c]) 92 | f2 = "{}_time".format(c) 93 | fields.append([f1, f2]) 94 | 95 | f1 = "{}/{}".format(categories[c], times[timeline]) 96 | f2 = "{}_data".format(c) 97 | fields.append([f1, f2]) 98 | else: 99 | f1 = "{}/kismet.common.rrd.serial_time".format(categories[category]) 100 | f2 = "{}_time".format(category) 101 | fields.append([f1, f2]) 102 | 103 | f1 = "{}/{}".format(categories[category], times[timeline]) 104 | f2 = "{}_data".format(category) 105 | fields.append([f1, f2]) 106 | 107 | payload = {"fields": fields} 108 | data = self.interact("POST", "packetchain/packet_stats.json", payload=payload) 109 | 110 | if isinstance(category, list): 111 | ret = [] 112 | 113 | for c in category: 114 | f1 = "{}_data".format(c) 115 | f2 = "{}_time".format(c) 116 | 117 | if not f1 in data or not f2 in data: 118 | msg = "Missing response {} / {} in data".format(f1, f2) 119 | raise KismetServiceError(msg, -1) 120 | 121 | ret.append(Packetchain._order_rrd(data[f1], timeline, data[f2])) 122 | 123 | return ret 124 | else: 125 | f1 = "{}_data".format(category) 126 | f2 = "{}_time".format(category) 127 | 128 | if not f1 in data or not f2 in data: 129 | msg = "Missing response {} / {} in data".format(f1, f2) 130 | raise KismetServiceError(msg, -1) 131 | 132 | return Packetchain._order_rrd(data[f1], timeline, data[f2]) 133 | 134 | 135 | -------------------------------------------------------------------------------- /kismet_rest/system.py: -------------------------------------------------------------------------------- 1 | """Abstraction for system endpoint.""" 2 | import datetime 3 | 4 | from .base_interface import BaseInterface 5 | 6 | 7 | class System(BaseInterface): 8 | """Wrap all interaction with /system/ endpoint.""" 9 | 10 | def get_status(self): 11 | """Return json representing Kismet system status.""" 12 | return self.interact("GET", "system/status.json") 13 | 14 | def get_system_time(self, time_format=None): 15 | """Return current time from Kismet REST API. 16 | 17 | Args: 18 | format (str or None): Format time before returning. Supported 19 | formats: None (return as dict), ``iso`` (ISO 8601). Defaults 20 | to None. 21 | """ 22 | from_api = self.interact("GET", "system/timestamp.json") 23 | if time_format is None: 24 | return from_api 25 | if time_format == "iso": 26 | seconds = from_api["kismet.system.timestamp.sec"] 27 | u_seconds = from_api["kismet.system.timestamp.usec"] 28 | timestamp = float(float(seconds) + (u_seconds / 1000000.0)) 29 | return datetime.datetime.fromtimestamp(timestamp).isoformat() 30 | raise ValueError("Invalid system time format: {}".format(format)) 31 | -------------------------------------------------------------------------------- /kismet_rest/utility.py: -------------------------------------------------------------------------------- 1 | """General utility functions located here.""" 2 | 3 | import os 4 | import re 5 | try: 6 | from urlparse import urljoin 7 | except ImportError: 8 | from urllib.parse import urljoin 9 | 10 | 11 | class Utility(object): 12 | """Utility class.""" 13 | 14 | @classmethod 15 | def build_full_url(cls, base_url, url_path): 16 | """Return a complete, well-formed URL. 17 | 18 | Args: 19 | base_url (str): Protocol, FQDN, port, and optional base path for 20 | query. Base path is unnecessary unless Kismet is behind a 21 | proxy. 22 | url_path (str): Relative URL path to Kismet API endpoint. 23 | 24 | Returns: 25 | str: Complete URL. 26 | 27 | """ 28 | base = "{}/".format(base_url.rstrip("/")) # Must always be one "/" 29 | result = urljoin(base, url_path.lstrip("/")) 30 | return result 31 | 32 | @classmethod 33 | def get_lib_version(cls): 34 | """Get version of kismet_Rest library.""" 35 | here_dir = os.path.abspath(os.path.dirname(__file__)) 36 | initfile = os.path.join(here_dir, "__init__.py") 37 | raw_init_file = cls.readfile(initfile) 38 | rx_compiled = re.compile(r"\s*__version__\s*=\s*\"(\S+)\"") 39 | ver = rx_compiled.search(raw_init_file).group(1) 40 | return ver 41 | 42 | @classmethod 43 | def readfile(cls, file_name): 44 | """Return contents of a file as a string.""" 45 | with open(file_name, 'r') as file: 46 | filestring = file.read() 47 | return filestring 48 | -------------------------------------------------------------------------------- /run_all_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xe 3 | 4 | echo "kismet.pcap file REQUIRED for testing." 5 | 6 | docker kill kismet; docker rm kismet || echo "No Kismet server artifacts" 7 | 8 | # Build Kismet from source 9 | docker build -t kismet-build -f ./dockerfiles/Dockerfile.kismet-app-master . 10 | 11 | # Build Kismet container 12 | docker build -t kismet-package -f ./dockerfiles/Dockerfile.kismet-app . 13 | 14 | # Build testing image for Python 2.7 15 | docker build -t kismet-rest:2.7 -f ./dockerfiles/Dockerfile.kismet-rest_2.7 . 16 | 17 | # Build testing image for Python 3.5 18 | docker build -t kismet-rest:ubu16 -f ./dockerfiles/Dockerfile.kismet-rest_ubu16 . 19 | 20 | # Build testing image for Python 3.7 21 | docker build -t kismet-rest:3.7 -f ./dockerfiles/Dockerfile.kismet-rest_3.7 . 22 | 23 | 24 | echo "Testing REST SDK (Ubuntu:16.04, Python 3.5) against master branch..." 25 | # Start Kismet container, load pcap. 26 | docker run \ 27 | -d \ 28 | --name=kismet \ 29 | --network=host \ 30 | --cap-add=SYS_PTRACE \ 31 | -v ${PWD}/kismet.pcap:/export/kismet.pcap \ 32 | kismet-build 33 | 34 | sleep 15 35 | 36 | docker run \ 37 | -it \ 38 | --rm \ 39 | --network=host \ 40 | --name=kismet-rest_ubu16 \ 41 | kismet-rest:ubu16 42 | 43 | docker kill kismet 44 | docker rm --force kismet 45 | 46 | echo "Testing REST SDK (Py2.7) against master branch..." 47 | # Start Kismet container, load pcap. 48 | docker run \ 49 | -d \ 50 | --name=kismet \ 51 | --network=host \ 52 | --cap-add=SYS_PTRACE \ 53 | -v ${PWD}/kismet.pcap:/export/kismet.pcap \ 54 | kismet-build 55 | 56 | sleep 15 57 | 58 | # Run tests for Python 2.7 59 | docker run \ 60 | -it \ 61 | --rm \ 62 | --network=host \ 63 | --name=kismet-rest_2.7 \ 64 | kismet-rest:2.7 65 | 66 | docker kill kismet 67 | docker rm kismet 68 | 69 | echo "Testing REST SDK (Py3.7) against master branch..." 70 | # Start Kismet container, load pcap. 71 | docker run \ 72 | -d \ 73 | --name=kismet \ 74 | --network=host \ 75 | --cap-add=SYS_PTRACE \ 76 | -v ${PWD}/kismet.pcap:/export/kismet.pcap \ 77 | kismet-build 78 | 79 | sleep 15 80 | 81 | # Run tests for Python 3.7 82 | docker run \ 83 | -it \ 84 | --rm \ 85 | --network=host \ 86 | --name=kismet-rest_3.7 \ 87 | kismet-rest:3.7 88 | 89 | 90 | # Kill and delete Kismet server container 91 | # docker logs kismet 92 | docker kill kismet 93 | docker rm --force kismet 94 | 95 | 96 | echo "Testing REST SDK (Py2.7) against current dpkg..." 97 | 98 | # Start Kismet container, load pcap. 99 | docker run \ 100 | -d \ 101 | --name=kismet \ 102 | --network=host \ 103 | --cap-add=SYS_PTRACE \ 104 | -v ${PWD}/kismet.pcap:/export/kismet.pcap \ 105 | kismet-package 106 | 107 | sleep 15 108 | 109 | # Run tests for Python 2.7 110 | docker run \ 111 | -it \ 112 | --rm \ 113 | --network=host \ 114 | --name=kismet-rest_2.7 \ 115 | kismet-rest:2.7 116 | 117 | docker kill kismet && docker rm kismet 118 | # exit $? 119 | 120 | echo "Testing REST SDK (Py3.7) against current dpkg..." 121 | 122 | # Start Kismet container, load pcap. 123 | docker run \ 124 | -d \ 125 | --name=kismet \ 126 | --network=host \ 127 | --cap-add=SYS_PTRACE \ 128 | -v ${PWD}/kismet.pcap:/export/kismet.pcap \ 129 | kismet-package 130 | 131 | sleep 15 132 | 133 | # Run tests for Python 3.7 134 | docker run \ 135 | -it \ 136 | --rm \ 137 | --network=host \ 138 | --name=kismet-rest_3.7 \ 139 | kismet-rest:3.7 140 | 141 | 142 | # Kill and delete Kismet server container 143 | # docker logs kismet 144 | docker kill kismet 145 | docker rm --force kismet 146 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Python setup.""" 4 | import os 5 | import re 6 | from setuptools import setup 7 | 8 | 9 | def read(file_name): 10 | """Return the contents of a file as a string.""" 11 | with open(os.path.join(os.path.dirname(__file__), file_name), 'r') as file: 12 | filestring = file.read() 13 | return filestring 14 | 15 | 16 | def get_version(): 17 | """Return the version of this module.""" 18 | raw_init_file = read("kismet_rest/__init__.py") 19 | rx_compiled = re.compile(r"\s*__version__\s*=\s*\"(\S+)\"") 20 | ver = rx_compiled.search(raw_init_file).group(1) 21 | return ver 22 | 23 | 24 | def build_long_desc(): 25 | """Return the long description.""" 26 | return "\n".join([read(f) for f in ["README.rst", "CHANGELOG.rst"]]) 27 | 28 | 29 | setup(name="kismet_rest", 30 | version=get_version(), 31 | author="Mike Kershaw / Dragorn", 32 | author_email="dragorn@kismetwireless.net", 33 | description="Simplified Python API for the Kismet REST interface", 34 | license="GPLv2", 35 | keywords="kismet", 36 | url="https://www.kismetwireless.net", 37 | download_url="https://kismetwireless.net/python-kismet-rest", 38 | packages=["kismet_rest"], 39 | install_requires="requests", 40 | long_description=build_long_desc(), 41 | classifiers=[ 42 | "Development Status :: 5 - Production/Stable", 43 | "Intended Audience :: Developers", 44 | "Operating System :: MacOS :: MacOS X", 45 | "Operating System :: POSIX :: Linux", 46 | "Programming Language :: Python :: 2.7", 47 | "Programming Language :: Python :: 3.5", 48 | "Programming Language :: Python :: 3.6", 49 | "Programming Language :: Python :: 3.7", 50 | "Topic :: Security", 51 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)" 52 | ]) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kismetwireless/python-kismet-rest/6b1ae410745dc3bee2ea859819962beb693f53e4/tests/__init__.py -------------------------------------------------------------------------------- /tests/integration/test_integration_alerts.py: -------------------------------------------------------------------------------- 1 | """Test kismet_rest.Alerts abstraction.""" 2 | import pprint 3 | 4 | import kismet_rest 5 | 6 | 7 | class TestIntegrationAlerts(object): 8 | """Test Alerts().""" 9 | 10 | def test_alerts_all(self): 11 | """Test getting alerts.""" 12 | alerts = kismet_rest.Alerts(username="admin", password="passwordy") 13 | all_alerts = alerts.all() 14 | for alert in all_alerts: 15 | assert isinstance(alert, dict) 16 | 17 | def test_alerts_callback(self): 18 | """Test alerts with callback.""" 19 | alerts = kismet_rest.Alerts(username="admin", password="passwordy") 20 | callback = pprint.pprint 21 | all_alerts = alerts.all(callback) 22 | assert all_alerts 23 | for alert in all_alerts: 24 | assert isinstance(alert, None) 25 | -------------------------------------------------------------------------------- /tests/integration/test_integration_base_interface.py: -------------------------------------------------------------------------------- 1 | """Integration tests for kismet_rest.BaseInterface.""" 2 | import json 3 | 4 | import kismet_rest 5 | 6 | 7 | class TestIntegrationBaseInterface(object): 8 | """Test BaseInterface class.""" 9 | 10 | def test_base_interface_instantiate_with_defaults(self): 11 | """Initiate BaseInterface with defaults, check attributes.""" 12 | interface = kismet_rest.BaseInterface(username="admin", 13 | password="passwordy") 14 | assert interface 15 | assert interface.host_uri == "http://127.0.0.1:2501" 16 | assert interface.username == "admin" 17 | assert interface.password == "passwordy" 18 | 19 | def test_base_interface_get_kismet_version(self): 20 | """Test the ability to get version from Kismet.""" 21 | interface = kismet_rest.BaseInterface(username="admin", 22 | password="passwordy") 23 | version = interface.get_kismet_version() 24 | print(json.dumps(version, indent=4)) 25 | # We need to fix this when version is published in API. 26 | assert version 27 | -------------------------------------------------------------------------------- /tests/integration/test_integration_datasources.py: -------------------------------------------------------------------------------- 1 | """Test kismet_rest.Datasources abstraction.""" 2 | import kismet_rest 3 | 4 | 5 | class TestIntegrationDatasources(object): 6 | """Test Datasources().""" 7 | 8 | def create_authenticated_session(self): 9 | """Return an authenticated session.""" 10 | return kismet_rest.Datasources(username="admin", 11 | password="passwordy", 12 | debug=True) 13 | 14 | def test_datasources_all(self): 15 | """Test getting datasources.""" 16 | datasources = self.create_authenticated_session() 17 | all_sources = datasources.all() 18 | for source in all_sources: 19 | assert isinstance(source, dict) 20 | 21 | def test_datasources_interfaces(self): 22 | """Test getting datasources.""" 23 | datasources = self.create_authenticated_session() 24 | all_sources = datasources.interfaces() 25 | for source in all_sources: 26 | assert isinstance(source, dict) 27 | 28 | def test_datasource_add_datasource(self): 29 | """Add pcap file datasource.""" 30 | datasources = self.create_authenticated_session() 31 | source = "/export/kismet.pcap:name=reconsume_this_yo" 32 | datasources.login() 33 | datasources.add(source) 34 | -------------------------------------------------------------------------------- /tests/integration/test_integration_devices.py: -------------------------------------------------------------------------------- 1 | """Test kismet_rest.Devices abstraction.""" 2 | import pprint 3 | 4 | import kismet_rest 5 | 6 | 7 | class TestIntegrationDevices(object): 8 | """Test Devices().""" 9 | 10 | def create_authenticated_session(self): 11 | return kismet_rest.Devices(username="admin", 12 | password="passwordy", 13 | debug=True) 14 | 15 | def test_devices_all(self): 16 | """Test getting devices.""" 17 | devices = self.create_authenticated_session() 18 | all_devices = devices.all() 19 | for device in all_devices: 20 | assert isinstance(device, dict) 21 | 22 | def test_devices_callback(self): 23 | """Test devices with callback.""" 24 | devices = self.create_authenticated_session() 25 | callback = pprint.pprint 26 | all_devices = devices.all(callback) 27 | assert all_devices 28 | for device in all_devices: 29 | assert isinstance(device, None) 30 | 31 | def test_devices_by_mac(self): 32 | """Test getting devices.""" 33 | devices = self.create_authenticated_session() 34 | device_list = ["00:00:00:00:00:00/00:00:00:00:00:00"] 35 | all_devices = devices.by_mac(devices=device_list) 36 | for device in all_devices: 37 | assert isinstance(device, dict) 38 | 39 | def test_devices_by_mac_callback(self): 40 | """Test devices with callback.""" 41 | devices = self.create_authenticated_session() 42 | callback = pprint.pprint 43 | device_list = ["00:00:00:00:00:00/00:00:00:00:00:00"] 44 | all_devices = devices.by_mac(callback, devices=device_list) 45 | assert all_devices 46 | for device in all_devices: 47 | assert isinstance(device, None) 48 | 49 | def test_devices_dot11_clients_of(self): 50 | """Test getting clients of device.""" 51 | devices = self.create_authenticated_session() 52 | target_devices = [x for x in devices.all()] 53 | target = target_devices[0]["kismet.device.base.key"] 54 | for client in devices.dot11_clients_of(target): 55 | assert isinstance(client, dict) 56 | 57 | def test_devices_dot11_access_points(self): 58 | """Test getting dot11 access points.""" 59 | devices = self.create_authenticated_session() 60 | for access_point in devices.dot11_access_points(): 61 | assert isinstance(access_point, dict) 62 | 63 | def test_get_device_by_key(self): 64 | """Test getting clients of device.""" 65 | devices = self.create_authenticated_session() 66 | target_devices = [x for x in devices.all()] 67 | target = target_devices[0]["kismet.device.base.key"] 68 | result = devices.by_key(target) 69 | assert isinstance(result, dict) 70 | -------------------------------------------------------------------------------- /tests/integration/test_integration_legacy.py: -------------------------------------------------------------------------------- 1 | """Integration tests for legacy KismetConnector object.""" 2 | import pprint 3 | import sys 4 | 5 | import pytest 6 | 7 | import kismet_rest 8 | from kismet_rest.exceptions import KismetLoginException 9 | 10 | 11 | class TestIntegrationLegacy(object): 12 | def instantiate_kismet_connector_noauth(self): 13 | return kismet_rest.KismetConnector(debug=True) 14 | 15 | def instantiate_kismet_connector_auth(self): 16 | return kismet_rest.KismetConnector(username="admin", 17 | password="passwordy", 18 | debug=True) 19 | 20 | def printy_callback(self, json_object): 21 | print("Printing via a callback...") 22 | pprint.pprint(json_object) 23 | 24 | def printy_callback_args(self, json_object, arg1, arg2): 25 | print("Printing via a callback...") 26 | print("{} {}".format(arg1, arg2)) 27 | pprint.pprint(json_object) 28 | 29 | def test_kismet_connector_instantiate(self): 30 | assert self.instantiate_kismet_connector_noauth() 31 | 32 | def test_kismet_connector_list_devices(self): 33 | connector = self.instantiate_kismet_connector_noauth() 34 | result = connector.device_summary() 35 | assert isinstance(result, list) 36 | assert isinstance(result[0], dict) 37 | 38 | def test_kismet_connector_list_devices_with_callback(self): 39 | connector = self.instantiate_kismet_connector_noauth() 40 | callback = self.printy_callback 41 | result = connector.device_summary(callback) 42 | assert result == [] 43 | 44 | def test_kismet_connector_list_devices_with_callback_args(self): 45 | connector = self.instantiate_kismet_connector_noauth() 46 | callback = self.printy_callback_args 47 | cbargs = ["First_arg", "somesuch other second thing..."] 48 | result = connector.device_summary(callback, cbargs) 49 | assert result == [] 50 | 51 | def test_kismet_connector_device_summary_since(self): 52 | connector = self.instantiate_kismet_connector_noauth() 53 | callback = self.printy_callback 54 | result = connector.device_summary_since() 55 | assert isinstance(result, list) 56 | 57 | def test_kismet_connector_smart_device_summary_since(self): 58 | connector = self.instantiate_kismet_connector_noauth() 59 | callback = self.printy_callback 60 | result = connector.smart_summary_since() 61 | assert isinstance(result, list) 62 | 63 | def test_kismet_connector_get_gps(self): 64 | connector = self.instantiate_kismet_connector_auth() 65 | result = connector.location() 66 | print(result.keys()) 67 | assert isinstance(result, dict) 68 | 69 | def test_kismet_connector_messages(self): 70 | connector = self.instantiate_kismet_connector_noauth() 71 | result = connector.messages() 72 | print(result) 73 | assert isinstance(result, dict) 74 | 75 | def test_kismet_connector_alerts(self): 76 | connector = self.instantiate_kismet_connector_noauth() 77 | result = connector.alerts() 78 | print(result.keys()) 79 | assert isinstance(result, dict) 80 | 81 | def test_kismet_connector_datasource_list_interfaces(self): 82 | connector = self.instantiate_kismet_connector_auth() 83 | interfaces = [x for x in connector.datasource_list_interfaces()] 84 | assert isinstance(interfaces, list) 85 | 86 | def test_kismet_connector_datasources(self): 87 | connector = self.instantiate_kismet_connector_noauth() 88 | result = connector.datasources() 89 | assert isinstance(result, list) 90 | 91 | def test_kismet_connector_dot11_access_points(self): 92 | connector = self.instantiate_kismet_connector_noauth() 93 | result = connector.dot11_access_points() 94 | print(sorted(result[0].keys())) 95 | assert isinstance(result, list) 96 | 97 | def test_kismet_connector_smart_device_list(self): 98 | connector = self.instantiate_kismet_connector_noauth() 99 | result = connector.smart_device_list() 100 | print(sorted(result[0].keys())) 101 | assert isinstance(result, list) 102 | 103 | def test_kismet_connector_system_status(self): 104 | connector = self.instantiate_kismet_connector_noauth() 105 | result = connector.system_status() 106 | print(result) 107 | assert isinstance(result, dict) 108 | 109 | def test_kismet_connector_list_devices_by_key(self): 110 | connector = self.instantiate_kismet_connector_noauth() 111 | target_device = connector.smart_device_list()[0] 112 | target_device_key = target_device["kismet.device.base.key"] 113 | result = connector.device(target_device_key) 114 | print(result) 115 | assert isinstance(result, dict) 116 | 117 | def test_kismet_connector_list_device_by_mac(self): 118 | connector = self.instantiate_kismet_connector_noauth() 119 | target_device = connector.smart_device_list()[0] 120 | target_device_key = target_device["kismet.device.base.macaddr"] 121 | result = connector.device_by_mac(target_device_key) 122 | print(result) 123 | assert isinstance(result, list) 124 | 125 | def test_kismet_connector_dot11_clients_of(self): 126 | connector = self.instantiate_kismet_connector_noauth() 127 | target_ap = connector.dot11_access_points()[0]["kismet.device.base.key"] # NOQA 128 | devices = connector.dot11_clients_of(target_ap) 129 | print(target_ap) 130 | print(devices) 131 | assert isinstance(devices, list) 132 | 133 | def test_kismet_connector_list_devices_by_key_field(self): 134 | connector = self.instantiate_kismet_connector_noauth() 135 | target_device = connector.smart_device_list()[0] 136 | target_device_key = target_device["kismet.device.base.key"] 137 | field = "kismet.device.base.type" 138 | result = connector.device(target_device_key, field) 139 | print(result) 140 | print(type(result)) 141 | if sys.version_info[0] < 3: 142 | assert isinstance(result, basestring) 143 | else: 144 | assert isinstance(result, str) 145 | 146 | def test_kismet_connector_raise_alert(self): 147 | connector = self.instantiate_kismet_connector_auth() 148 | name = "ScaryAlert" 149 | text = "Super scary things on your wireless." 150 | assert connector.raise_alert(name, text) 151 | 152 | def test_kismet_connector_define_alert(self): 153 | connector = self.instantiate_kismet_connector_noauth() 154 | name = "ScaryAlert" 155 | description = "Super scary things on your wireless." 156 | assert connector.define_alert(name, description) 157 | 158 | def test_kismet_connector_add_datasource_fail(self): 159 | connector = self.instantiate_kismet_connector_noauth() 160 | source = "MY_DATA_SOURCE" 161 | with pytest.raises(KismetLoginException): 162 | connector.add_datasource(source) 163 | 164 | def test_kismet_connector_config_datasource_set_hop_fail(self): 165 | connector = self.instantiate_kismet_connector_noauth() 166 | target_datasource = connector.datasources()[0] 167 | uuid = target_datasource["kismet.datasource.uuid"] 168 | with pytest.raises(KismetLoginException): 169 | connector.config_datasource_set_hop(uuid) 170 | 171 | def test_kismet_connector_config_datasource_set_hop_channels(self): 172 | connector = self.instantiate_kismet_connector_noauth() 173 | target_datasource = connector.datasources()[0] 174 | uuid = target_datasource["kismet.datasource.uuid"] 175 | rate = "123" 176 | channels = [1, 2, 3] 177 | with pytest.raises(KismetLoginException): 178 | connector.config_datasource_set_hop_channels(uuid, rate, channels) 179 | 180 | def test_kismet_connector_config_datasource_set_hop_rate(self): 181 | connector = self.instantiate_kismet_connector_noauth() 182 | target_datasource = connector.datasources()[0] 183 | uuid = target_datasource["kismet.datasource.uuid"] 184 | rate = "123" 185 | with pytest.raises(KismetLoginException): 186 | connector.config_datasource_set_hop_rate(uuid, rate) 187 | 188 | def test_kismet_connector_config_datasource_set_channel(self): 189 | connector = self.instantiate_kismet_connector_noauth() 190 | target_datasource = connector.datasources()[0] 191 | uuid = target_datasource["kismet.datasource.uuid"] 192 | channel = "1" 193 | with pytest.raises(KismetLoginException): 194 | connector.config_datasource_set_channel(uuid, channel) 195 | 196 | def test_kismet_connector_add_datasource(self): 197 | connector = self.instantiate_kismet_connector_auth() 198 | source = "/export/kismet.pcap:name=reconsume_this_yo" 199 | connector.login() 200 | connector.add_datasource(source) 201 | -------------------------------------------------------------------------------- /tests/integration/test_integration_messages.py: -------------------------------------------------------------------------------- 1 | """Test kismet_rest.Messages abstraction.""" 2 | 3 | import kismet_rest 4 | 5 | 6 | class TestIntegrationMessages(object): 7 | """Test Messages().""" 8 | 9 | def create_authenticated_session(self): 10 | """Return an authenticated session.""" 11 | return kismet_rest.Messages(username="admin", 12 | password="passwordy", 13 | debug=True) 14 | 15 | def test_messages_all(self): 16 | """Test getting messages.""" 17 | messages = self.create_authenticated_session() 18 | all_messages = messages.all() 19 | for message in all_messages: 20 | assert isinstance(message, dict) 21 | -------------------------------------------------------------------------------- /tests/integration/test_integration_system.py: -------------------------------------------------------------------------------- 1 | """Test kismet_rest.System abstraction.""" 2 | import json 3 | 4 | import pytest 5 | 6 | import kismet_rest 7 | 8 | 9 | class TestIntegrationSystem(object): 10 | """Test System().""" 11 | 12 | def create_authenticated_session(self): 13 | return kismet_rest.System(username="admin", 14 | password="passwordy", 15 | debug=True) 16 | 17 | def test_system_get_status(self): 18 | """Test getting system status.""" 19 | system = self.create_authenticated_session() 20 | status = system.get_status() 21 | print(json.dumps(status, indent=4)) 22 | assert isinstance(status, dict) 23 | 24 | def test_system_get_system_time(self): 25 | """Test retrieval of system time.""" 26 | system = self.create_authenticated_session() 27 | system_time = system.get_system_time() 28 | print(json.dumps(system_time, indent=4)) 29 | assert isinstance(system_time, dict) 30 | 31 | def test_system_get_system_time_isoformat(self): 32 | """Test getting time in ISO format.""" 33 | system = self.create_authenticated_session() 34 | system_time = system.get_system_time("iso") 35 | print(system_time) 36 | assert isinstance(system_time, str) 37 | 38 | def test_system_get_system_time_badformat(self): 39 | """Test exception emitted from bad system time request.""" 40 | system = self.create_authenticated_session() 41 | with pytest.raises(ValueError): 42 | system.get_system_time("bad") 43 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kismetwireless/python-kismet-rest/6b1ae410745dc3bee2ea859819962beb693f53e4/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_unit_logger.py: -------------------------------------------------------------------------------- 1 | """Test Logger.""" 2 | import kismet_rest 3 | 4 | 5 | class TestUnitLogger(object): 6 | """Test the Logger.""" 7 | 8 | def test_all_log_methods(self): 9 | """Test each log level.""" 10 | logger = kismet_rest.Logger() 11 | logger.set_info() 12 | logger.set_debug() 13 | for x in [logger.critical, logger.error, logger.warn, logger.info, 14 | logger.debug]: 15 | x("test message") 16 | assert True 17 | -------------------------------------------------------------------------------- /tests/unit/test_unit_utility.py: -------------------------------------------------------------------------------- 1 | """Test the Utility class.""" 2 | import kismet_rest 3 | 4 | 5 | class TestUnitUtility(object): 6 | """Test the Utility class.""" 7 | 8 | def test_unit_utility_build_full_url_simple(self): 9 | """Test the URL builder with relative path.""" 10 | base = "http://localhost:2501" 11 | path = "v1/devices" 12 | control = "http://localhost:2501/v1/devices" 13 | result = kismet_rest.Utility.build_full_url(base, path) 14 | assert result == control 15 | 16 | def test_unit_utility_build_full_url_leading_slash(self): 17 | """Test the URL builder's handling of a leading slash.""" 18 | base = "http://localhost:2501" 19 | path = "/v1/devices" 20 | control = "http://localhost:2501/v1/devices" 21 | result = kismet_rest.Utility.build_full_url(base, path) 22 | assert result == control 23 | 24 | def test_unit_utility_build_full_url_double_leading_slash(self): 25 | """Test building URL with double leading slash.""" 26 | base = "http://localhost:2501" 27 | path = "//v1/devices" 28 | control = "http://localhost:2501/v1/devices" 29 | result = kismet_rest.Utility.build_full_url(base, path) 30 | assert result == control 31 | 32 | def test_unit_utility_build_full_url_proxypath(self): 33 | """Test URL building with proxy path.""" 34 | base = "http://frontend.proxy:2501/kismetpath/" 35 | path = "v1/devices" 36 | control = "http://frontend.proxy:2501/kismetpath/v1/devices" 37 | result = kismet_rest.Utility.build_full_url(base, path) 38 | assert result == control 39 | 40 | def test_unit_utility_build_full_url_proxypath_no_trailing_slash(self): 41 | """Test building URL with proxy path and no trailing slash.""" 42 | base = "http://frontend.proxy:2501/kismetpath" 43 | path = "v1/devices" 44 | control = "http://frontend.proxy:2501/kismetpath/v1/devices" 45 | result = kismet_rest.Utility.build_full_url(base, path) 46 | assert result == control 47 | --------------------------------------------------------------------------------