Full Code of lanmaster53/honeybadger for AI

master a09a8c25c843 cached
35 files
140.4 KB
36.4k tokens
84 symbols
1 requests
Download .txt
Repository: lanmaster53/honeybadger
Branch: master
Commit: a09a8c25c843
Files: 35
Total size: 140.4 KB

Directory structure:
gitextract_vjpgv5fm/

├── .gitignore
├── LICENSE.txt
├── README.md
├── server/
│   ├── honeybadger/
│   │   ├── __init__.py
│   │   ├── constants.py
│   │   ├── decorators.py
│   │   ├── models.py
│   │   ├── parsers.py
│   │   ├── plugins.py
│   │   ├── processors.py
│   │   ├── static/
│   │   │   ├── badger.css
│   │   │   ├── badger.js
│   │   │   ├── common.js
│   │   │   ├── honey.jar
│   │   │   ├── normalize.css
│   │   │   ├── skeleton.css
│   │   │   └── sorttable.js
│   │   ├── templates/
│   │   │   ├── admin.html
│   │   │   ├── beacons.html
│   │   │   ├── demo.html
│   │   │   ├── layout.html
│   │   │   ├── log.html
│   │   │   ├── login.html
│   │   │   ├── map.html
│   │   │   ├── profile.html
│   │   │   ├── profile_activate.html
│   │   │   ├── register.html
│   │   │   └── targets.html
│   │   ├── utils.py
│   │   ├── validators.py
│   │   └── views.py
│   ├── honeybadger.py
│   └── requirements.txt
└── util/
    ├── wireless_survey.ps1
    └── wireless_survey.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.pyc
*sublime*
venv/
agents/
server_old/
data.db


================================================
FILE: LICENSE.txt
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    {one line to give the program's name and a brief idea of what it does.}
    Copyright (C) {year}  {name of author}

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    {project}  Copyright (C) {year}  {fullname}
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.


================================================
FILE: README.md
================================================
# HoneyBadger v2

HoneyBadger is a framework for targeted geolocation. While honeypots are traditionally used to passively detect malicious actors, HoneyBadger is an Active Defense tool to determine who the malicious actor is and where they are located. HoneyBadger leverages "agents" built in various technologies that harvest the requisite information from the target host in order to geolocate them. These agents report back to the HoneyBadger API, where the data is stored and made available in the HoneyBadger user interface.

An early prototype of HoneyBadger (v1) can be seen in the presentation "[Hide and Seek: Post-Exploitation Style](http://youtu.be/VJTrRMqHU5U)" from ShmooCon 2013. The associated Metasploit Framework modules mentioned in the above presentation can be found [here](https://github.com/v10l3nt/metasploit-framework/tree/master/modules/auxiliary/badger). Note: These modules have not been updated to work with v2 of the API.

## Getting Started

### Pre-requisites

* Python 3.x

### Installation (Ubuntu and OS X)

1. Install [pip](https://pip.pypa.io/en/stable/installing/).
2. Clone the HoneyBadger repository.

    ```
    $ git clone https://github.com/lanmaster53/honeybadger.git
    ```

3. Install the dependencies.

    ```
    $ cd honeybadger/server
    $ pip install -r requirements.txt
    ```

4. Initialize the database. The provided username and password will become the administrator account.

    ```
    $ python
    >>> import honeybadger
    >>> honeybadger.initdb(<username>, <password>)
    ```

5. Start the HoneyBadger server. API keys are required to use maps and geolocation services.

    ```
    $ python ./honeybadger.py -gk <GOOGLE_API_KEY> -ik <IPSTACK_API_KEY>
    ```

    Honeybadger will still run without the API keys, but mapping and geolocation functionality will be limited as a result.

    View usage information with either of the following:

   ```
   $ python ./honeybadger.py -h
   $ python ./honeybadger.py --help
   ```

6. Visit the application and authenticate.
7. Add users and targets as needed using their respective pages.
8. Deploy agents for the desired target.

Clicking the "demo" button next to any of the targets will launch a demo web page containing an `HTML`, `JavaScript`, and `Applet` agent for that target.

### Fresh Start

Make a mess and want to start over fresh? Do this.

```
$ python
>>> import honeybadger
>>> honeybadger.dropdb()
>>> honeybadger.initdb(<username>, <password>)
```

## API Usage

### IP Geolocation

This method geolocates the target based on the source IP of the request and assigns the resolved location to the given target and agent.

Example: (Method: `GET`)

```
http://<path:honeybadger>/api/beacon/<guid:target>/<string:agent>
```

### Known Coordinates

This method accepts previously resolved location data for the given target and agent.

Example: (Method: `GET`)

```
http://<path:honeybadger>/api/beacon/<guid:target>/<string:agent>?lat=<float:latitude>&lng=<float:longitude>&acc=<integer:accuracy>
```

### Wireless Survey

This method accepts wireless survey data and parses the information on the server-side, extracting what is needed to make a Google API geolocation call. The resolved geolocation data is then assigned to the given target. Parsers currently exist for survey data from Windows, Linux and OS X using the following commands:

Windows:

```
cmd.exe /c netsh wlan show networks mode=bssid | findstr "SSID Signal Channel"
```

The `util` directory contains a PowerShell script that can be used to automatically send test data to the server:

```
powershell .\wireless_survey.ps1 -uri <URI>
```

Linux:

```
/bin/sh -c iwlist scan | egrep 'Address|ESSID|Signal'
```

The `util` directory contains a shell script that can be used to automatically send test data to the server:

```
bash ./wireless_survey.sh <URL>
```

OS X:

```
/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -s
```

Example: (Method: `POST`)

```
http://<path:honeybadger>/api/beacon/<guid:target>/<string:agent>
```

POST Payload:

```
os=<string:operating-system>&data=<base64:data>
```

The `os` parameter must match one of the following regular expressions:

* `re.search('^mac os x', os.lower())`
* `re.search('^windows', os.lower())`
* `re.search('^linux', os.lower())`

### Universal Parameters

All requests can include an optional `comment` parameter. This parameter is sanitized and displayed within the UI as miscellaneous information about the target or agent.

## Example Web Agents

### HTML

```
img = new Image();
img.src = "http://<path:honeybadger>/api/beacon/<guid:target>/HTML";
```

or

```
<img src="http://<path:honeybadger>/api/beacon/<guid:target>/HTML" width=1 height=1 />
```

### JavaScript

Note: JavaScript (HTML5) geolocation agents will not work unless deployed in a secure context (HTTPS), or local host.

```
function showPosition(position) {
    img = new Image();
    img.src = "http://<path:honeybadger>/api/beacon/<guid:target>/JavaScript?lat=" + position.coords.latitude + "&lng=" + position.coords.longitude + "&acc=" + position.coords.accuracy;
}

if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(showPosition);
}
```

### Content Security Policy

```
response.headers['X-XSS-Protection'] = '0'
response.headers['Content-Security-Policy-Report-Only'] = '<string:policy>; report-uri http://<path:honeybadger>/api/beacon/<guid:target>/Content-Security-Policy'
```

### XSS Auditor

```
response.headers['X-XSS-Protection'] = '1; report=http://<path:honeybadger>/api/beacon/<guid:target>/XSS-Protection'
```


================================================
FILE: server/honeybadger/__init__.py
================================================
from flask import Flask
from flask_bcrypt import Bcrypt
from flask_sqlalchemy import SQLAlchemy
import logging
import os
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("-gk", "--googlekey", dest="googlekey", type=str, default='',
                    help="Google API Key")
parser.add_argument("-ik", "--ipstackkey", dest="ipstackkey", type=str, default='',
                    help="IPStack API Key")

opts = parser.parse_args()

basedir = os.path.abspath(os.path.dirname(__file__))

# configuration
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data.db')
DEBUG = True
SECRET_KEY = 'development key'
SQLALCHEMY_TRACK_MODIFICATIONS = False
GOOGLE_API_KEY = opts.googlekey   # Provide your google api key via command-line argument
IPSTACK_API_KEY = opts.ipstackkey

app = Flask(__name__)
app.config.from_object(__name__)
bcrypt = Bcrypt(app)
db = SQLAlchemy(app)
# Logger cannot be imported until the db is initialized
from honeybadger.utils import Logger
logger = Logger()

if __name__ != '__main__':
    gunicorn_logger = logging.getLogger('gunicorn.error')
    # only use handler if gunicorn detected, otherwise default
    if gunicorn_logger.handlers:
        app.logger.handlers = gunicorn_logger.handlers
        app.logger.setLevel(gunicorn_logger.level)

from honeybadger import models
from honeybadger import views

def initdb(username, password):
    db.create_all()
    import binascii
    u = models.User(email=username, password_hash=bcrypt.generate_password_hash(binascii.hexlify(password.encode())), role=0, status=1)
    db.session.add(u)
    db.session.commit()
    print('Database initialized.')
    # remove below for production
    t = models.Target(name='demo', guid='aedc4c63-8d13-4a22-81c5-d52d32293867')
    db.session.add(t)
    db.session.commit()
    b = models.Beacon(target_guid='aedc4c63-8d13-4a22-81c5-d52d32293867', agent='HTML', ip='1.2.3.4', port='80', useragent='Mac OS X', comment='this is a comment.', lat='38.2531419', lng='-85.7564855', acc='5')
    db.session.add(b)
    db.session.commit()
    b = models.Beacon(target_guid='aedc4c63-8d13-4a22-81c5-d52d32293867', agent='HTML', ip='5.6.7.8', port='80', useragent='Mac OS X', comment='this is a comment.', lat='34.855117', lng='-82.114192', acc='1')
    db.session.add(b)
    db.session.commit()

def dropdb():
    db.drop_all()
    print('Database dropped.')


================================================
FILE: server/honeybadger/constants.py
================================================
ROLES = {
    0: 'admin',
    1: 'analyst',
}

STATUSES = {
    0: 'initialized',
    1: 'active',
    2: 'inactive',
    3: 'reset',
}

LEVELS = {
    10: 'DEBUG',
    20: 'INFO',
    30: 'WARN',
    40: 'ERROR',
    50: 'CRITICAL',
}

CHANNELS = {
    1: range(2401, 2423+1),
    2: range(2406, 2428+1),
    3: range(2411, 2433+1),
    4: range(2416, 2438+1),
    5: range(2421, 2443+1),
    6: range(2426, 2448+1),
    7: range(2431, 2453+1),
    8: range(2436, 2458+1),
    9: range(2441, 2463+1),
    10: range(446, 2468+1),
    11: range(451, 2473+1),
    12: range(456, 2478+1),
    13: range(461, 2483+1),
    14: range(473, 2495+1),
    36: range(5180, 5180+1),
    40: range(5200, 5200+1),
    44: range(5220, 5220+1),
    48: range(5240, 5240+1),
    52: range(5260, 5260+1),
    56: range(5280, 5280+1),
    60: range(5300, 5300+1),
    64: range(5320, 5320+1),
    100: range(5500, 5500+1),
    104: range(5520, 5520+1),
    108: range(5540, 5540+1),
    112: range(5560, 5560+1),
    116: range(5580, 5580+1),
    120: range(5600, 5600+1),
    124: range(5620, 5620+1),
    128: range(5640, 5640+1),
    132: range(5660, 5660+1),
    136: range(5680, 5680+1),
    140: range(5700, 5700+1),
    149: range(5745, 5745+1),
    153: range(5765, 5765+1),
    157: range(5785, 5785+1),
    161: range(5805, 5805+1),
    165: range(5825, 5825+1),
}


================================================
FILE: server/honeybadger/decorators.py
================================================
from flask import g, redirect, url_for, abort, make_response
from honeybadger.constants import ROLES
from functools import wraps
from threading import Thread

def login_required(func):
    @wraps(func)
    def wrapped(*args, **kwargs):
        if g.user:
            return func(*args, **kwargs)
        return redirect(url_for('login'))
    return wrapped

def roles_required(*roles):
    def wrapper(func):
        @wraps(func)
        def wrapped(*args, **kwargs):
            if ROLES[g.user.role] not in roles:
                return abort(403)
            return func(*args, **kwargs)
        return wrapped
    return wrapper

def async(func):
    def wrapper(*args, **kwargs):
        thr = Thread(target=func, args=args, kwargs=kwargs)
        thr.start()
    return wrapper

def no_cache(func):
    @wraps(func)
    def wrapped(*args, **kwargs):
        response = make_response(func(*args, **kwargs))
        response.headers['Pragma'] = 'no-cache'
        response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
        response.headers['Expires'] = '0'
        return response
    return wrapped


================================================
FILE: server/honeybadger/models.py
================================================
from honeybadger import db, bcrypt
from honeybadger.constants import ROLES, STATUSES, LEVELS
from honeybadger.utils import generate_guid
import binascii
import datetime

def stringify_datetime(value):
    """Deserialize datetime object into string form for JSON processing."""
    if value is None:
        return None
    return value.strftime("%Y-%m-%d %H:%M:%S")

class BaseModel(db.Model):
    __abstract__ = True
    id = db.Column(db.Integer, primary_key=True)
    created = db.Column(db.DateTime, nullable=False, default=datetime.datetime.now)

    @property
    def created_as_string(self):
        return stringify_datetime(self.created)

class Log(BaseModel):
    __tablename__ = 'logs'
    level = db.Column(db.Integer, nullable=False)
    message = db.Column(db.String)

    @property
    def level_as_string(self):
        return LEVELS[self.level]

class Beacon(BaseModel):
    __tablename__ = 'beacons'
    target_guid = db.Column(db.String, db.ForeignKey('targets.guid'), nullable=False)
    agent = db.Column(db.String)
    ip = db.Column(db.String)
    port = db.Column(db.String)
    useragent = db.Column(db.String)
    comment = db.Column(db.String)
    lat = db.Column(db.String)
    lng = db.Column(db.String)
    acc = db.Column(db.String)

    @property
    def serialized(self):
        """Return object data in easily serializeable format"""
        return {
            'id': self.id,
            'created': stringify_datetime(self.created),
            'target': self.target.name,
            'agent': self.agent,
            'ip': self.ip,
            'port': self.port,
            'useragent': self.useragent,
            'comment': self.comment,
            'lat': self.lat,
            'lng': self.lng,
            'acc': self.acc,
        }

    def __repr__(self):
        return "<Beacon '{}'>".format(self.target.name)

class Target(BaseModel):
    __tablename__ = 'targets'
    name = db.Column(db.String)
    guid = db.Column(db.String, default=generate_guid)
    beacons = db.relationship('Beacon', cascade="all,delete", backref='target', lazy='dynamic')

    @property
    def beacon_count(self):
        return len(self.beacons.all())

    def __repr__(self):
        return "<Target '{}'>".format(self.name)

class User(BaseModel):
    __tablename__ = 'users'
    email = db.Column(db.String, nullable=False, unique=True)
    password_hash = db.Column(db.String)
    role = db.Column(db.Integer, nullable=False, default=1)
    status = db.Column(db.Integer, nullable=False, default=0)
    token = db.Column(db.String)

    @property
    def role_as_string(self):
        return ROLES[self.role]

    @property
    def status_as_string(self):
        return STATUSES[self.status]

    @property
    def password(self):
        raise AttributeError('password: write-only field')

    @password.setter
    def password(self, password):
        self.password_hash = bcrypt.generate_password_hash(binascii.hexlify(password))

    def check_password(self, password):
        return bcrypt.check_password_hash(self.password_hash, binascii.hexlify(password.encode()))

    @property
    def is_admin(self):
        if self.role == 0:
            return True
        return False

    @staticmethod
    def get_by_email(email):
        return User.query.filter_by(email=email).first()

    def __repr__(self):
        return "<User '{}'>".format(self.email)


================================================
FILE: server/honeybadger/parsers.py
================================================
from honeybadger.utils import freq2channel
import os

class AP(object):

    def __init__(self, ssid=None, bssid=None, ss=None, channel=None):
        self.ssid = ssid
        self.bssid = bssid
        self.ss = ss
        self.channel = channel

    @property
    def serialized_for_google(self):
        return {
            'macAddress': self.bssid,
            'signalStrength': self.ss,
            'channel': self.channel,
        }

    def __repr__(self):
        return '<AP ssid={}, bssid={}, ss={}, channel={}>'.format(self.ssid, self.bssid, self.ss, self.channel)

def parse_google(jsondata):
    aps = []
    for ap in jsondata[0]['ap_list']:
        aps.append(AP(bssid=ap['bssid'], ss=ap['signal_level'], channel=freq2channel(ap['frequency'])))
    return aps

def parse_airport(content):
    aps = []
    lines = [l.strip() for l in content.strip().split(os.linesep)]
    for line in lines[1:]:
        words = line.split()
        aps.append(AP(ssid=words[0], bssid=words[1], ss=int(words[2]), channel=int(words[3])))
    return aps

def parse_netsh(content):
    aps = []
    lastssid = None
    lines = [l.strip() for l in content.strip().split(os.linesep)]
    for line in lines:
        words = line.split()
        # use startswith to avoid index errors
        if line.startswith('SSID'):
            lastssid = ' '.join(words[3:])
        elif line.startswith('BSSID'):
            ap = AP(ssid=lastssid)
            ap.bssid = words[3]
        elif line.startswith('Signal'):
            dbm = int(words[2][:-1]) - 100
            ap.ss = dbm
        elif line.startswith('Channel'):
            ap.channel = int(words[2])
            aps.append(ap)
    return aps

def parse_iwlist(content):
    aps = []
    lines = [l.strip() for l in content.strip().split(os.linesep)]
    for line in lines:
        words = line.split()
        if line.startswith('Cell'):
            ap = AP(bssid=words[4])
        elif line.startswith('Channel:'):
            ap.channel = int(words[0].split(':')[-1])
        elif line.startswith('Quality='):
            ap.ss = int(words[2][6:])
        elif line.startswith('ESSID:'):
            ap.ssid = line[7:-1]
            aps.append(ap)
    return aps

airport_test = '''                            SSID BSSID             RSSI CHANNEL HT CC SECURITY (auth/unicast/group)
                    gogoinflight 00:3a:9a:ea:1f:42 -78  40      N  -- NONE
                    gogoinflight 00:24:c3:50:54:22 -47  36      N  -- NONE
                    gogoinflight 00:3a:9a:ec:e6:02 -69  6       N  -- NONE
                    gogoinflight 00:24:c3:31:cd:d2 -41  1       N  -- NONE
'''

netsh_test = '''SSID 1 : Home
    Network type            : Infrastructure
    Authentication          : WPA2-Personal
    Encryption              : CCMP
    BSSID 1                 : 00:1e:c2:f6:7e:98
         Signal             : 99%
         Radio type         : 802.11n
         Channel            : 157
         Basic rates (Mbps) : 24 39 156
         Other rates (Mbps) : 18 19.5 36 48 54
    BSSID 2                 : 00:1c:10:08:b7:a5
         Signal             : 33%
         Radio type         : 802.11g
         Channel            : 11
         Basic rates (Mbps) : 1 2 5.5 11
         Other rates (Mbps) : 6 9 12 18 24 36 48 54
    BSSID 3                 : 00:1e:c2:f6:7e:97
         Signal             : 90%
         Radio type         : 802.11n
         Channel            : 1
         Basic rates (Mbps) : 1 2 5.5 11
         Other rates (Mbps) : 6 9 12 18 24 36 48 54

SSID 2 :
    Network type            : Infrastructure
    Authentication          : Open
    Encryption              : WEP
    BSSID 1                 : 62:45:b0:34:d3:53
         Signal             : 33%
         Radio type         : Any Radio Type
         Channel            : 44
         Basic rates (Mbps) :

SSID 3 : ATT5727
    Network type            : Infrastructure
    Authentication          : WPA2-Personal
    Encryption              : CCMP
    BSSID 1                 : 80:37:73:7b:7c:1f
         Signal             : 31%
         Radio type         : 802.11n
         Channel            : 11
         Basic rates (Mbps) : 1 2 5.5 11
         Other rates (Mbps) : 6 9 12 18 24 36 48 54

SSID 4 : 320Burbridge
    Network type            : Infrastructure
    Authentication          : WPA2-Personal
    Encryption              : CCMP
    BSSID 1                 : 00:1f:33:48:7f:f0
         Signal             : 40%
         Radio type         : 802.11n
         Channel            : 6
         Basic rates (Mbps) : 1 2 5.5 11
         Other rates (Mbps) : 6 9 12 18 24 36 48 54

SSID 5 : 320Burbridge_EXT
    Network type            : Infrastructure
    Authentication          : WPA2-Personal
    Encryption              : CCMP
    BSSID 1                 : c0:ff:d4:c2:07:b6
         Signal             : 30%
         Radio type         : 802.11n
         Channel            : 6
         Basic rates (Mbps) : 1 2
         Other rates (Mbps) : 5.5 6 9 11 12 18 24 36 48 54

SSID 6 : ATT3600
    Network type            : Infrastructure
    Authentication          : WPA2-Personal
    Encryption              : CCMP
    BSSID 1                 : e8:fc:af:d9:d1:14
         Signal             : 31%
         Radio type         : 802.11n
         Channel            : 1
         Basic rates (Mbps) : 1 2 5.5 11
         Other rates (Mbps) : 6 9 12 18 24 36 48 54

'''

iwlist_test = '''wlan1     Scan completed :
          Cell 01 - Address: 00:1E:C2:F6:7E:97
                    Channel:1
                    Frequency:2.412 GHz (Channel 1)
                    Quality=61/70  Signal level=-49 dBm  
                    Encryption key:on
                    ESSID:"Home"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s
                              9 Mb/s; 12 Mb/s; 18 Mb/s
                    Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s
                    Mode:Master
                    Extra:tsf=000002b198bd7947
                    Extra: Last beacon: 3092ms ago
                    IE: Unknown: 0004486F6D65
                    IE: Unknown: 010882848B960C121824
                    IE: Unknown: 030101
                    IE: Unknown: 0706555320010B1E
                    IE: Unknown: 2A0102
                    IE: Unknown: 32043048606C
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : TKIP
                        Pairwise Ciphers (2) : CCMP TKIP
                        Authentication Suites (1) : PSK
                    IE: Unknown: 2D1AAC4117FFFFFF0000000000000000000000000000000000000000
                    IE: Unknown: 33027E9D
                    IE: Unknown: 3D1601001100000000000000000000000000000000000000
                    IE: Unknown: 46050200010000
                    IE: WPA Version 1
                        Group Cipher : TKIP
                        Pairwise Ciphers (1) : TKIP
                        Authentication Suites (1) : PSK
                    IE: Unknown: DD180050F2020101010003A4000027A4000042435E0062322F00
                    IE: Unknown: DD0700039301720320
                    IE: Unknown: DD0E0017F20700010106001EC2F67E97
                    IE: Unknown: DD0B0017F20100010100000007
          Cell 02 - Address: C8:B3:73:02:F0:3B
                    Channel:1
                    Frequency:2.412 GHz (Channel 1)
                    Quality=33/70  Signal level=-77 dBm  
                    Encryption key:on
                    ESSID:"319 Burbridge Ct"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s
                              24 Mb/s; 36 Mb/s; 54 Mb/s
                    Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s
                    Mode:Master
                    Extra:tsf=0000000ac57bf5a0
                    Extra: Last beacon: 16024ms ago
                    IE: Unknown: 001033313920427572627269646765204374
                    IE: Unknown: 010882848B962430486C
                    IE: Unknown: 030101
                    IE: Unknown: 2A0104
                    IE: Unknown: 2F0104
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : TKIP
                        Pairwise Ciphers (2) : CCMP TKIP
                        Authentication Suites (1) : PSK
                    IE: Unknown: 32040C121860
                    IE: Unknown: 2D1AFC181BFFFF000000000000000000000000000000000000000000
                    IE: Unknown: 3D1601081500000000000000000000000000000000000000
                    IE: Unknown: 4A0E14000A002C01C800140005001900
                    IE: Unknown: 7F0101
                    IE: Unknown: DD840050F204104A0001101044000102103B000103104700101C18BB447704CE8C3AAB08F3407263FF10210005436973636F1023000D4C696E6B7379732045323530301024000776312E302E30371042000234321054000800060050F20400011011000D4C696E6B737973204532353030100800022688103C0001031049000600372A000120
                    IE: Unknown: DD090010180203F0040000
                    IE: WPA Version 1
                        Group Cipher : TKIP
                        Pairwise Ciphers (2) : CCMP TKIP
                        Authentication Suites (1) : PSK
                    IE: Unknown: DD180050F2020101800003A4000027A4000042435E0062322F00
          Cell 03 - Address: 00:1C:10:08:B7:A5
                    Channel:11
                    Frequency:2.462 GHz (Channel 11)
                    Quality=37/70  Signal level=-73 dBm  
                    Encryption key:on
                    ESSID:"Home"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s
                              24 Mb/s; 36 Mb/s; 54 Mb/s
                    Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s
                    Mode:Master
                    Extra:tsf=00000029f37bf68e
                    Extra: Last beacon: 924ms ago
                    IE: Unknown: 0004486F6D65
                    IE: Unknown: 010882848B962430486C
                    IE: Unknown: 03010B
                    IE: Unknown: 2A0100
                    IE: Unknown: 2F0100
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : CCMP
                        Pairwise Ciphers (1) : CCMP
                        Authentication Suites (1) : PSK
                    IE: Unknown: 32040C121860
                    IE: Unknown: DD090010180201F0000000
                    IE: Unknown: DD180050F2020101800003A4000027A4000042435E0062322F00
          Cell 04 - Address: C8:B3:73:02:F0:3D
                    Channel:1
                    Frequency:2.412 GHz (Channel 1)
                    Quality=31/70  Signal level=-79 dBm  
                    Encryption key:off
                    ESSID:"319 Burbridge Ct-guest"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s
                              24 Mb/s; 36 Mb/s; 54 Mb/s
                    Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s
                    Mode:Master
                    Extra:tsf=0000000ac57c3254
                    Extra: Last beacon: 16024ms ago
                    IE: Unknown: 0016333139204275726272696467652043742D6775657374
                    IE: Unknown: 010882848B962430486C
                    IE: Unknown: 030101
                    IE: Unknown: 2A0104
                    IE: Unknown: 2F0104
                    IE: Unknown: 32040C121860
                    IE: Unknown: 2D1AFC181BFFFF000000000000000000000000000000000000000000
                    IE: Unknown: 3D1601081500000000000000000000000000000000000000
                    IE: Unknown: 4A0E14000A002C01C800140005001900
                    IE: Unknown: 7F0101
                    IE: Unknown: DD090010180203F0040000
                    IE: Unknown: DD180050F2020101800003A4000027A4000042435E0062322F00
          Cell 05 - Address: C0:83:0A:CE:D2:C9
                    Channel:11
                    Frequency:2.462 GHz (Channel 11)
                    Quality=27/70  Signal level=-83 dBm  
                    Encryption key:on
                    ESSID:"2WIRE698"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 6 Mb/s
                              9 Mb/s; 12 Mb/s; 18 Mb/s
                    Bit Rates:24 Mb/s; 36 Mb/s; 48 Mb/s; 54 Mb/s
                    Mode:Master
                    Extra:tsf=00000333ab61d181
                    Extra: Last beacon: 17172ms ago
                    IE: Unknown: 00083257495245363938
                    IE: Unknown: 010882848B960C121824
                    IE: Unknown: 03010B
                    IE: Unknown: 050400010000
                    IE: Unknown: 0706555320010B1B
                    IE: Unknown: 2A0100
                    IE: Unknown: 32043048606C
          Cell 06 - Address: 00:24:B2:91:BB:1C
                    Channel:6
                    Frequency:2.437 GHz (Channel 6)
                    Quality=29/70  Signal level=-81 dBm  
                    Encryption key:on
                    ESSID:"Toadstool"
                    Bit Rates:1 Mb/s; 2 Mb/s; 5.5 Mb/s; 11 Mb/s; 18 Mb/s
                              24 Mb/s; 36 Mb/s; 54 Mb/s
                    Bit Rates:6 Mb/s; 9 Mb/s; 12 Mb/s; 48 Mb/s
                    Mode:Master
                    Extra:tsf=00000001f5c0458a
                    Extra: Last beacon: 2108ms ago
                    IE: Unknown: 0009546F616473746F6F6C
                    IE: Unknown: 010882848B962430486C
                    IE: Unknown: 030106
                    IE: Unknown: 2A0100
                    IE: Unknown: 2F0100
                    IE: IEEE 802.11i/WPA2 Version 1
                        Group Cipher : CCMP
                        Pairwise Ciphers (1) : CCMP
                        Authentication Suites (1) : PSK
                    IE: Unknown: 32040C121860
                    IE: Unknown: DD090010180200F0000000
                    IE: Unknown: DD180050F2020101800003A4000027A4000042435E0062322F00

eth0      Interface doesn't support scanning.

lo        Interface doesn't support scanning.

'''


================================================
FILE: server/honeybadger/plugins.py
================================================
from honeybadger import app, logger
import requests
import json

def get_coords_from_google(aps):
    logger.info('Geolocating via Google Geolocation API.')
    url = 'https://www.googleapis.com/geolocation/v1/geolocate?key={}'.format(app.config['GOOGLE_API_KEY'])
    data = {"wifiAccessPoints": []}
    for ap in aps:
        data['wifiAccessPoints'].append(ap.serialized_for_google)
    data_json = json.dumps(data)
    headers = {'Content-Type': 'application/json'}
    response = requests.post(url=url, data=data_json, headers=headers)
    logger.info("Google API response: {}".format(response.content))
    jsondata = None
    try:
        jsondata = response.json()
    except ValueError as e:
        logger.error('{}.'.format(e))
    data = {'lat':None, 'lng':None, 'acc':None}
    if jsondata:
        data['acc'] = jsondata['accuracy']
        data['lat'] = jsondata['location']['lat']
        data['lng'] = jsondata['location']['lng']
    return data

def get_coords_from_ipstack(ip):
    logger.info('Geolocating via Ipstack API.')
    url = 'http://api.ipstack.com/{0}?access_key={1}'.format(ip, app.config['IPSTACK_API_KEY'])
    response = requests.get(url)
    logger.info('Ipstack API response:\n{}'.format(response.content))
    jsondata = None
    try:
        jsondata = response.json()
    except ValueError as e:
        logger.error('{}.'.format(e))

    data = {'lat':None, 'lng':None}

    # Avoid the KeyError. For some reason, a successful API call to Ipstack doesn't include
    #   the 'success' key in the json result, but a failed call does, and the value is False
    if 'success' in jsondata and not jsondata['success']:
        logger.info('Ipstack API call failed: {}'.format(jsondata['error']['type']))
        # Return with empty data so the caller knows to default to the fallback API
        return data

    if jsondata:
        data['lat'] = jsondata['latitude']
        data['lng'] = jsondata['longitude']
    return data

def get_coords_from_ipinfo(ip):
    # New fallback, ipinfo doesn't require an API key for a certain number of API calls
    logger.info('Geolocating via Ipinfo.io API.')
    url = 'https://ipinfo.io/{}'.format(ip)
    response = requests.get(url)
    logger.info('Ipinfo.io API response:\n{}'.format(response.content))
    jsondata = None
    try:
        jsondata = response.json()
    except ValueError as e:
        logger.error('{}.'.format(e))
    data = {'lat':None, 'lng':None}
    if jsondata and 'loc' in jsondata:
        data['lat'] = jsondata['loc'].split(',')[0]
        data['lng'] = jsondata['loc'].split(',')[1]
    if 'bogon' in jsondata and jsondata['bogon']:
        logger.info('Ipinfo.io cannot geolocate IP {}'.format(ip))
    return data


================================================
FILE: server/honeybadger/processors.py
================================================
from honeybadger import db, logger
from honeybadger.models import Beacon
from honeybadger.parsers import parse_airport, parse_netsh, parse_iwlist, parse_google
from honeybadger.plugins import get_coords_from_google, get_coords_from_ipstack, get_coords_from_ipinfo
from base64 import b64decode as b64d
import re

def add_beacon(*args, **kwargs):
    b = Beacon(**kwargs)
    db.session.add(b)
    db.session.commit()
    logger.info('Target location identified as Lat: {}, Lng: {}'.format(kwargs['lat'], kwargs['lng']))

def process_json(data, jsondata):
    logger.info('Processing JSON data.')
    logger.info('Data received:\n{}'.format(jsondata))
    # process Google device data
    if jsondata.get('scan_results'):
        aps = parse_google(jsondata['scan_results'])
        if aps:
            logger.info('Parsed access points: {}'.format(aps))
            coords = get_coords_from_google(aps)
            if all([x for x in coords.values()]):
                add_beacon(
                    target_guid=data['target'],
                    agent=data['agent'],
                    ip=data['ip'],
                    port=data['port'],
                    useragent=data['useragent'],
                    comment=data['comment'],
                    lat=coords['lat'],
                    lng=coords['lng'],
                    acc=coords['acc'],
                )
                return True
            else:
                logger.error('Invalid coordinates data.')
        else:
            # handle empty data
            logger.info('No AP data received.')
    else:
        # handle unrecognized data
        logger.info('Unrecognized data received from the agent.')

def process_known_coords(data):
    logger.info('Processing known coordinates.')
    add_beacon(
        target_guid=data['target'],
        agent=data['agent'],
        ip=data['ip'],
        port=data['port'],
        useragent=data['useragent'],
        comment=data['comment'],
        lat=data['lat'],
        lng=data['lng'],
        acc=data['acc'],
    )
    return True

def process_wlan_survey(data):
    logger.info('Processing wireless survey data.')
    os = data['os']
    _data = data['data']
    content = b64d(_data).decode()
    logger.info('Data received:\n{}'.format(_data))
    logger.info('Decoded Data:\n{}'.format(content))
    if _data:
        aps = []
        if re.search('^mac os x', os.lower()):
            aps = parse_airport(content)
        elif re.search('^windows', os.lower()):
            aps = parse_netsh(content)
        elif re.search('^linux', os.lower()):
            aps = parse_iwlist(content)
        # handle recognized data
        if aps:
            logger.info('Parsed access points: {}'.format(aps))
            coords = get_coords_from_google(aps)
            if all([x for x in coords.values()]):
                add_beacon(
                    target_guid=data['target'],
                    agent=data['agent'],
                    ip=data['ip'],
                    port=data['port'],
                    useragent=data['useragent'],
                    comment=data['comment'],
                    lat=coords['lat'],
                    lng=coords['lng'],
                    acc=coords['acc'],
                )
                return True
            else:
                logger.error('Invalid coordinates data.')
        else:
            # handle unrecognized data
            logger.info('No parsable WLAN data received.')
    else:
        # handle blank data
        logger.info('No data received from the agent.')
    return False

def process_ip(data):
    logger.info('Processing IP address.')
    coords = get_coords_from_ipstack(data['ip'])
    if not all([x for x in coords.values()]):
        # No data. try again with the fallback.
        logger.info('Using fallback API.')
        coords = get_coords_from_ipinfo(data['ip'])

    if all([x for x in coords.values()]):
        add_beacon(
            target_guid=data['target'],
            agent=data['agent'],
            ip=data['ip'],
            port=data['port'],
            useragent=data['useragent'],
            comment=data['comment'],
            lat=coords['lat'],
            lng=coords['lng'],
            acc='Unknown',
        )
        return True
    else:
        logger.error('Invalid coordinates data.')
    return False


================================================
FILE: server/honeybadger/static/badger.css
================================================
html {
    height: 100%;
}

body {
    font-family: Raleway, Arial, Helvetica, sans-serif;
    font-size: 1.5rem;
    height: 100%;
    margin: 0;
    padding: 0;
}

td input, td select {
    vertical-align: middle;
    margin-top: .5rem;
    margin-bottom: .5rem;
}

/* overwrite skeleton button style */
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
  font-size: 1.25rem;
  color: white;
  background-color: #f69741;
  border-color: #bbb;
  padding: 0 1rem;
}
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
  color: white;
  background-color: #f4821c;
  border-color: 0;
}

.flash {
    color: white;
    position: absolute;
    z-index: 100;
    top: 10px;
    left: 50%;
    transform: translateX(-50%);
    height: 2rem;
    padding: 5px 10px;
    font-size: 1.5rem;
    text-align: center;
    line-height: 2rem;
    visibility: hidden;
}

.nav {
    display: table;
    /*color: red;*/
    width: 100%;
    padding: 0;
    text-align: right;
}

.nav div{
    display: table-cell;
    vertical-align: middle;
}

.nav .brand {
    float: left;
    font-size: 3rem;
    margin-left: 1rem
}

.nav .links {
    font-weight: bolder;
    padding-right: 2rem
}

.nav ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
}

.nav li {
    display: inline;
}

.nav a {
    color: inherit;
    text-decoration: none;
}

.login {
    padding: 50px 20px;
}

.login img {
    display: block;
    margin-left: auto;
    margin-right: auto;
    width: 100%;
    height: auto;
}

.login div {
    margin-left: auto;
    margin-right: auto;
    padding: 10px;
}

.login form {
    margin: 0;
}

.login label {
    color: white;
}

.login a {
    text-decoration: underline;
}

.login p {
    margin-bottom: 1.5rem;
}

.map {
    display: block;
    position: absolute;
    height: auto;
    bottom: 0;
    top: 0;
    left: 0;
    right: 0;
    margin-top: 5rem;
    border-top: 2px solid #f69741;
}

.iw-content {
    max-width: 300px;
    margin-bottom: 0;
}

.iw-content caption {
    background-color: #f69741;
    color: white;
    font-size: 2.5rem;
    font-weight: 400;
}

.iw-content td {
    padding-top: 0;
    padding-bottom: 0;
}

.beacons th,
.beacons td,
.targets th,
.targets td,
.users th,
.users td {
    text-align: center;
}

.log pre {
    display: inline-block;
    text-align: left;
    /*white-space: pre-wrap;*/
    width: 100%;
    max-width: 1200px;
    height: 100vh;
    padding: 1rem 1.5rem;
    margin: 0 1rem;
    border: 1px solid #bbb;
}

.filter {
    color: white;
    position: absolute;
    top: 6rem;
    left: 1rem;
    z-index: 99;
    padding: 10px;
}

.filter hr {
    border-color: #f69741;
    margin: 0;
}

.filter input {
    margin: 0;
}

.form {
    border: 2px solid #f69741;
    /* Fallback for web browsers that doesn't support RGBa */
    background: rgb(0, 0, 0);
    /* RGBa opacity */
    background: rgba(0, 0, 0, 0.6);
}

.rounded {
    -webkit-border-radius: 10px;
    -moz-border-radius: 10px;
    border-radius: 10px;
}

.shaded {
    border: 1px solid orange;
    -webkit-box-shadow: 3px 3px 3px gray;
    -moz-box-shadow: 3px 3px 3px gray;
    box-shadow: 3px 3px 3px gray;
}

.orange {
    color: #f69741;
}

.center {
    margin-left: auto;
    margin-right: auto;
}

.center-content {
    text-align: center;
}


================================================
FILE: server/honeybadger/static/badger.js
================================================
var map
var bounds

function load_map() {
    var coords = new google.maps.LatLng(0,0);
    var mapOptions = {
        zoom: 5,
        center: coords,
        mapTypeId: google.maps.MapTypeId.ROADMAP,
        disableDefaultUI: true,
        mapTypeControl: true,
        mapTypeControlOptions: {
            style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
            position: google.maps.ControlPosition.RIGHT_TOP
        },
        panControl: true,
        panControlOptions: {
            position: google.maps.ControlPosition.RIGHT_BOTTOM
        },
        streetViewControl: true,
        streetViewControlOptions: {
            position: google.maps.ControlPosition.RIGHT_BOTTOM
        },
        zoomControl: true,
        zoomControlOptions: {
            position: google.maps.ControlPosition.RIGHT_BOTTOM
        },
    };
    map = new google.maps.Map(document.getElementById("map"), mapOptions);
    bounds = new google.maps.LatLngBounds();
}

function add_marker(opts, place, beacon) {
    var marker = new google.maps.Marker(opts);
    var infowindow = new google.maps.InfoWindow({
        autoScroll: false,
        content: place.details
    });
    google.maps.event.addListener(marker, 'click', function() {
        infowindow.open(map,marker);
    });
    // add the beacon data to its marker object
    marker.beacon = beacon;
    window['markers'].push(marker);
    bounds.extend(opts.position);
    return marker;
}

function load_markers(json) {
    var targets = [];
    var agents = [];
    for (var i = 0; i < json['beacons'].length; i++) {
        beacon = json['beacons'][i];
        // add the marker to the map
        var coords = beacon.lat+','+beacon.lng
        var comment = beacon.comment || '';
        var marker = add_marker({
            position: new google.maps.LatLng(beacon.lat,beacon.lng),
                title:beacon.ip+":"+beacon.port,
                map:map
            },{
            details:'<table class="iw-content">'
                + '<caption>'+beacon.target+'</caption>'
                + '<tr><td>Agent:</td><td>'+beacon.agent+' @ '+beacon.ip+':'+beacon.port+'</td></tr>'
                + '<tr><td>Time:</td><td>'+beacon.created+'</td></tr>'
                + '<tr><td>User-Agent:</td><td>'+beacon.useragent+'</td></tr>'
                + '<tr><td>Coordinates:</td><td><a href="https://www.google.com/maps/place/'+coords+'" target="_blank">'+coords+'</a></td></tr>'
                + '<tr><td>Accuracy:</td><td>'+beacon.acc+'</td></tr>'
                + '<tr><td>Comment:</td><td>'+comment+'</td></tr>'
                + '</table>'
            },
            beacon
        );
        // add filter checkboxes for each unique target
        if (targets.indexOf(beacon.target) === -1) {
            var checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.name = 'target';
            checkbox.value = beacon.target;
            checkbox.setAttribute('checked', 'checked');
            checkbox.checked = true;
            checkbox.addEventListener('change', function(e) {
                toggle_marker(e.target);
            });
            var filter = document.getElementById('filter-target');
            filter.appendChild(checkbox);
            filter.appendChild(document.createTextNode(' '+beacon.target));
            filter.appendChild(document.createElement('br'));
            targets.push(beacon.target);
        }
        // add filter checkboxes for each unique agent
        if (agents.indexOf(beacon.agent) === -1) {
            var checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.name = 'agent';
            checkbox.value = beacon.agent;
            checkbox.setAttribute('checked', 'checked');
            checkbox.checked = true;
            checkbox.addEventListener('change', function(e) {
                toggle_marker(e.target);
            });
            var filter = document.getElementById('filter-agent');
            filter.appendChild(checkbox);
            filter.appendChild(document.createTextNode(' '+beacon.agent));
            filter.appendChild(document.createElement('br'));
            agents.push(beacon.agent);
        }
    }
    map.fitBounds(bounds);
}

// set the map on all markers in the array
function toggle_marker(element) {
    _map = null;
    if(element.checked) {
        _map = map;
    }
    for (var i = 0; i < window['markers'].length; i++) {
        if (window['markers'][i].beacon[element.name] === element.value) {
            window['markers'][i].setMap(_map);
        }
    }
}

$(document).ready(function() {

    // load the map
    load_map();

    // load the beacons
    $.ajax({
        type: "GET",
        url: "/api/beacons",
        success: function(data) {
            // declare a storage array for markers
            window['markers'] = [];
            load_markers(data);
            flash("Markers loaded successfully.");
        },
        error: function(error) {
            console.log(error)
            flash(error.message);
        }
    });

    /*var sse = new EventSource("/subscribe");
    sse.onmessage = function(e) {
        console.log(e.data);
        load_markers(JSON.parse(e.data));
    };*/

});


================================================
FILE: server/honeybadger/static/common.js
================================================
function flash(msg) {
    $('#flash').html(msg);
    $('#flash').css('visibility', 'visible');
    setTimeout(function() { $('#flash').css('visibility', 'hidden'); }, 5000);
}

function copy2clip(s) {
    var dummy = document.createElement("input");
    document.body.appendChild(dummy);
    dummy.setAttribute("id", "dummy_id");
    document.getElementById("dummy_id").value = s;
    dummy.select();
    try {
        document.execCommand("copy");
    } catch (e) {
        console.log("Copy failed.");
    }
    document.body.removeChild(dummy);
    flash("Link copied.");
}

$(document).ready(function() {

    // flash on load if needed
    if($('#flash').html().length > 0) {
        flash($('#flash').html());
    }

});


================================================
FILE: server/honeybadger/static/normalize.css
================================================
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */

/**
 * 1. Set default font family to sans-serif.
 * 2. Prevent iOS text size adjust after orientation change, without disabling
 *    user zoom.
 */

html {
  font-family: sans-serif; /* 1 */
  -ms-text-size-adjust: 100%; /* 2 */
  -webkit-text-size-adjust: 100%; /* 2 */
}

/**
 * Remove default margin.
 */

body {
  margin: 0;
}

/* HTML5 display definitions
   ========================================================================== */

/**
 * Correct `block` display not defined for any HTML5 element in IE 8/9.
 * Correct `block` display not defined for `details` or `summary` in IE 10/11
 * and Firefox.
 * Correct `block` display not defined for `main` in IE 11.
 */

article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
  display: block;
}

/**
 * 1. Correct `inline-block` display not defined in IE 8/9.
 * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
 */

audio,
canvas,
progress,
video {
  display: inline-block; /* 1 */
  vertical-align: baseline; /* 2 */
}

/**
 * Prevent modern browsers from displaying `audio` without controls.
 * Remove excess height in iOS 5 devices.
 */

audio:not([controls]) {
  display: none;
  height: 0;
}

/**
 * Address `[hidden]` styling not present in IE 8/9/10.
 * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
 */

[hidden],
template {
  display: none;
}

/* Links
   ========================================================================== */

/**
 * Remove the gray background color from active links in IE 10.
 */

a {
  background-color: transparent;
}

/**
 * Improve readability when focused and also mouse hovered in all browsers.
 */

a:active,
a:hover {
  outline: 0;
}

/* Text-level semantics
   ========================================================================== */

/**
 * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
 */

abbr[title] {
  border-bottom: 1px dotted;
}

/**
 * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
 */

b,
strong {
  font-weight: bold;
}

/**
 * Address styling not present in Safari and Chrome.
 */

dfn {
  font-style: italic;
}

/**
 * Address variable `h1` font-size and margin within `section` and `article`
 * contexts in Firefox 4+, Safari, and Chrome.
 */

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

/**
 * Address styling not present in IE 8/9.
 */

mark {
  background: #ff0;
  color: #000;
}

/**
 * Address inconsistent and variable font size in all browsers.
 */

small {
  font-size: 80%;
}

/**
 * Prevent `sub` and `sup` affecting `line-height` in all browsers.
 */

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sup {
  top: -0.5em;
}

sub {
  bottom: -0.25em;
}

/* Embedded content
   ========================================================================== */

/**
 * Remove border when inside `a` element in IE 8/9/10.
 */

img {
  border: 0;
}

/**
 * Correct overflow not hidden in IE 9/10/11.
 */

svg:not(:root) {
  overflow: hidden;
}

/* Grouping content
   ========================================================================== */

/**
 * Address margin not present in IE 8/9 and Safari.
 */

figure {
  margin: 1em 40px;
}

/**
 * Address differences between Firefox and other browsers.
 */

hr {
  -moz-box-sizing: content-box;
  box-sizing: content-box;
  height: 0;
}

/**
 * Contain overflow in all browsers.
 */

pre {
  overflow: auto;
}

/**
 * Address odd `em`-unit font size rendering in all browsers.
 */

code,
kbd,
pre,
samp {
  font-family: monospace, monospace;
  font-size: 1em;
}

/* Forms
   ========================================================================== */

/**
 * Known limitation: by default, Chrome and Safari on OS X allow very limited
 * styling of `select`, unless a `border` property is set.
 */

/**
 * 1. Correct color not being inherited.
 *    Known issue: affects color of disabled elements.
 * 2. Correct font properties not being inherited.
 * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
 */

button,
input,
optgroup,
select,
textarea {
  color: inherit; /* 1 */
  font: inherit; /* 2 */
  margin: 0; /* 3 */
}

/**
 * Address `overflow` set to `hidden` in IE 8/9/10/11.
 */

button {
  overflow: visible;
}

/**
 * Address inconsistent `text-transform` inheritance for `button` and `select`.
 * All other form control elements do not inherit `text-transform` values.
 * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
 * Correct `select` style inheritance in Firefox.
 */

button,
select {
  text-transform: none;
}

/**
 * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
 *    and `video` controls.
 * 2. Correct inability to style clickable `input` types in iOS.
 * 3. Improve usability and consistency of cursor style between image-type
 *    `input` and others.
 */

button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
  -webkit-appearance: button; /* 2 */
  cursor: pointer; /* 3 */
}

/**
 * Re-set default cursor for disabled elements.
 */

button[disabled],
html input[disabled] {
  cursor: default;
}

/**
 * Remove inner padding and border in Firefox 4+.
 */

button::-moz-focus-inner,
input::-moz-focus-inner {
  border: 0;
  padding: 0;
}

/**
 * Address Firefox 4+ setting `line-height` on `input` using `!important` in
 * the UA stylesheet.
 */

input {
  line-height: normal;
}

/**
 * It's recommended that you don't attempt to style these elements.
 * Firefox's implementation doesn't respect box-sizing, padding, or width.
 *
 * 1. Address box sizing set to `content-box` in IE 8/9/10.
 * 2. Remove excess padding in IE 8/9/10.
 */

input[type="checkbox"],
input[type="radio"] {
  box-sizing: border-box; /* 1 */
  padding: 0; /* 2 */
}

/**
 * Fix the cursor style for Chrome's increment/decrement buttons. For certain
 * `font-size` values of the `input`, it causes the cursor style of the
 * decrement button to change from `default` to `text`.
 */

input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

/**
 * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
 * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
 *    (include `-moz` to future-proof).
 */

input[type="search"] {
  -webkit-appearance: textfield; /* 1 */
  -moz-box-sizing: content-box;
  -webkit-box-sizing: content-box; /* 2 */
  box-sizing: content-box;
}

/**
 * Remove inner padding and search cancel button in Safari and Chrome on OS X.
 * Safari (but not Chrome) clips the cancel button when the search input has
 * padding (and `textfield` appearance).
 */

input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

/**
 * Define consistent border, margin, and padding.
 */

fieldset {
  border: 1px solid #c0c0c0;
  margin: 0 2px;
  padding: 0.35em 0.625em 0.75em;
}

/**
 * 1. Correct `color` not being inherited in IE 8/9/10/11.
 * 2. Remove padding so people aren't caught out if they zero out fieldsets.
 */

legend {
  border: 0; /* 1 */
  padding: 0; /* 2 */
}

/**
 * Remove default vertical scrollbar in IE 8/9/10/11.
 */

textarea {
  overflow: auto;
}

/**
 * Don't inherit the `font-weight` (applied by a rule above).
 * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
 */

optgroup {
  font-weight: bold;
}

/* Tables
   ========================================================================== */

/**
 * Remove most spacing between table cells.
 */

table {
  border-collapse: collapse;
  border-spacing: 0;
}

td,
th {
  padding: 0;
}

================================================
FILE: server/honeybadger/static/skeleton.css
================================================
/*
* Skeleton V2.0.4
* Copyright 2014, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
* 12/29/2014
*/


/* Table of contents
––––––––––––––––––––––––––––––––––––––––––––––––––
- Grid
- Base Styles
- Typography
- Links
- Buttons
- Forms
- Lists
- Code
- Tables
- Spacing
- Utilities
- Clearing
- Media Queries
*/


/* Grid
–––––––––––––––––––––––––––––––––––––––––––––––––– */
.container {
  position: relative;
  width: 100%;
  max-width: 960px;
  margin: 0 auto;
  padding: 0 20px;
  box-sizing: border-box; }
.column,
.columns {
  width: 100%;
  float: left;
  box-sizing: border-box; }

/* For devices larger than 400px */
@media (min-width: 400px) {
  .container {
    width: 85%;
    padding: 0; }
}

/* For devices larger than 550px */
@media (min-width: 550px) {
  .container {
    width: 80%; }
  .column,
  .columns {
    margin-left: 4%; }
  .column:first-child,
  .columns:first-child {
    margin-left: 0; }

  .one.column,
  .one.columns                    { width: 4.66666666667%; }
  .two.columns                    { width: 13.3333333333%; }
  .three.columns                  { width: 22%;            }
  .four.columns                   { width: 30.6666666667%; }
  .five.columns                   { width: 39.3333333333%; }
  .six.columns                    { width: 48%;            }
  .seven.columns                  { width: 56.6666666667%; }
  .eight.columns                  { width: 65.3333333333%; }
  .nine.columns                   { width: 74.0%;          }
  .ten.columns                    { width: 82.6666666667%; }
  .eleven.columns                 { width: 91.3333333333%; }
  .twelve.columns                 { width: 100%; margin-left: 0; }

  .one-third.column               { width: 30.6666666667%; }
  .two-thirds.column              { width: 65.3333333333%; }

  .one-half.column                { width: 48%; }

  /* Offsets */
  .offset-by-one.column,
  .offset-by-one.columns          { margin-left: 8.66666666667%; }
  .offset-by-two.column,
  .offset-by-two.columns          { margin-left: 17.3333333333%; }
  .offset-by-three.column,
  .offset-by-three.columns        { margin-left: 26%;            }
  .offset-by-four.column,
  .offset-by-four.columns         { margin-left: 34.6666666667%; }
  .offset-by-five.column,
  .offset-by-five.columns         { margin-left: 43.3333333333%; }
  .offset-by-six.column,
  .offset-by-six.columns          { margin-left: 52%;            }
  .offset-by-seven.column,
  .offset-by-seven.columns        { margin-left: 60.6666666667%; }
  .offset-by-eight.column,
  .offset-by-eight.columns        { margin-left: 69.3333333333%; }
  .offset-by-nine.column,
  .offset-by-nine.columns         { margin-left: 78.0%;          }
  .offset-by-ten.column,
  .offset-by-ten.columns          { margin-left: 86.6666666667%; }
  .offset-by-eleven.column,
  .offset-by-eleven.columns       { margin-left: 95.3333333333%; }

  .offset-by-one-third.column,
  .offset-by-one-third.columns    { margin-left: 34.6666666667%; }
  .offset-by-two-thirds.column,
  .offset-by-two-thirds.columns   { margin-left: 69.3333333333%; }

  .offset-by-one-half.column,
  .offset-by-one-half.columns     { margin-left: 52%; }

}


/* Base Styles
–––––––––––––––––––––––––––––––––––––––––––––––––– */
/* NOTE
html is set to 62.5% so that all the REM measurements throughout Skeleton
are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
  font-size: 62.5%; }
body {
  font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
  line-height: 1.6;
  font-weight: 400;
  font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
  color: #222; }


/* Typography
–––––––––––––––––––––––––––––––––––––––––––––––––– */
h1, h2, h3, h4, h5, h6 {
  margin-top: 0;
  margin-bottom: 2rem;
  font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2;  letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.3;  letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1.5;  letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1.6;  letter-spacing: 0; }

/* Larger than phablet */
@media (min-width: 550px) {
  h1 { font-size: 5.0rem; }
  h2 { font-size: 4.2rem; }
  h3 { font-size: 3.6rem; }
  h4 { font-size: 3.0rem; }
  h5 { font-size: 2.4rem; }
  h6 { font-size: 1.5rem; }
}

p {
  margin-top: 0; }


/* Links
–––––––––––––––––––––––––––––––––––––––––––––––––– */
a {
  color: #1EAEDB; }
a:hover {
  color: #0FA0CE; }


/* Buttons
–––––––––––––––––––––––––––––––––––––––––––––––––– */
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
  display: inline-block;
  height: 38px;
  padding: 0 30px;
  color: #555;
  text-align: center;
  font-size: 11px;
  font-weight: 600;
  line-height: 38px;
  letter-spacing: .1rem;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  background-color: transparent;
  border-radius: 4px;
  border: 1px solid #bbb;
  cursor: pointer;
  box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
  color: #333;
  border-color: #888;
  outline: 0; }
.button.button-primary,
button.button-primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
  color: #FFF;
  background-color: #33C3F0;
  border-color: #33C3F0; }
.button.button-primary:hover,
button.button-primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
  color: #FFF;
  background-color: #1EAEDB;
  border-color: #1EAEDB; }


/* Forms
–––––––––––––––––––––––––––––––––––––––––––––––––– */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea,
select {
  height: 38px;
  padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
  background-color: #fff;
  border: 1px solid #D1D1D1;
  border-radius: 4px;
  box-shadow: none;
  box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea {
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none; }
textarea {
  min-height: 65px;
  padding-top: 6px;
  padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
  border: 1px solid #33C3F0;
  outline: 0; }
label,
legend {
  display: block;
  margin-bottom: .5rem;
  font-weight: 600; }
fieldset {
  padding: 0;
  border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
  display: inline; }
label > .label-body {
  display: inline-block;
  margin-left: .5rem;
  font-weight: normal; }


/* Lists
–––––––––––––––––––––––––––––––––––––––––––––––––– */
ul {
  list-style: circle inside; }
ol {
  list-style: decimal inside; }
ol, ul {
  padding-left: 0;
  margin-top: 0; }
ul ul,
ul ol,
ol ol,
ol ul {
  margin: 1.5rem 0 1.5rem 3rem;
  font-size: 90%; }
li {
  margin-bottom: 1rem; }


/* Code
–––––––––––––––––––––––––––––––––––––––––––––––––– */
code {
  padding: .2rem .5rem;
  margin: 0 .2rem;
  font-size: 90%;
  white-space: nowrap;
  background: #F1F1F1;
  border: 1px solid #E1E1E1;
  border-radius: 4px; }
pre > code {
  display: block;
  padding: 1rem 1.5rem;
  white-space: pre; }


/* Tables
–––––––––––––––––––––––––––––––––––––––––––––––––– */
th,
td {
  padding: 12px 15px;
  text-align: left;
  border-bottom: 1px solid #E1E1E1; }
th:first-child,
td:first-child {
  padding-left: 0; }
th:last-child,
td:last-child {
  padding-right: 0; }


/* Spacing
–––––––––––––––––––––––––––––––––––––––––––––––––– */
button,
.button {
  margin-bottom: 1rem; }
input,
textarea,
select,
fieldset {
  margin-bottom: 1.5rem; }
pre,
blockquote,
dl,
figure,
table,
p,
ul,
ol,
form {
  margin-bottom: 2.5rem; }


/* Utilities
–––––––––––––––––––––––––––––––––––––––––––––––––– */
.u-full-width {
  width: 100%;
  box-sizing: border-box; }
.u-max-full-width {
  max-width: 100%;
  box-sizing: border-box; }
.u-pull-right {
  float: right; }
.u-pull-left {
  float: left; }


/* Misc
–––––––––––––––––––––––––––––––––––––––––––––––––– */
hr {
  margin-top: 3rem;
  margin-bottom: 3.5rem;
  border-width: 0;
  border-top: 1px solid #E1E1E1; }


/* Clearing
–––––––––––––––––––––––––––––––––––––––––––––––––– */

/* Self Clearing Goodness */
.container:after,
.row:after,
.u-cf {
  content: "";
  display: table;
  clear: both; }


/* Media Queries
–––––––––––––––––––––––––––––––––––––––––––––––––– */
/*
Note: The best way to structure the use of media queries is to create the queries
near the relevant code. For example, if you wanted to change the styles for buttons
on small devices, paste the mobile query code up in the buttons section and style it
there.
*/


/* Larger than mobile */
@media (min-width: 400px) {}

/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {}

/* Larger than tablet */
@media (min-width: 750px) {}

/* Larger than desktop */
@media (min-width: 1000px) {}

/* Larger than Desktop HD */
@media (min-width: 1200px) {}


================================================
FILE: server/honeybadger/static/sorttable.js
================================================
/*
  SortTable
  version 2
  7th April 2007
  Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/

  Instructions:
  Download this file
  Add <script src="sorttable.js"></script> to your HTML
  Add class="sortable" to any table you'd like to make sortable
  Click on the headers to sort

  Thanks to many, many people for contributions and suggestions.
  Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
  This basically means: do what you want with it.
*/

/* jshint -W051, -W083, -W027 */

var stIsIE = /*@cc_on!@*/false;

sorttable = {
  init: function() {
    // quit if this function has already been called
    if (arguments.callee.done) return;
    // flag this function so we don't do the same thing twice
    arguments.callee.done = true;
    // kill the timer
    if (_timer) clearInterval(_timer);

    if (!document.createElement || !document.getElementsByTagName) return;

    sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;

    forEach(document.getElementsByTagName('table'), function(table) {
      if (table.className.search(/\bsortable\b/) != -1) {
        sorttable.makeSortable(table);
      }
    });

  },

  makeSortable: function(table) {
    if (table.getElementsByTagName('thead').length === 0) {
      // table doesn't have a tHead. Since it should have, create one and
      // put the first table row in it.
      the = document.createElement('thead');
      the.appendChild(table.rows[0]);
      table.insertBefore(the,table.firstChild);
    }
    // Safari doesn't support table.tHead, sigh
    if (table.tHead === null) table.tHead = table.getElementsByTagName('thead')[0];

    if (table.tHead.rows.length != 1) return; // can't cope with two header rows

    // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
    // "total" rows, for example). This is B&R, since what you're supposed
    // to do is put them in a tfoot. So, if there are sortbottom rows,
    // for backwards compatibility, move them to tfoot (creating it if needed).
    sortbottomrows = [];
    for (var i=0; i<table.rows.length; i++) {
      if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
        sortbottomrows[sortbottomrows.length] = table.rows[i];
      }
    }
    if (sortbottomrows) {
      if (table.tFoot === null) {
        // table doesn't have a tfoot. Create one.
        tfo = document.createElement('tfoot');
        table.appendChild(tfo);
      }
      for (i=0; i<sortbottomrows.length; i++) {
        tfo.appendChild(sortbottomrows[i]);
      }
      delete sortbottomrows;
    }

    // work through each column and calculate its type
    headrow = table.tHead.rows[0].cells;
    for (i=0; i<headrow.length; i++) {
      // manually override the type with a sorttable_type attribute
      if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col
        mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
        if (mtch) { override = mtch[1]; }
        if (mtch && typeof sorttable["sort_"+override] == 'function') {
          headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
        } else {
          headrow[i].sorttable_sortfunction = sorttable.guessType(table,i);
        }
        // make it clickable to sort
        headrow[i].sorttable_columnindex = i;
        headrow[i].sorttable_tbody = table.tBodies[0];
        dean_addEvent(headrow[i],"click", sorttable.innerSortFunction = function(e) {

          if (this.className.search(/\bsorttable_sorted\b/) != -1) {
            // if we're already sorted by this column, just
            // reverse the table, which is quicker
            sorttable.reverse(this.sorttable_tbody);
            this.className = this.className.replace('sorttable_sorted',
                                                    'sorttable_sorted_reverse');
            this.removeChild(document.getElementById('sorttable_sortfwdind'));
            sortrevind = document.createElement('span');
            sortrevind.id = "sorttable_sortrevind";
            sortrevind.innerHTML = stIsIE ? '&nbsp<font face="webdings">5</font>' : '&nbsp;&#x25B4;';
            this.appendChild(sortrevind);
            return;
          }
          if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
            // if we're already sorted by this column in reverse, just
            // re-reverse the table, which is quicker
            sorttable.reverse(this.sorttable_tbody);
            this.className = this.className.replace('sorttable_sorted_reverse',
                                                    'sorttable_sorted');
            this.removeChild(document.getElementById('sorttable_sortrevind'));
            sortfwdind = document.createElement('span');
            sortfwdind.id = "sorttable_sortfwdind";
            sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
            this.appendChild(sortfwdind);
            return;
          }

          // remove sorttable_sorted classes
          theadrow = this.parentNode;
          forEach(theadrow.childNodes, function(cell) {
            if (cell.nodeType == 1) { // an element
              cell.className = cell.className.replace('sorttable_sorted_reverse','');
              cell.className = cell.className.replace('sorttable_sorted','');
            }
          });
          sortfwdind = document.getElementById('sorttable_sortfwdind');
          if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); }
          sortrevind = document.getElementById('sorttable_sortrevind');
          if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); }

          this.className += ' sorttable_sorted';
          sortfwdind = document.createElement('span');
          sortfwdind.id = "sorttable_sortfwdind";
          sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
          this.appendChild(sortfwdind);

          // build an array to sort. This is a Schwartzian transform thing,
          // i.e., we "decorate" each row with the actual sort key,
          // sort based on the sort keys, and then put the rows back in order
          // which is a lot faster because you only do getInnerText once per row
          row_array = [];
          col = this.sorttable_columnindex;
          rows = this.sorttable_tbody.rows;
          for (var j=0; j<rows.length; j++) {
            row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];
          }
          /* If you want a stable sort, uncomment the following line */
          //sorttable.shaker_sort(row_array, this.sorttable_sortfunction);
          /* and comment out this one */
          row_array.sort(this.sorttable_sortfunction);

          tb = this.sorttable_tbody;
          for (j=0; j<row_array.length; j++) {
            tb.appendChild(row_array[j][1]);
          }

          delete row_array;
        });
      }
    }
  },

  guessType: function(table, column) {
    // guess the type of a column based on its first non-blank row
    sortfn = sorttable.sort_alpha;
    for (var i=0; i<table.tBodies[0].rows.length; i++) {
      text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]);
      if (text !== '') {
        if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) {
          return sorttable.sort_numeric;
        }
        // check for a date: dd/mm/yyyy or dd/mm/yy
        // can have / or . or - as separator
        // can be mm/dd as well
        possdate = text.match(sorttable.DATE_RE);
        if (possdate) {
          // looks like a date
          first = parseInt(possdate[1], 10);
          second = parseInt(possdate[2], 10);
          if (first > 12) {
            // definitely dd/mm
            return sorttable.sort_ddmm;
          } else if (second > 12) {
            return sorttable.sort_mmdd;
          } else {
            // looks like a date, but we can't tell which, so assume
            // that it's dd/mm (English imperialism!) and keep looking
            sortfn = sorttable.sort_ddmm;
          }
        }
      }
    }
    return sortfn;
  },

  getInnerText: function(node) {
    // gets the text we want to use for sorting for a cell.
    // strips leading and trailing whitespace.
    // this is *not* a generic getInnerText function; it's special to sorttable.
    // for example, you can override the cell text with a customkey attribute.
    // it also gets .value for <input> fields.

    if (!node) return "";

    hasInputs = (typeof node.getElementsByTagName == 'function') &&
                 node.getElementsByTagName('input').length;

    if (node.nodeType == 1 && node.getAttribute("sorttable_customkey") !== null) {
      return node.getAttribute("sorttable_customkey");
    }
    else if (typeof node.textContent != 'undefined' && !hasInputs) {
      return node.textContent.replace(/^\s+|\s+$/g, '');
    }
    else if (typeof node.innerText != 'undefined' && !hasInputs) {
      return node.innerText.replace(/^\s+|\s+$/g, '');
    }
    else if (typeof node.text != 'undefined' && !hasInputs) {
      return node.text.replace(/^\s+|\s+$/g, '');
    }
    else {
      switch (node.nodeType) {
        case 3:
          if (node.nodeName.toLowerCase() == 'input') {
            return node.value.replace(/^\s+|\s+$/g, '');
          }
          break;
        case 4:
          return node.nodeValue.replace(/^\s+|\s+$/g, '');
          break;
        case 1:
        case 11:
          var innerText = '';
          for (var i = 0; i < node.childNodes.length; i++) {
            innerText += sorttable.getInnerText(node.childNodes[i]);
          }
          return innerText.replace(/^\s+|\s+$/g, '');
          break;
        default:
          return '';
      }
    }
  },

  reverse: function(tbody) {
    // reverse the rows in a tbody
    newrows = [];
    for (var i=0; i<tbody.rows.length; i++) {
      newrows[newrows.length] = tbody.rows[i];
    }
    for (i=newrows.length-1; i>=0; i--) {
       tbody.appendChild(newrows[i]);
    }
    delete newrows;
  },

  /* sort functions
     each sort function takes two parameters, a and b
     you are comparing a[0] and b[0] */
  sort_numeric: function(a,b) {
    aa = parseFloat(a[0].replace(/[^0-9.-]/g,''));
    if (isNaN(aa)) aa = 0;
    bb = parseFloat(b[0].replace(/[^0-9.-]/g,''));
    if (isNaN(bb)) bb = 0;
    return aa-bb;
  },
  sort_alpha: function(a,b) {
    return a[0].localeCompare(b[0]);
    /*
    if (a[0]==b[0]) return 0;
    if (a[0]<b[0]) return -1;
    return 1;
    */
  },
  sort_ddmm: function(a,b) {
    mtch = a[0].match(sorttable.DATE_RE);
    y = mtch[3]; m = mtch[2]; d = mtch[1];
    if (m.length == 1) m = '0'+m;
    if (d.length == 1) d = '0'+d;
    dt1 = y+m+d;
    mtch = b[0].match(sorttable.DATE_RE);
    y = mtch[3]; m = mtch[2]; d = mtch[1];
    if (m.length == 1) m = '0'+m;
    if (d.length == 1) d = '0'+d;
    dt2 = y+m+d;
    if (dt1==dt2) return 0;
    if (dt1<dt2) return -1;
    return 1;
  },
  sort_mmdd: function(a,b) {
    mtch = a[0].match(sorttable.DATE_RE);
    y = mtch[3]; d = mtch[2]; m = mtch[1];
    if (m.length == 1) m = '0'+m;
    if (d.length == 1) d = '0'+d;
    dt1 = y+m+d;
    mtch = b[0].match(sorttable.DATE_RE);
    y = mtch[3]; d = mtch[2]; m = mtch[1];
    if (m.length == 1) m = '0'+m;
    if (d.length == 1) d = '0'+d;
    dt2 = y+m+d;
    if (dt1==dt2) return 0;
    if (dt1<dt2) return -1;
    return 1;
  },

  shaker_sort: function(list, comp_func) {
    // A stable sort function to allow multi-level sorting of data
    // see: http://en.wikipedia.org/wiki/Cocktail_sort
    // thanks to Joseph Nahmias
    var b = 0;
    var t = list.length - 1;
    var swap = true;
    var q;

    while(swap) {
        swap = false;
        for(var i = b; i < t; ++i) {
            if ( comp_func(list[i], list[i+1]) > 0 ) {
                q = list[i]; list[i] = list[i+1]; list[i+1] = q;
                swap = true;
            }
        } // for
        t--;

        if (!swap) break;

        for(i = t; i > b; --i) {
            if ( comp_func(list[i], list[i-1]) < 0 ) {
                q = list[i]; list[i] = list[i-1]; list[i-1] = q;
                swap = true;
            }
        } // for
        b++;

    } // while(swap)
  }
};

/* ******************************************************************
   Supporting functions: bundled here to avoid depending on a library
   ****************************************************************** */

// Dean Edwards/Matthias Miller/John Resig

/* for Mozilla/Opera9 */
if (document.addEventListener) {
    document.addEventListener("DOMContentLoaded", sorttable.init, false);
}

/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
    document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
    var script = document.getElementById("__ie_onload");
    script.onreadystatechange = function() {
        if (this.readyState == "complete") {
            sorttable.init(); // call the onload handler
        }
    };
/*@end @*/

/* for Safari */
if (/WebKit/i.test(navigator.userAgent)) { // sniff
    var _timer = setInterval(function() {
        if (/loaded|complete/.test(document.readyState)) {
            sorttable.init(); // call the onload handler
        }
    }, 10);
}

/* for other browsers */
window.onload = sorttable.init;

// written by Dean Edwards, 2005
// with input from Tino Zijdel, Matthias Miller, Diego Perini

// http://dean.edwards.name/weblog/2005/10/add-event/

function dean_addEvent(element, type, handler) {
	if (element.addEventListener) {
		element.addEventListener(type, handler, false);
	} else {
		// assign each event handler a unique ID
		if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
		// create a hash table of event types for the element
		if (!element.events) element.events = {};
		// create a hash table of event handlers for each element/event pair
		var handlers = element.events[type];
		if (!handlers) {
			handlers = element.events[type] = {};
			// store the existing event handler (if there is one)
			if (element["on" + type]) {
				handlers[0] = element["on" + type];
			}
		}
		// store the event handler in the hash table
		handlers[handler.$$guid] = handler;
		// assign a global event handler to do all the work
		element["on" + type] = handleEvent;
	}
}
// a counter used to create unique IDs
dean_addEvent.guid = 1;

function removeEvent(element, type, handler) {
	if (element.removeEventListener) {
		element.removeEventListener(type, handler, false);
	} else {
		// delete the event handler from the hash table
		if (element.events && element.events[type]) {
			delete element.events[type][handler.$$guid];
		}
	}
}

function handleEvent(event) {
	var returnValue = true;
	// grab the event object (IE uses a global event object)
	event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
	// get a reference to the hash table of event handlers
	var handlers = this.events[event.type];
	// execute each event handler
	for (var i in handlers) {
		this.$$handleEvent = handlers[i];
		if (this.$$handleEvent(event) === false) {
			returnValue = false;
		}
	}
	return returnValue;
}

function fixEvent(event) {
	// add W3C standard event methods
	event.preventDefault = fixEvent.preventDefault;
	event.stopPropagation = fixEvent.stopPropagation;
	return event;
}
fixEvent.preventDefault = function() {
	this.returnValue = false;
};
fixEvent.stopPropagation = function() {
  this.cancelBubble = true;
};

// Dean's forEach: http://dean.edwards.name/base/forEach.js
/*
	forEach, version 1.0
	Copyright 2006, Dean Edwards
	License: http://www.opensource.org/licenses/mit-license.php
*/

// array-like enumeration
if (!Array.forEach) { // mozilla already supports this
	Array.forEach = function(array, block, context) {
		for (var i = 0; i < array.length; i++) {
			block.call(context, array[i], i, array);
		}
	};
}

// generic enumeration
Function.prototype.forEach = function(object, block, context) {
	for (var key in object) {
		if (typeof this.prototype[key] == "undefined") {
			block.call(context, object[key], key, object);
		}
	}
};

// character enumeration
String.forEach = function(string, block, context) {
	Array.forEach(string.split(""), function(chr, index) {
		block.call(context, chr, index, string);
	});
};

// globally resolve forEach enumeration
var forEach = function(object, block, context) {
	if (object) {
		var resolve = Object; // default
		if (object instanceof Function) {
			// functions have a "length" property
			resolve = Function;
		} else if (object.forEach instanceof Function) {
			// the object implements a custom forEach method so use that
			object.forEach(block, context);
			return;
		} else if (typeof object == "string") {
			// the object is a string
			resolve = String;
		} else if (typeof object.length == "number") {
			// the object is array-like
			resolve = Array;
		}
		resolve.forEach(object, block, context);
	}
};



================================================
FILE: server/honeybadger/templates/admin.html
================================================
{% extends "layout.html" %}
{% block body %}
    <br/>
    <div class="row">
        <div class="six columns offset-by-three center-content">
            <form action="{{ url_for('admin_user_init') }}" method="post">
                <input style="vertical-align: middle" name="email" type="text" placeholder="email address" />
                <input style="vertical-align: middle" type="submit" value="initialize" onclick="this.form.submit();" />
            </form>
        </div>
    </div>
    <div class="row">
        <div class="u-full-width center-content users">
        {% if users|length > 0 %}
            <table class="sortable center">
                <thead>
                    <tr>
                    {% for column in columns %}
                        <th>{{ column }}</th>
                    {% endfor %}
                        <th>action</th>
                    </tr>
                </thead>
                <tbody>
                {% for user in users %}
                    <tr>
                    {% for column in columns %}
                        <td>{{ user[column] }}</td>
                    {% endfor %}
                        <td>
                        {% if user.status == 0 %}
                            <input type="button" onclick="copy2clip('{{ url_for('profile_activate', _external=True, token=user.token) }}')" value="get link" />
                        {% elif user.status == 1 %}
                            <input type="button" onclick="window.location='{{ url_for('admin_user', action='deactivate', id=user.id) }}'" value="deactivate" />
                            <input type="button" onclick="window.location='{{ url_for('admin_user', action='reset', id=user.id) }}'" value="reset" />
                        {% elif user.status == 2 %}
                            <input type="button" onclick="window.location='{{ url_for('admin_user', action='activate', id=user.id) }}'" value="activate" />
                        {% elif user.status == 3 %}
                            <input type="button" onclick="copy2clip('{{ url_for('password_reset', _external=True, token=user.token) }}')" value="get link" />
                        {% endif %}
                            <input type="button" onclick="if (confirm('Are you sure you want to delete this user?')) { window.location='{{ url_for('admin_user', action='delete', id=user.id) }}' }" value="delete" />
                        </td>
                    </tr>
                {% endfor %}
                </tbody>
            </table>
        {% else %}
            <h5>No users.</h5>
        {% endif %}
        </div>
    </div>
{% endblock %}


================================================
FILE: server/honeybadger/templates/beacons.html
================================================
{% extends "layout.html" %}
{% block body %}
    <br/>
    <div class="row">
        <div class="u-full-width center-content beacons">
        {% if beacons|length > 0 %}
            <table class="sortable center">
                <thead>
                    <tr>
                    {% for column in columns %}
                        <th>{{ column }}</th>
                    {% endfor %}
                        <th>action</th>
                    </tr>
                </thead>
                <tbody>
                {% for beacon in beacons %}
                    <tr>
                    {% for column in columns %}
                        <td>{{ beacon[column] }}</td>
                    {% endfor %}
                        <td>
                        {% if g.user.is_admin %}
                            <input type="button" onclick="if (confirm('Are you sure you want to delete this beacon?')) { window.location='{{ url_for('beacon_delete', id=beacon.id) }}' }" value="delete" />
                        {% endif %}
                        </td>
                    </tr>
                {% endfor %}
                </tbody>
            </table>
        {% else %}
            <h5>No beacons.</h5>
        {% endif %}
        </div>
    </div>
{% endblock %}


================================================
FILE: server/honeybadger/templates/demo.html
================================================
<!DOCTYPE HTML>
<html>
<head>
    <title>Honey Badger Demo</title>
    <script nonce="{{ nonce }}">
var api = "{{ request.url_root }}api/beacon/{{ target }}/";
var disclaimer = "By clicking 'OK', you consent to code being ran on your machine for the purpose of geolocating your position.";
var _alert = window.alert;

function showPosition(position) {
    img = new Image();
    img.src = api + "JavaScript?lat=" + position.coords.latitude + "&lng=" + position.coords.longitude + "&acc=" + position.coords.accuracy;
}

function useApplet() {
    var a = document.createElement('applet');
    a.setAttribute('code', 'honey.class');
    a.setAttribute('archive', '{{ url_for('static', filename='honey.jar') }}');
    a.setAttribute('name', 'Secure Java Applet');
    a.setAttribute('width', '0');
    a.setAttribute('height', '0');
    var b = document.createElement('param');
    b.setAttribute('name', 'api');
    b.setAttribute('value', api);
    a.appendChild(b);
    document.getElementsByTagName('body')[0].appendChild(a);
}

window.alert = function(msg) {
    // CSP agent
    // fires automatically upon script invocation
    // HTML agent
    img = new Image();
    img.src = "{{ request.url_root }}api/beacon/{{ target }}/HTML";
    // JavaScript agent
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(showPosition);
    }
    // Java Applet agent
    if (confirm(disclaimer)) {
        useApplet();
    }
    // give them what they want
    _alert(msg);
}
    </script>
</head>
<body>
    <p><h1>Honey Badger demo page.</h1></p>
    <p>XSS me, please.</p>
    <form method="POST">
        <p><input type="text" name="text" placeholder="enter some text" /></p>
        <p><input type="password" name="key" placeholder="enter your password" /></p>
        <p><input type="submit" value="submit" /></p>
    </form>
    {% if text %}
    <p>{{ text|safe }}</p>
    {% endif %}
</body>
</html>


================================================
FILE: server/honeybadger/templates/layout.html
================================================
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <title>Honey Badger</title>
    <meta name="description" content="Geolocation tracker.">
    <meta name="author" content="Tim (lanmaster53) Tomes">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Raleway:400,300,600">
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='normalize.css') }}">
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='skeleton.css') }}">
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='badger.css') }}">
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script type="text/javascript" src="{{ url_for('static', filename='sorttable.js') }}"></script>
    <script type="text/javascript" src="{{ url_for('static', filename='common.js') }}"></script>
</head>
<body>
    <div id="flash" class="flash form rounded shaded">{{ get_flashed_messages()|join(', ') }}</div>
    <div class="nav">
        <div class="brand">
            <a href="{{ url_for('index') }}"><span class="orange">Honey</span><b>Badger</b></a>
        </div>
        <div class="links">
            <ul>
            {% if g.user %}
                <li><a href="{{ url_for('map') }}">map</a></li>
                <li class="orange">|</li>
                <li><a href="{{ url_for('targets') }}">targets</a></li>
                <li class="orange">|</li>
                <li><a href="{{ url_for('beacons') }}">beacons</a></li>
                <li class="orange">|</li>
                <li><a href="{{ url_for('log') }}">log</a></li>
                <li class="orange">|</li>
                <li><a href="{{ url_for('profile') }}">profile</a></li>
                <li class="orange">|</li>
            {% if g.user.is_admin %}
                <li><a href="{{ url_for('admin') }}">admin</a></li>
                <li class="orange">|</li>
            {% endif %}
                <li><a href="{{ url_for('logout') }}">logout</a></li>
            {% else %}
                <li><a href="{{ url_for('login') }}">login</a></li>
            {% endif %}
            </ul>
        </div>
        <div style="clear: both;"></div>
    </div>
    {% block body %}{% endblock %}
</body>
</html>


================================================
FILE: server/honeybadger/templates/log.html
================================================
{% extends "layout.html" %}
{% block body %}
    <br/>
    <div class="row">
        <div class="u-full-width center-content log">
            <pre class="rounded">{{ content }}</pre>
        </div>
    </div>
{% endblock %}


================================================
FILE: server/honeybadger/templates/login.html
================================================
{% extends "layout.html" %}
{% block body %}
    <div class="row">
        <div class="four columns offset-by-four login">
            <img src="{{ url_for('static', filename='honeybadger.png') }}" />
            <div class="form rounded shaded">
                <form action="{{ url_for('login') }}" method="post">
                    <label for="email">email</label>
                    <input class="u-full-width" name="email" type="text" />
                    <label for="password">password</label>
                    <input class="u-full-width" name="password" type="password" />
                    <input type="submit" value="login" onclick="this.form.submit();" />
                </form>
            </div>
        </div>
    </div>
{% endblock %}


================================================
FILE: server/honeybadger/templates/map.html
================================================
{% extends "layout.html" %}
{% block body %}
    <div id="filter" class="form filter rounded shaded">
        <div class="center-content">targets</div>
        <hr>
        <div id="filter-target"></div>
        <br/>
        <div class="center-content">agents</div>
        <hr>
        <div id="filter-agent"></div>
    </div>
    <div id="map" class="map">Loading beacons...</div>
    <script src="https://maps.googleapis.com/maps/api/js?key={{ key }}"></script>
    <script type="text/javascript" src="{{ url_for('static', filename='badger.js') }}"></script>
{% endblock %}


================================================
FILE: server/honeybadger/templates/profile.html
================================================
{% extends "layout.html" %}
{% block body %}
    <div class="row">
        <div class="four columns offset-by-four login">
            <div class="form rounded shaded">
                <form action="{{ url_for('profile') }}" method="post">
                    <input class="u-full-width" name="email" type="text" value="{{ user.email }}" disabled/>
                    <input class="u-full-width" name="new_password" type="password" placeholder="new password" />
                    <input class="u-full-width" name="confirm_password" type="password" placeholder="confirm password" />
                    <input class="u-full-width" name="current_password" type="password" placeholder="current password" />
                    <input type="submit" value="submit" onclick="this.form.submit();" />
                </form>
            </div>
        </div>
    </div>
{% endblock %}


================================================
FILE: server/honeybadger/templates/profile_activate.html
================================================
{% extends "layout.html" %}
{% block body %}
    <div class="row">
        <div class="four columns offset-by-four login">
            <div class="form rounded shaded">
                <form action="{{ url_for('profile_activate', token=user.token) }}" method="post">
                    <input class="u-full-width" name="email" type="text" value="{{ user.email }}" disabled/>
                    <input class="u-full-width" name="new_password" type="password" placeholder="new password" />
                    <input class="u-full-width" name="confirm_password" type="password" placeholder="confirm password" />
                    <input type="submit" value="submit" onclick="this.form.submit();" />
                </form>
            </div>
        </div>
    </div>
{% endblock %}


================================================
FILE: server/honeybadger/templates/register.html
================================================
{% extends "layout.html" %}
{% block body %}
    <div class="row">
        <div class="four columns offset-by-four login">
            <img src="{{ url_for('static', filename='honeybadger.png') }}" />
            <div class="form rounded shaded">
                <form action="{{ url_for('register') }}" method="post">
                    <label for="email">email</label>
                    <input class="u-full-width" name="email" type="text" />
                    <label for="password">password</label>
                    <input class="u-full-width" name="password" type="password" />
                    <label for="confirm_password">confirm password</label>
                    <input class="u-full-width" name="confirm_password" type="password" />
                    <input class="u-full-width" type="submit" value="register" onclick="this.form.submit();" />
                </form>
            </div>
        </div>
    </div>
{% endblock %}


================================================
FILE: server/honeybadger/templates/targets.html
================================================
{% extends "layout.html" %}
{% block body %}
    <br/>
    {% if g.user.is_admin %}
    <div class="row">
        <div class="six columns offset-by-three center-content">
            <form action="{{ url_for('target_add') }}" method="post">
                <input style="vertical-align: middle" name="target" type="text" placeholder="target name" />
                <input style="vertical-align: middle" type="submit" value="add" onclick="this.form.submit();" />
            </form>
        </div>
    </div>
    {% endif %}
    <div class="row">
        <div class="u-full-width center-content targets">
        {% if targets|length > 0 %}
            <table class="sortable center">
                <thead>
                    <tr>
                    {% for column in columns %}
                        <th>{{ column }}</th>
                    {% endfor %}
                        <th>action</th>
                    </tr>
                </thead>
                <tbody>
                {% for target in targets %}
                    <tr>
                    {% for column in columns %}
                        <td>{{ target[column] }}</td>
                    {% endfor %}
                        <td>
                            <input type="button" onclick="window.open('{{ url_for('demo', guid=target.guid) }}', '_blank')" value="demo" />
                        {% if g.user.is_admin %}
                            <input type="button" onclick="if (confirm('Are you sure you want to delete this target and all of its beacons?')) { window.location='{{ url_for('target_delete', guid=target.guid) }}' }" value="delete" />
                        {% endif %}
                        </td>
                    </tr>
                {% endfor %}
                </tbody>
            </table>
        {% else %}
            <h5>No targets.</h5>
        {% endif %}
        </div>
    </div>
{% endblock %}


================================================
FILE: server/honeybadger/utils.py
================================================
import binascii
import os
import base64
import uuid

def generate_guid():
    return str(uuid.uuid4())

def generate_token(n=40):
    return binascii.hexlify(os.urandom(n))

def generate_nonce(n):
    return base64.b64encode(os.urandom(n)).decode()

from honeybadger.constants import CHANNELS

def freq2channel(freq):
    for channel in CHANNELS:
        if freq in CHANNELS[channel]:
            return channel

from honeybadger.models import Log
from honeybadger import db

class Logger(object):

    def _log(self, l, s):
        log = Log(
            level=l,
            message=s,
        )
        db.session.add(log)
        db.session.commit()

    def debug(self, s):
        self._log(10, s)

    def info(self, s):
        self._log(20, s)

    def warn(self, s):
        self._log(30, s)

    def error(self, s):
        self._log(40, s)

    def critical(self, s):
        self._log(50, s)


================================================
FILE: server/honeybadger/validators.py
================================================
import re

# 1 upper, 1 lower, 1 special, 1 number, minimim 10 chars
PASSWORD_REGEX = r'(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*\(\)]).{10,}'
EMAIL_REGEX = r'[^@]+@[a-zA-Z\d-]+(?:\.[a-zA-Z\d-]+)+'

def is_valid_email(email):
    if not re.match(r'^{}$'.format(EMAIL_REGEX), email):
        return False
    return True

def is_valid_password(password):
    if not re.match(r'^{}$'.format(PASSWORD_REGEX), password):
        return False
    return True


================================================
FILE: server/honeybadger/views.py
================================================
from flask import request, make_response, session, g, redirect, url_for, render_template, jsonify, flash, abort
from flask_cors import cross_origin
from honeybadger import app, db, logger
from honeybadger.processors import process_known_coords, process_wlan_survey, process_ip, process_json
from honeybadger.validators import is_valid_email, is_valid_password
from honeybadger.decorators import login_required, roles_required
from honeybadger.constants import ROLES
from honeybadger.utils import generate_token, generate_nonce
from honeybadger.models import User, Target, Beacon, Log
import os
from base64 import b64decode as b64d

# request preprocessors

@app.before_request
def load_user():
    g.user = None
    if session.get('user_id'):
        g.user = User.query.filter_by(id=session["user_id"]).first()

# control panel ui views

@app.route('/')
@app.route('/index')
@login_required
def index():
    return redirect(url_for('map'))

@app.route('/map')
@login_required
def map():
    return render_template('map.html', key=app.config['GOOGLE_API_KEY'])

@app.route('/beacons')
@login_required
def beacons():
    beacons = [b.serialized for t in Target.query.all() for b in t.beacons.all()]
    columns = ['id', 'target', 'agent', 'lat', 'lng', 'acc', 'ip', 'created']
    return render_template('beacons.html', columns=columns, beacons=beacons)

@app.route('/beacon/delete/<int:id>')
@login_required
@roles_required('admin')
def beacon_delete(id):
    beacon = Beacon.query.get(id)
    if beacon:
        db.session.delete(beacon)
        db.session.commit()
        flash('Beacon deleted.')
    else:
        flash('Invalid beacon ID.')
    return redirect(url_for('beacons'))

@app.route('/targets')
@login_required
def targets():
    targets = Target.query.all()
    columns = ['id', 'name', 'guid', 'beacon_count']
    return render_template('targets.html', columns=columns, targets=targets)

@app.route('/target/add', methods=['POST'])
@login_required
@roles_required('admin')
def target_add():
    name = request.form['target']
    if name:
        target = Target(
            name=name,
        )
        db.session.add(target)
        db.session.commit()
        flash('Target added.')
    return redirect(url_for('targets'))

@app.route('/target/delete/<string:guid>')
@login_required
@roles_required('admin')
def target_delete(guid):
    target = Target.query.filter_by(guid=guid).first()
    if target:
        db.session.delete(target)
        db.session.commit()
        flash('Target deleted.')
    else:
        flash('Invalid target GUID.')
    return redirect(url_for('targets'))

@app.route('/profile', methods=['GET', 'POST'])
@login_required
def profile():
    if request.method == 'POST':
        if g.user.check_password(request.form['current_password']):
            new_password = request.form['new_password']
            if new_password == request.form['confirm_password']:
                if is_valid_password(new_password):
                    g.user.password = new_password
                    db.session.add(g.user)
                    db.session.commit()
                    flash('Profile updated.')
                else:
                    flash('Password does not meet complexity requirements.')
            else:
                flash('Passwords do not match.')
        else:
            flash('Incorrect current password.')
    return render_template('profile.html', user=g.user)

# use an alternate route for reset as long as the logic is similar to init
@app.route('/password/reset/<string:token>', methods=['GET', 'POST'], endpoint='password_reset')
@app.route('/profile/activate/<string:token>', methods=['GET', 'POST'])
def profile_activate(token):
    user = User.query.filter_by(token=token).first()
    if user and user.status in (0, 3):
        if request.method == 'POST':
            new_password = request.form['new_password']
            if new_password == request.form['confirm_password']:
                if is_valid_password(new_password):
                    user.password = new_password
                    user.status = 1
                    user.token = None
                    db.session.add(user)
                    db.session.commit()
                    flash('Profile activated.')
                    return redirect(url_for('login'))
                else:
                    flash('Password does not meet complexity requirements.')
            else:
                flash('Passwords do not match.')
        return render_template('profile_activate.html', user=user)
    # abort to 404 for obscurity
    abort(404)

@app.route('/admin')
@login_required
@roles_required('admin')
def admin():
    users = User.query.all()
    columns = ['email', 'role_as_string', 'status_as_string']
    return render_template('admin.html', columns=columns, users=users, roles=ROLES)

@app.route('/admin/user/init', methods=['POST'])
@login_required
@roles_required('admin')
def admin_user_init():
    email = request.form['email']
    if is_valid_email(email):
        if not User.query.filter_by(email=email).first():
            user = User(
                email=email,
                token=generate_token(),
            )
            db.session.add(user)
            db.session.commit()
            flash('User initialized.')
        else:
            flash('Username already exists.')
    else:
        flash('Invalid email address.')
    # send notification to user
    return redirect(url_for('admin'))

@app.route('/admin/user/<string:action>/<int:id>')
@login_required
@roles_required('admin')
def admin_user(action, id):
    user = User.query.get(id)
    if user:
        if user != g.user:
            if action == 'activate' and user.status == 2:
                user.status = 1
                db.session.add(user)
                db.session.commit()
                flash('User activated.')
            elif action == 'deactivate' and user.status == 1:
                user.status = 2
                db.session.add(user)
                db.session.commit()
                flash('User deactivated.')
            elif action == 'reset' and user.status == 1:
                user.status = 3
                user.token = generate_token()
                db.session.add(user)
                db.session.commit()
                flash('User reset.')
            elif action == 'delete':
                db.session.delete(user)
                db.session.commit()
                flash('User deleted.')
            else:
                flash('Invalid user action.')
        else:
            flash('Self-modification denied.')
    else:
        flash('Invalid user ID.')
    return redirect(url_for('admin'))

@app.route('/login', methods=['GET', 'POST'])
def login():
    # redirect to home if already logged in
    if session.get('user_id'):
        return redirect(url_for('index'))
    if request.method == 'POST':
        user = User.get_by_email(request.form['email'])
        if user and user.status == 1 and user.check_password(request.form['password']):
            session['user_id'] = user.id
            flash('You have successfully logged in.')
            return redirect(url_for('index'))
        flash('Invalid username or password.')
    return render_template('login.html')

@app.route('/logout')
@login_required
def logout():
    session.pop('user_id', None)
    flash('You have been logged out')
    return redirect(url_for('index'))

@app.route('/demo/<string:guid>', methods=['GET', 'POST'])
def demo(guid):
    text = None
    if request.method == 'POST':
        text = request.values['text']
        key = request.values['key']
        if g.user.check_password(key):
            if text and 'alert(' in text:
                text = 'Congrats! You entered: {}'.format(text)
            else:
                text = 'Nope. Try again.'
        else:
            text = 'Incorrect password.'
    nonce = generate_nonce(24)
    response = make_response(render_template('demo.html', target=guid, text=text, nonce=nonce))
    response.headers['X-XSS-Protection'] = '0'#'1; report=https://hb.lanmaster53.com/api/beacon/{}/X-XSS-Protection'.format(guid)
    uri = url_for('api_beacon', target=guid, agent='Content-Security-Policy')
    response.headers['Content-Security-Policy-Report-Only'] = 'script-src \'nonce-{}\'; report-uri {}'.format(nonce, uri)
    return response

@app.route('/log')
@login_required
def log():
    # hidden capability to clear logs
    if request.values.get('clear'):
        Log.query.delete()
        db.session.commit()
        return redirect(url_for('log'))
    content = ''
    logs = Log.query.order_by(Log.created).all()
    for log in logs:
        content += '[{}] [{}] {}{}'.format(log.created_as_string, log.level_as_string, log.message, os.linesep)
    return render_template('log.html', content=content)

# control panel api views

@app.route('/api/beacons')
@login_required
def api_beacons():
    beacons = [b.serialized for t in Target.query.all() for b in t.beacons.all()]
    return jsonify(beacons=beacons)

# agent api views

@app.route('/api/beacon/<target>/<agent>', methods=['GET', 'POST'])
@cross_origin()
def api_beacon(target, agent):
    logger.info('{}'.format('='*50))
    data = {'target': target, 'agent': agent}
    logger.info('Target: {}'.format(target))
    logger.info('Agent: {}'.format(agent))
    # check if target is valid
    if target not in [x.guid for x in Target.query.all()]:
        logger.error('Invalid target GUID.')
        abort(404)
    # extract universal parameters
    comment = b64d(request.values.get('comment', '')) or None
    ip = request.environ['REMOTE_ADDR']
    port = request.environ['REMOTE_PORT']
    useragent = request.environ['HTTP_USER_AGENT']
    data.update({'comment': comment, 'ip': ip, 'port': port, 'useragent': useragent})
    logger.info('Connection from {} @ {}:{} via {}'.format(target, ip, port, agent))
    logger.info('Parameters: {}'.format(request.values.to_dict()))
    logger.info('User-Agent: {}'.format(useragent))
    logger.info('Comment: {}'.format(comment))
    data.update(request.values.to_dict())
    # process json payloads
    if request.json:
        if process_json(data, request.json):
            abort(404)
    # process known coordinates
    if all(k in data for k in ('lat', 'lng', 'acc')):
        if process_known_coords(data):
            abort(404)
    # process wireless survey
    elif all(k in data for k in ('os', 'data')):
        if process_wlan_survey(data):
            abort(404)
    # process ip geolocation (includes fallback)
    process_ip(data)
    abort(404)


================================================
FILE: server/honeybadger.py
================================================
from honeybadger import app

if __name__ == '__main__':
    app.run(host='0.0.0.0')#threaded=True)


================================================
FILE: server/requirements.txt
================================================
Flask
Flask-Bcrypt
Flask-SQLAlchemy
Flask-CORS
requests


================================================
FILE: util/wireless_survey.ps1
================================================
# Accept a command-line argument for the URI
param (
    [string]$uri = ""
)

#Input error checking: Valid argument name/value?
if($uri -eq "") {
    echo "Usage: .\wireless_survey.ps1 -uri <URI>"
    exit 1
}

# Poll for wireless network information with Netsh
$wifi = netsh wlan show networks mode=bssid | findstr "SSID Signal Channel"

# Fix data before Base64-encoding to preserve line breaks.
# The server-side parser breaks without this.
echo $wifi > wifidat.txt                                # Write netsh results to temporary file
$wifi = Get-Content wifidat.txt -Encoding UTF8 -Raw     # Update wifi variable with Raw switch
rm wifidat.txt                                          # Remove temporary file

# Set encoding of wifi data
$wifibytes = [System.Text.Encoding]::UTF8.GetBytes($wifi)

# Base64-encode the wifi data bytes
$wifienc = [Convert]::ToBase64String($wifibytes)

# Assemble post data
$postdat = @{os='windows';data=$wifienc}

# Send POST request to server, using CMD as agent
try {
    $statuscode = (Invoke-WebRequest -Uri "$uri/CMD" -Method POST -Body $postdat).statuscode
} catch {
    $statuscode = $_.Exception.Response.StatusCode.Value__
}

# Output error checking: Expected response code?
if ([string]::IsNullOrEmpty($statuscode)){
    echo "Unable to reach the specified URI."
    echo "Check the URI and the HoneyBadger server, and try again."
    exit 1
} elseif($statuscode -eq 404) {
    echo "The requested server responded with 404."
    echo "Either the page really was not found, or the request was successful."
    echo "Check the HoneyBadger web client for data to verify."
    exit 0
} else {
    echo "The requested server responded with an unexpected response code."
    echo "Check the URI and the HoneyBadger server, and try again."
    exit 1
}

================================================
FILE: util/wireless_survey.sh
================================================
#!/bin/bash

# Input error checking: Valid argument count?
if [[ "$#" -ne 1 ]]; then
    echo "Usage: ./wireless_survey.sh <URL>"
    exit 1
fi

# Get wireless information
wireless_data=$(iwlist scan 2>&1 | egrep 'Address|ESSID|Signal')

# Create post data string
post_data="os=linux&data=$(echo "$wireless_data" | base64 -w 0)"

# Send the data to the URL supplied via command line argument. Store the response code.
response_code=$(curl -d "$post_data" "$1/CMD" --write-out %{http_code} --silent --output /dev/null)
curl_code="$?"

# Output error checking: Expected response code?
if [[ $response_code -eq "000" ]]; then
    echo "Unable to reach the specified URL. Curl response code: $curl_code."
    exit 1;
elif [[ $response_code -eq "404" ]]; then
    echo "The requested server responded with 404."
    echo "Either the page really was not found, or the request was successful."
    echo "Check the HoneyBadger web client for data to verify."
    exit 0;
else
    echo "The URL responded with an unexpected response code."
    echo "Check the URL and the HoneyBadger server, and try again."
    exit 1;
fi
Download .txt
gitextract_vjpgv5fm/

├── .gitignore
├── LICENSE.txt
├── README.md
├── server/
│   ├── honeybadger/
│   │   ├── __init__.py
│   │   ├── constants.py
│   │   ├── decorators.py
│   │   ├── models.py
│   │   ├── parsers.py
│   │   ├── plugins.py
│   │   ├── processors.py
│   │   ├── static/
│   │   │   ├── badger.css
│   │   │   ├── badger.js
│   │   │   ├── common.js
│   │   │   ├── honey.jar
│   │   │   ├── normalize.css
│   │   │   ├── skeleton.css
│   │   │   └── sorttable.js
│   │   ├── templates/
│   │   │   ├── admin.html
│   │   │   ├── beacons.html
│   │   │   ├── demo.html
│   │   │   ├── layout.html
│   │   │   ├── log.html
│   │   │   ├── login.html
│   │   │   ├── map.html
│   │   │   ├── profile.html
│   │   │   ├── profile_activate.html
│   │   │   ├── register.html
│   │   │   └── targets.html
│   │   ├── utils.py
│   │   ├── validators.py
│   │   └── views.py
│   ├── honeybadger.py
│   └── requirements.txt
└── util/
    ├── wireless_survey.ps1
    └── wireless_survey.sh
Download .txt
SYMBOL INDEX (84 symbols across 12 files)

FILE: server/honeybadger/__init__.py
  function initdb (line 44) | def initdb(username, password):
  function dropdb (line 62) | def dropdb():

FILE: server/honeybadger/decorators.py
  function login_required (line 6) | def login_required(func):
  function roles_required (line 14) | def roles_required(*roles):
  function wrapper (line 25) | def wrapper(*args, **kwargs):
  function no_cache (line 30) | def no_cache(func):

FILE: server/honeybadger/models.py
  function stringify_datetime (line 7) | def stringify_datetime(value):
  class BaseModel (line 13) | class BaseModel(db.Model):
    method created_as_string (line 19) | def created_as_string(self):
  class Log (line 22) | class Log(BaseModel):
    method level_as_string (line 28) | def level_as_string(self):
  class Beacon (line 31) | class Beacon(BaseModel):
    method serialized (line 44) | def serialized(self):
    method __repr__ (line 60) | def __repr__(self):
  class Target (line 63) | class Target(BaseModel):
    method beacon_count (line 70) | def beacon_count(self):
    method __repr__ (line 73) | def __repr__(self):
  class User (line 76) | class User(BaseModel):
    method role_as_string (line 85) | def role_as_string(self):
    method status_as_string (line 89) | def status_as_string(self):
    method password (line 93) | def password(self):
    method password (line 97) | def password(self, password):
    method check_password (line 100) | def check_password(self, password):
    method is_admin (line 104) | def is_admin(self):
    method get_by_email (line 110) | def get_by_email(email):
    method __repr__ (line 113) | def __repr__(self):

FILE: server/honeybadger/parsers.py
  class AP (line 4) | class AP(object):
    method __init__ (line 6) | def __init__(self, ssid=None, bssid=None, ss=None, channel=None):
    method serialized_for_google (line 13) | def serialized_for_google(self):
    method __repr__ (line 20) | def __repr__(self):
  function parse_google (line 23) | def parse_google(jsondata):
  function parse_airport (line 29) | def parse_airport(content):
  function parse_netsh (line 37) | def parse_netsh(content):
  function parse_iwlist (line 57) | def parse_iwlist(content):

FILE: server/honeybadger/plugins.py
  function get_coords_from_google (line 5) | def get_coords_from_google(aps):
  function get_coords_from_ipstack (line 27) | def get_coords_from_ipstack(ip):
  function get_coords_from_ipinfo (line 52) | def get_coords_from_ipinfo(ip):

FILE: server/honeybadger/processors.py
  function add_beacon (line 8) | def add_beacon(*args, **kwargs):
  function process_json (line 14) | def process_json(data, jsondata):
  function process_known_coords (line 45) | def process_known_coords(data):
  function process_wlan_survey (line 60) | def process_wlan_survey(data):
  function process_ip (line 102) | def process_ip(data):

FILE: server/honeybadger/static/badger.js
  function load_map (line 4) | function load_map() {
  function add_marker (line 33) | function add_marker(opts, place, beacon) {
  function load_markers (line 49) | function load_markers(json) {
  function toggle_marker (line 113) | function toggle_marker(element) {

FILE: server/honeybadger/static/common.js
  function flash (line 1) | function flash(msg) {
  function copy2clip (line 7) | function copy2clip(s) {

FILE: server/honeybadger/static/sorttable.js
  function dean_addEvent (line 382) | function dean_addEvent(element, type, handler) {
  function removeEvent (line 408) | function removeEvent(element, type, handler) {
  function handleEvent (line 419) | function handleEvent(event) {
  function fixEvent (line 435) | function fixEvent(event) {

FILE: server/honeybadger/utils.py
  function generate_guid (line 6) | def generate_guid():
  function generate_token (line 9) | def generate_token(n=40):
  function generate_nonce (line 12) | def generate_nonce(n):
  function freq2channel (line 17) | def freq2channel(freq):
  class Logger (line 25) | class Logger(object):
    method _log (line 27) | def _log(self, l, s):
    method debug (line 35) | def debug(self, s):
    method info (line 38) | def info(self, s):
    method warn (line 41) | def warn(self, s):
    method error (line 44) | def error(self, s):
    method critical (line 47) | def critical(self, s):

FILE: server/honeybadger/validators.py
  function is_valid_email (line 7) | def is_valid_email(email):
  function is_valid_password (line 12) | def is_valid_password(password):

FILE: server/honeybadger/views.py
  function load_user (line 16) | def load_user():
  function index (line 26) | def index():
  function map (line 31) | def map():
  function beacons (line 36) | def beacons():
  function beacon_delete (line 44) | def beacon_delete(id):
  function targets (line 56) | def targets():
  function target_add (line 64) | def target_add():
  function target_delete (line 78) | def target_delete(guid):
  function profile (line 90) | def profile():
  function profile_activate (line 111) | def profile_activate(token):
  function admin (line 136) | def admin():
  function admin_user_init (line 144) | def admin_user_init():
  function admin_user (line 165) | def admin_user(action, id):
  function login (line 198) | def login():
  function logout (line 213) | def logout():
  function demo (line 219) | def demo(guid):
  function log (line 240) | def log():
  function api_beacons (line 256) | def api_beacons():
  function api_beacon (line 264) | def api_beacon(target, agent):
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (153K chars).
[
  {
    "path": ".gitignore",
    "chars": 50,
    "preview": "*.pyc\n*sublime*\nvenv/\nagents/\nserver_old/\ndata.db\n"
  },
  {
    "path": "LICENSE.txt",
    "chars": 35141,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "README.md",
    "chars": 5627,
    "preview": "# HoneyBadger v2\n\nHoneyBadger is a framework for targeted geolocation. While honeypots are traditionally used to passive"
  },
  {
    "path": "server/honeybadger/__init__.py",
    "chars": 2391,
    "preview": "from flask import Flask\nfrom flask_bcrypt import Bcrypt\nfrom flask_sqlalchemy import SQLAlchemy\nimport logging\nimport os"
  },
  {
    "path": "server/honeybadger/constants.py",
    "chars": 1356,
    "preview": "ROLES = {\n    0: 'admin',\n    1: 'analyst',\n}\n\nSTATUSES = {\n    0: 'initialized',\n    1: 'active',\n    2: 'inactive',\n  "
  },
  {
    "path": "server/honeybadger/decorators.py",
    "chars": 1127,
    "preview": "from flask import g, redirect, url_for, abort, make_response\nfrom honeybadger.constants import ROLES\nfrom functools impo"
  },
  {
    "path": "server/honeybadger/models.py",
    "chars": 3393,
    "preview": "from honeybadger import db, bcrypt\nfrom honeybadger.constants import ROLES, STATUSES, LEVELS\nfrom honeybadger.utils impo"
  },
  {
    "path": "server/honeybadger/parsers.py",
    "chars": 13979,
    "preview": "from honeybadger.utils import freq2channel\nimport os\n\nclass AP(object):\n\n    def __init__(self, ssid=None, bssid=None, s"
  },
  {
    "path": "server/honeybadger/plugins.py",
    "chars": 2727,
    "preview": "from honeybadger import app, logger\nimport requests\nimport json\n\ndef get_coords_from_google(aps):\n    logger.info('Geolo"
  },
  {
    "path": "server/honeybadger/processors.py",
    "chars": 4352,
    "preview": "from honeybadger import db, logger\nfrom honeybadger.models import Beacon\nfrom honeybadger.parsers import parse_airport, "
  },
  {
    "path": "server/honeybadger/static/badger.css",
    "chars": 3490,
    "preview": "html {\n    height: 100%;\n}\n\nbody {\n    font-family: Raleway, Arial, Helvetica, sans-serif;\n    font-size: 1.5rem;\n    he"
  },
  {
    "path": "server/honeybadger/static/badger.js",
    "chars": 5279,
    "preview": "var map\nvar bounds\n\nfunction load_map() {\n    var coords = new google.maps.LatLng(0,0);\n    var mapOptions = {\n        z"
  },
  {
    "path": "server/honeybadger/static/common.js",
    "chars": 727,
    "preview": "function flash(msg) {\n    $('#flash').html(msg);\n    $('#flash').css('visibility', 'visible');\n    setTimeout(function()"
  },
  {
    "path": "server/honeybadger/static/normalize.css",
    "chars": 7797,
    "preview": "/*! normalize.css v3.0.2 | MIT License | git.io/normalize */\n\n/**\n * 1. Set default font family to sans-serif.\n * 2. Pre"
  },
  {
    "path": "server/honeybadger/static/skeleton.css",
    "chars": 9952,
    "preview": "/*\n* Skeleton V2.0.4\n* Copyright 2014, Dave Gamache\n* www.getskeleton.com\n* Free to use under the MIT license.\n* http://"
  },
  {
    "path": "server/honeybadger/static/sorttable.js",
    "chars": 17025,
    "preview": "/*\n  SortTable\n  version 2\n  7th April 2007\n  Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/\n\n  Inst"
  },
  {
    "path": "server/honeybadger/templates/admin.html",
    "chars": 2650,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n    <br/>\n    <div class=\"row\">\n        <div class=\"six columns offset-by-t"
  },
  {
    "path": "server/honeybadger/templates/beacons.html",
    "chars": 1273,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n    <br/>\n    <div class=\"row\">\n        <div class=\"u-full-width center-con"
  },
  {
    "path": "server/honeybadger/templates/demo.html",
    "chars": 1931,
    "preview": "<!DOCTYPE HTML>\n<html>\n<head>\n    <title>Honey Badger Demo</title>\n    <script nonce=\"{{ nonce }}\">\nvar api = \"{{ reques"
  },
  {
    "path": "server/honeybadger/templates/layout.html",
    "chars": 2373,
    "preview": "<!DOCTYPE HTML>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>Honey Badger</title>\n    <meta name=\"description\" co"
  },
  {
    "path": "server/honeybadger/templates/log.html",
    "chars": 225,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n    <br/>\n    <div class=\"row\">\n        <div class=\"u-full-width center-con"
  },
  {
    "path": "server/honeybadger/templates/login.html",
    "chars": 759,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n    <div class=\"row\">\n        <div class=\"four columns offset-by-four login"
  },
  {
    "path": "server/honeybadger/templates/map.html",
    "chars": 578,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n    <div id=\"filter\" class=\"form filter rounded shaded\">\n        <div class"
  },
  {
    "path": "server/honeybadger/templates/profile.html",
    "chars": 880,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n    <div class=\"row\">\n        <div class=\"four columns offset-by-four login"
  },
  {
    "path": "server/honeybadger/templates/profile_activate.html",
    "chars": 785,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n    <div class=\"row\">\n        <div class=\"four columns offset-by-four login"
  },
  {
    "path": "server/honeybadger/templates/register.html",
    "chars": 952,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n    <div class=\"row\">\n        <div class=\"four columns offset-by-four login"
  },
  {
    "path": "server/honeybadger/templates/targets.html",
    "chars": 1910,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n    <br/>\n    {% if g.user.is_admin %}\n    <div class=\"row\">\n        <div c"
  },
  {
    "path": "server/honeybadger/utils.py",
    "chars": 905,
    "preview": "import binascii\nimport os\nimport base64\nimport uuid\n\ndef generate_guid():\n    return str(uuid.uuid4())\n\ndef generate_tok"
  },
  {
    "path": "server/honeybadger/validators.py",
    "chars": 460,
    "preview": "import re\n\n# 1 upper, 1 lower, 1 special, 1 number, minimim 10 chars\nPASSWORD_REGEX = r'(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9]"
  },
  {
    "path": "server/honeybadger/views.py",
    "chars": 10595,
    "preview": "from flask import request, make_response, session, g, redirect, url_for, render_template, jsonify, flash, abort\nfrom fla"
  },
  {
    "path": "server/honeybadger.py",
    "chars": 99,
    "preview": "from honeybadger import app\n\nif __name__ == '__main__':\n    app.run(host='0.0.0.0')#threaded=True)\n"
  },
  {
    "path": "server/requirements.txt",
    "chars": 56,
    "preview": "Flask\nFlask-Bcrypt\nFlask-SQLAlchemy\nFlask-CORS\nrequests\n"
  },
  {
    "path": "util/wireless_survey.ps1",
    "chars": 1845,
    "preview": "# Accept a command-line argument for the URI\r\nparam (\r\n    [string]$uri = \"\"\r\n)\r\n\r\n#Input error checking: Valid argumen"
  },
  {
    "path": "util/wireless_survey.sh",
    "chars": 1114,
    "preview": "#!/bin/bash\n\n# Input error checking: Valid argument count?\nif [[ \"$#\" -ne 1 ]]; then\n    echo \"Usage: ./wireless_survey."
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the lanmaster53/honeybadger GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (140.4 KB), approximately 36.4k tokens, and a symbol index with 84 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!