initial commit
commit
8b5b397ebc
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://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 <https://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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
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
|
||||
<https://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
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
@ -0,0 +1,67 @@
|
|||
<img src="https://github.com/rabobank-cdc/Blue-ATTACK/wiki/images/logo.png" alt="Blue ATT&CK" width=20% height=20%>
|
||||
|
||||
# Blue ATT&CK
|
||||
#### Mapping your blue team to ATT&CK
|
||||
|
||||
To get started with Blue ATT&CK, check out the
|
||||
[Wiki](https://github.com/rabobank-cdc/Blue-ATTACK/wiki/Getting-started).
|
||||
|
||||
Blue ATT&CK will help blue teams in scoring and comparing data source quality, visibility coverage, detection coverage and threat actor behaviours. The Blue ATT&CK framework consists of a Python tool, YAML administration files and [scoring tables](https://github.com/rabobank-cdc/Blue-ATTACK/raw/master/scoring_table.xlsx) for the different aspects.
|
||||
|
||||
Blue ATT&CK will help you to:
|
||||
|
||||
- Administrate and score the quality of your data sources.
|
||||
- Get insight on the visibility you have on for example endpoints.
|
||||
- Map your detection coverage.
|
||||
- Map threat actor behaviours.
|
||||
- Compare visibility, detections and threat actor behaviours in order to uncover possible improvements in detection and visibility. This can help you to prioritise your blue teaming efforts.
|
||||
|
||||
## Authors and contribution
|
||||
This project is developed and maintained by [Marcus Bakker](https://github.com/marcusbakker) (Twitter: [@bakker3m](https://twitter.com/bakk3rm)) and [Ruben Bouman](https://github.com/rubinatorz) (Twitter: [@rubenb_2](https://twitter.com/rubenb_2/)). Feel free to contact, DMs are open.
|
||||
|
||||
We welcome contributions! Contributions can be both in code, as well as in ideas you might have for further development, usability improvements, etc.
|
||||
|
||||
### Work of others
|
||||
Some functionality within Blue ATT&CK was inspired by work of
|
||||
others:
|
||||
- Roberto Rodriguez's work on data quality and scoring of ATT&CK techniques ([How Hot Is Your Hunt Team?](https://cyberwardog.blogspot.com/2017/07/how-hot-is-your-hunt-team.html), [Ready to hunt? First, Show me your data!](https://cyberwardog.blogspot.com/2017/12/ready-to-hunt-first-show-me-your-data.html)).
|
||||
- The MITRE ATT&CK Mapping project on GitHub:
|
||||
https://github.com/siriussecurity/mitre-attack-mapping.
|
||||
|
||||
## Example
|
||||
|
||||
YAML files are used for administrating scores and relevant metadata. All
|
||||
of which can be visualised by loading JSON layer files into the [ATT&CK Navigator](https://github.com/mitre-attack/attack-navigator) (some types of scores and metadata can also be written to Excel).
|
||||
|
||||
See below an example of mapping your data sources to ATT&CK which gives you a rough overview of your visibility coverage:
|
||||
|
||||
<img src="https://github.com/rabobank-cdc/Blue-ATTACK/wiki/images/example_data_sources.png" alt="Blue ATT&CK"><br>
|
||||
|
||||
|
||||
## Installation and requirements
|
||||
|
||||
See our GitHub Wiki: [Installation and requirements](https://github.com/rabobank-cdc/Blue-ATTACK/wiki/Installation-and-requirements).
|
||||
|
||||
## Future developments
|
||||
|
||||
- Add more graphs:
|
||||
- [ ] Detections: improvement based on newly added detections and improvements on the level/score of existing detections. Possibly with a changelog with support for comments.
|
||||
- [ ] Visibility: improvement in the quality of an existing data source.
|
||||
- Groups:
|
||||
- [ ] Have groups YAML file type that contains a count on how popular a certain technique is. This can be very useful to map things such as Red Canary's [Threat Detection Report 2019](https://redcanary.com/resources/guides/threat-detection-report/).
|
||||
- Excel output for:
|
||||
- [ ] Techniques administration YAML file: visibility coverage
|
||||
- [ ] Techniques administration YAML file: detection coverage
|
||||
- Data quality Excel sheet:
|
||||
- [ ] Add colors to the data quality scores in the Excel sheet.
|
||||
- YAML files:
|
||||
- [ ] Create an option within the tool to migrate an old administration YAML file version to a new version (such as adding specific key-value pairs).
|
||||
- MITRE ATT&CK updates
|
||||
- [ ] Have a smart way of knowing what to update in your data source and technique administration files once MITRE releases updates.
|
||||
- [ ] Data sources: check for missing data sources in data sources administration files.
|
||||
- Minimal visibility
|
||||
- [ ] Integrate information into the framework on what a minimal set of visibility for a technique should be, before you can say to have useful visibility (e.g. technique X requires at least to have visibility on process monitoring, process command line monitoring and DLL monitoring.)
|
||||
|
||||
## License: GPL-3.0
|
||||
[Blue ATT&CK's GNU General Public License v3.0](https://github.com/rabobank-cdc/Blue-ATTACK/blob/master/LICENSE)
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
import argparse
|
||||
from interactive_menu import *
|
||||
import os
|
||||
import signal
|
||||
|
||||
|
||||
def init_menu():
|
||||
"""
|
||||
Initialise the command line parameter menu.
|
||||
:return:
|
||||
"""
|
||||
menu_parser = argparse.ArgumentParser(description='Create MITRE ATT&CK layers for visibility, detection and groups.',
|
||||
epilog='Source: https://github.com/rabobank-cdc/Blue-ATTACK')
|
||||
menu_parser.add_argument('--version', action='version', version='%(prog)s ' + VERSION)
|
||||
menu_parser.add_argument('-i', '--interactive', help='launch the interactive menu, which has support for all modes',
|
||||
action='store_true')
|
||||
|
||||
# add subparsers
|
||||
subparsers = menu_parser.add_subparsers(title='MODE',
|
||||
description='Select the mode to use. Every mode has its own arguments and '
|
||||
'help info displayed using: {visibility, detection, group, '
|
||||
'generic} --help', metavar='', dest='subparser')
|
||||
|
||||
# create the data source parser
|
||||
parser_data_sources = subparsers.add_parser('datasource', help='data source mapping and quality',
|
||||
aliases=['ds'],
|
||||
description='Create a heat map based on data sources, output data '
|
||||
'sources to Excel or generate a data source improvement '
|
||||
'graph.')
|
||||
parser_data_sources.add_argument('-f', '--file', help='path to the data source administration YAML file',
|
||||
required=True)
|
||||
parser_data_sources.add_argument('-l', '--layer', help='generate a data source layer for the ATT&CK navigator',
|
||||
action='store_true')
|
||||
parser_data_sources.add_argument('-e', '--excel', help='generate an Excel sheet with all data source',
|
||||
action='store_true')
|
||||
parser_data_sources.add_argument('-g', '--graph', help='generate a graph with data sources added through time',
|
||||
action='store_true')
|
||||
parser_data_sources.add_argument('-y', '--yaml', help='generate a technique administration YAML file with '
|
||||
'visibility scores based on the number of available data '
|
||||
'sources',
|
||||
action='store_true')
|
||||
|
||||
# create the visibility parser
|
||||
parser_visibility = subparsers.add_parser('visibility', aliases=['v'],
|
||||
help='visibility coverage mapping based on techniques and data sources',
|
||||
description='Create a heat map based on visibility scores or overlay '
|
||||
'visibility with detections.')
|
||||
parser_visibility.add_argument('-ft', '--file-tech', help='path to the technique administration YAML file (used to '
|
||||
'score the level of visibility)', required=True)
|
||||
parser_visibility.add_argument('-fd', '--file-ds', help='path to the data source administration YAML file (used to '
|
||||
'add metadata on the involved data sources)',
|
||||
required=True)
|
||||
parser_visibility.add_argument('-l', '--layer', help='generate a visibility layer for the ATT&CK navigator',
|
||||
action='store_true')
|
||||
parser_visibility.add_argument('-o', '--overlay', help='generate a visibility layer overlayed with detections for '
|
||||
'the ATT&CK navigator.', action='store_true')
|
||||
|
||||
# create the detection parser
|
||||
parser_detection = subparsers.add_parser('detection', aliases=['d'],
|
||||
help='detection coverage mapping based on techniques',
|
||||
description='Create a heat map based on detection scores, overlay '
|
||||
'detections with visibility or generate a detection '
|
||||
'improvement graph.')
|
||||
parser_detection.add_argument('-ft', '--file-tech', help='path to the technique administration YAML file (used to '
|
||||
'score the level of visibility)', required=True)
|
||||
parser_detection.add_argument('-fd', '--file-ds', help='path to the data source administration YAML file (used in '
|
||||
'the overlay with visibility to add metadata on the '
|
||||
'involved data sources)')
|
||||
parser_detection.add_argument('-l', '--layer', help='generate detection layer for the ATT&CK navigator',
|
||||
action='store_true')
|
||||
parser_detection.add_argument('-o', '--overlay', help='generate a detection layer overlayed with visibility for '
|
||||
'the ATT&CK navigator.', action='store_true')
|
||||
parser_detection.add_argument('-g', '--graph', help='generate a graph with detections added through time',
|
||||
action='store_true')
|
||||
|
||||
# create the group parser
|
||||
parser_group = subparsers.add_parser('group', aliases=['g'],
|
||||
description='Create threat actor group heat maps, compare group(s) and '
|
||||
'compare group(s) with visibility and detection coverage.',
|
||||
help='threat actor group mapping')
|
||||
parser_group.add_argument('-g', '--groups', help='specify the groups to include separated using commas. '
|
||||
'Group can be their ID, name or alias (default is all groups). '
|
||||
'Other option is to provide a YAML file with a custom group(s) '
|
||||
'(default = all)',
|
||||
default='all')
|
||||
parser_group.add_argument('-o', '--overlay', help='specify what to overlay on the group(s) (provided using the '
|
||||
'arguments \'-g/--groups\'): group(s), visibility or detection. '
|
||||
'When overlaying a GROUP: the group can be their ID, name or '
|
||||
'alias separated using commas. Or provide a file path of a YAML '
|
||||
'file with a custom group(s). When overlaying DETECTION or '
|
||||
'VISIBILITY provide a YAML with the technique administration.')
|
||||
parser_group.add_argument('-t', '--overlay-type', help='specify the type of overlay (default = group)',
|
||||
choices=['group', 'visibility', 'detection'], default='group')
|
||||
parser_group.add_argument('--software-group', help='add techniques to the heat map by checking which software is '
|
||||
'used by group(s), and hence which techniques the software '
|
||||
'supports (does not influence the scores). If overlay group(s) '
|
||||
'are provided, only software related to those group(s) are '
|
||||
'included', action='store_true', default=False)
|
||||
parser_group.add_argument('-p', '--platform', help='specify the platform (default = Windows)',
|
||||
choices=['all', 'Linux', 'macOS', 'Windows'], default='Windows')
|
||||
parser_group.add_argument('-s', '--stage', help='specify the stage (default = attack)',
|
||||
choices=['attack', 'pre-attack'], default='attack')
|
||||
|
||||
# create the generic parser
|
||||
parser_generic = subparsers.add_parser('generic', description='Generic functions which will output to stdout.',
|
||||
help='includes: statistics on ATT&CK data source and updates on techniques'
|
||||
', groups and software', aliases=['ge'])
|
||||
|
||||
parser_generic.add_argument('-s', '--statistics', help='get a sorted count on how much techniques are covered by a '
|
||||
'particular data source', action='store_true')
|
||||
|
||||
parser_generic.add_argument('-u', '--updates', help='get a sorted list for when updates were released for '
|
||||
'techniques, groups or software',
|
||||
choices=['techniques', 'groups', 'software'])
|
||||
parser_generic.add_argument('--sort', help='sorting of the output from \'-u/--update\' on modified or creation '
|
||||
'date (default = modified)', choices=['modified', 'created'],
|
||||
default='modified')
|
||||
|
||||
return menu_parser
|
||||
|
||||
|
||||
def menu(menu_parser):
|
||||
"""
|
||||
Parser for the command line parameter menu and calls the appropriate functions.
|
||||
:param menu_parser: the argparse menu as created with 'init_menu()'
|
||||
:return:
|
||||
"""
|
||||
args = menu_parser.parse_args()
|
||||
|
||||
if args.interactive:
|
||||
interactive_menu()
|
||||
|
||||
elif args.subparser in ['datasource', 'ds']:
|
||||
if check_file_type(args.file, FILE_TYPE_DATA_SOURCE_ADMINISTRATION):
|
||||
if args.layer:
|
||||
generate_data_sources_layer(args.file)
|
||||
if args.excel:
|
||||
export_data_source_list_to_excel(args.file)
|
||||
if args.graph:
|
||||
plot_data_sources_graph(args.file)
|
||||
if args.yaml:
|
||||
generate_technique_administration_file(args.file)
|
||||
|
||||
elif args.subparser in ['visibility', 'v']:
|
||||
if check_file_type(args.file_tech, FILE_TYPE_TECHNIQUE_ADMINISTRATION) and \
|
||||
check_file_type(args.file_ds, FILE_TYPE_DATA_SOURCE_ADMINISTRATION):
|
||||
if args.layer:
|
||||
generate_visibility_layer(args.file_tech, args.file_ds, False)
|
||||
if args.overlay:
|
||||
generate_visibility_layer(args.file_tech, args.file_ds, True)
|
||||
|
||||
elif args.subparser in ['group', 'g']:
|
||||
generate_group_heat_map(args.groups, args.overlay, args.overlay_type, args.stage, args.platform, args.software_group)
|
||||
|
||||
elif args.subparser in ['detection', 'd']:
|
||||
if args.overlay:
|
||||
if not args.file_ds:
|
||||
print('[!] Doing an overlay requires adding the data source administration YAML file (\'--file-ds\')')
|
||||
quit()
|
||||
if not check_file_type(args.file_ds, FILE_TYPE_DATA_SOURCE_ADMINISTRATION):
|
||||
quit()
|
||||
|
||||
if check_file_type(args.file_tech, FILE_TYPE_TECHNIQUE_ADMINISTRATION):
|
||||
if args.layer:
|
||||
generate_detection_layer(args.file_tech, args.file_ds, False)
|
||||
if args.overlay and check_file_type(args.file_ds, FILE_TYPE_DATA_SOURCE_ADMINISTRATION):
|
||||
generate_detection_layer(args.file_tech, args.file_ds, True)
|
||||
if args.graph:
|
||||
plot_detection_graph(args.file_tech)
|
||||
|
||||
elif args.subparser in ['generic', 'ge']:
|
||||
if args.statistics:
|
||||
get_statistics()
|
||||
elif args.updates:
|
||||
get_updates(args.updates, args.sort)
|
||||
|
||||
|
||||
def prepare_folders():
|
||||
"""
|
||||
Create the folders 'cache' and 'output' if they do not exist.
|
||||
:return:
|
||||
"""
|
||||
if not os.path.exists('cache'):
|
||||
os.mkdir('cache')
|
||||
if not os.path.exists('output'):
|
||||
os.mkdir('output')
|
||||
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
"""
|
||||
Function to handles exiting via Ctrl+C.
|
||||
:param signum:
|
||||
:param frame:
|
||||
:return:
|
||||
"""
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
prepare_folders()
|
||||
menu(init_menu())
|
|
@ -0,0 +1,264 @@
|
|||
import simplejson
|
||||
from generic import *
|
||||
import xlsxwriter
|
||||
import copy
|
||||
# Imports for pandas and plotly are because of performance reasons in the function that uses these libraries.
|
||||
|
||||
|
||||
def generate_data_sources_layer(filename):
|
||||
"""
|
||||
Generates a generic layer for data sources.
|
||||
:param filename: the filename of the yaml file containing the data sources administration
|
||||
:return:
|
||||
"""
|
||||
my_data_sources, name, platform, exceptions = _load_data_sources(filename)
|
||||
|
||||
# Do the mapping between my data sources and MITRE data sources:
|
||||
my_techniques = _map_and_colorize_techniques(my_data_sources, exceptions)
|
||||
|
||||
layer = get_layer_template_data_sources("Data sources " + name, 'description', 'attack', platform)
|
||||
layer['techniques'] = my_techniques
|
||||
|
||||
json_string = simplejson.dumps(layer).replace('}, ', '},\n')
|
||||
output_filename = 'output/data_sources_' + normalize_name_to_filename(name) + '.json'
|
||||
with open(output_filename, 'w') as f:
|
||||
f.write(json_string)
|
||||
print("File written: " + output_filename)
|
||||
|
||||
|
||||
def plot_data_sources_graph(filename):
|
||||
"""
|
||||
Generates a line graph which shows the improvements on numbers of data sources through time.
|
||||
:param filename: the filename of the yaml file containing the data sources administration
|
||||
:return:
|
||||
"""
|
||||
my_data_sources, name, platform, exceptions = _load_data_sources(filename)
|
||||
|
||||
graph_values = []
|
||||
for t in my_data_sources.values():
|
||||
if t['date_connected']:
|
||||
yyyymm = t['date_connected'].strftime('%Y-%m')
|
||||
graph_values.append({'date': yyyymm, 'count': 1})
|
||||
|
||||
import pandas as pd
|
||||
df = pd.DataFrame(graph_values).groupby('date', as_index=False)[['count']].sum()
|
||||
df['cumcount'] = df.ix[::1, 'count'].cumsum()[::1]
|
||||
|
||||
output_filename = 'output/graph_data_sources.html'
|
||||
|
||||
import plotly
|
||||
import plotly.graph_objs as go
|
||||
plotly.offline.plot(
|
||||
{'data': [go.Scatter(x=df['date'], y=df['cumcount'])],
|
||||
'layout': go.Layout(title="# of data sources for " + name)},
|
||||
filename=output_filename, auto_open=False
|
||||
)
|
||||
print("File written: " + output_filename)
|
||||
|
||||
|
||||
def export_data_source_list_to_excel(filename):
|
||||
"""
|
||||
Makes an overview of all MITRE ATT&CK data sources (via techniques) and lists which data sources are present
|
||||
in the yaml administration including all properties and data quality score.
|
||||
:param filename: the filename of the yaml file containing the data sources administration
|
||||
:return:
|
||||
"""
|
||||
my_data_sources, name, platform, exceptions = _load_data_sources(filename, filter_empty_scores=False)
|
||||
|
||||
excel_filename = 'output/data_sources.xlsx'
|
||||
workbook = xlsxwriter.Workbook(excel_filename)
|
||||
worksheet = workbook.add_worksheet('Data sources')
|
||||
|
||||
# Formatting:
|
||||
format_bold_left = workbook.add_format({'align': 'left', 'bold': True})
|
||||
format_title = workbook.add_format({'align': 'left', 'bold': True, 'font_size': '14'})
|
||||
format_center = workbook.add_format({'align': 'center'})
|
||||
|
||||
# Title
|
||||
worksheet.write(0, 0, 'Data sources for ' + name, format_title)
|
||||
|
||||
# Header columns
|
||||
worksheet.write(2, 0, 'Data source name', format_bold_left)
|
||||
worksheet.write(2, 1, 'Date registered', format_bold_left)
|
||||
worksheet.write(2, 2, 'Date connected', format_bold_left)
|
||||
worksheet.write(2, 3, 'Products', format_bold_left)
|
||||
worksheet.write(2, 4, 'Comment', format_bold_left)
|
||||
worksheet.write(2, 5, 'Available for data analytics', format_bold_left)
|
||||
worksheet.write(2, 6, 'DQ: device completeness', format_bold_left)
|
||||
worksheet.write(2, 7, 'DQ: data field completeness', format_bold_left)
|
||||
worksheet.write(2, 8, 'DQ: timeliness', format_bold_left)
|
||||
worksheet.write(2, 9, 'DQ: consistency', format_bold_left)
|
||||
worksheet.write(2, 10, 'DQ: retention', format_bold_left)
|
||||
worksheet.write(2, 11, 'DQ: score', format_bold_left)
|
||||
|
||||
worksheet.set_column(0, 0, 35)
|
||||
worksheet.set_column(1, 2, 15)
|
||||
worksheet.set_column(3, 4, 35)
|
||||
worksheet.set_column(5, 5, 24)
|
||||
worksheet.set_column(6, 7, 25)
|
||||
worksheet.set_column(8, 10, 15)
|
||||
worksheet.set_column(11, 11, 10)
|
||||
|
||||
# Putting the data sources data:
|
||||
y = 3
|
||||
for d in get_all_mitre_data_sources():
|
||||
worksheet.write(y, 0, d)
|
||||
if d in my_data_sources.keys():
|
||||
ds = my_data_sources[d]
|
||||
worksheet.write(y, 1, str(ds['date_registered']).replace('None', ''))
|
||||
worksheet.write(y, 2, str(ds['date_connected']).replace('None', ''))
|
||||
worksheet.write(y, 3, ', '.join(ds['products']).replace('None', ''))
|
||||
worksheet.write(y, 4, str(ds['comment']) if ds['comment'] else '')
|
||||
worksheet.write(y, 5, str(ds['available_for_data_analytics']))
|
||||
worksheet.write(y, 6, ds['data_quality']['device_completeness'], format_center)
|
||||
worksheet.write(y, 7, ds['data_quality']['data_field_completeness'], format_center)
|
||||
worksheet.write(y, 8, ds['data_quality']['timeliness'], format_center)
|
||||
worksheet.write(y, 9, ds['data_quality']['consistency'], format_center)
|
||||
worksheet.write(y, 10, ds['data_quality']['retention'], format_center)
|
||||
|
||||
score = 0
|
||||
score_count = 0
|
||||
for s in ds['data_quality'].values():
|
||||
if s != 0:
|
||||
score_count += 1
|
||||
score += s
|
||||
if score > 0:
|
||||
score = score/score_count
|
||||
|
||||
worksheet.write(y, 11, score, format_center)
|
||||
y += 1
|
||||
|
||||
worksheet.autofilter(2, 0, 2, 11)
|
||||
worksheet.freeze_panes(3, 0)
|
||||
try:
|
||||
workbook.close()
|
||||
print("File written: " + excel_filename)
|
||||
except Exception as e:
|
||||
print('[!] Error while writing Excel file: %s' % str(e))
|
||||
|
||||
|
||||
def _load_data_sources(filename, filter_empty_scores=True):
|
||||
"""
|
||||
Loads the data sources (including all properties) from the given yaml file.
|
||||
:param filename: the filename of the yaml file containing the data sources administration
|
||||
:return: dictionaty with data sources, name, platform and exceptions list.
|
||||
"""
|
||||
my_data_sources = {}
|
||||
with open(filename, 'r') as yaml_file:
|
||||
yaml_content = yaml.load(yaml_file, Loader=yaml.FullLoader)
|
||||
for d in yaml_content['data_sources']:
|
||||
dq = d['data_quality']
|
||||
if not filter_empty_scores:
|
||||
my_data_sources[d['data_source_name']] = d
|
||||
elif dq['device_completeness'] > 0 and dq['data_field_completeness'] > 0 and dq['timeliness'] > 0 and dq['consistency'] > 0:
|
||||
my_data_sources[d['data_source_name']] = d
|
||||
name = yaml_content['name']
|
||||
platform = yaml_content['platform']
|
||||
exceptions = [t['technique_id'] for t in yaml_content['exceptions']]
|
||||
return my_data_sources, name, platform, exceptions
|
||||
|
||||
|
||||
def _map_and_colorize_techniques(my_ds, exceptions):
|
||||
"""
|
||||
Determine the color of the techniques based on how many data sources are available per technique.
|
||||
:param my_ds: the configured data sources
|
||||
:return: a dictionary with techniques that can be used in the layer's output file
|
||||
"""
|
||||
techniques = load_attack_data(DATATYPE_ALL_TECH)
|
||||
technique_colors = {}
|
||||
|
||||
# Color the techniques based on how many data sources are available.
|
||||
for t in techniques:
|
||||
if t['data_sources']:
|
||||
total_ds_count = len(t['data_sources'])
|
||||
ds_count = 0
|
||||
for ds in t['data_sources']:
|
||||
if ds in my_ds.keys():
|
||||
ds_count += 1
|
||||
if total_ds_count > 0:
|
||||
result = (float(ds_count) / float(total_ds_count)) * 100
|
||||
color = COLOR_DS_25p if result <= 25 else COLOR_DS_50p if result <= 50 else COLOR_DS_75p \
|
||||
if result <= 75 else COLOR_DS_99p if result <= 99 else COLOR_DS_100p
|
||||
technique_colors[t['technique_id']] = color
|
||||
|
||||
my_techniques = map_techniques_to_data_sources(techniques, my_ds)
|
||||
|
||||
output_techniques = []
|
||||
for t, v in my_techniques.items():
|
||||
if t not in exceptions:
|
||||
for tactic in v['tactics']:
|
||||
d = {}
|
||||
d['techniqueID'] = t
|
||||
# d['score'] = 50
|
||||
d['color'] = technique_colors[t]
|
||||
d['comment'] = ''
|
||||
d['enabled'] = True
|
||||
d['tactic'] = tactic.lower().replace(' ', '-')
|
||||
d['metadata'] = [{'name': '-Available data sources', 'value': ', '.join(v['my_data_sources'])},
|
||||
{'name': '-ATT&CK data sources', 'value': ', '.join(v['data_sources'])},
|
||||
{'name': '-Products', 'value': ', '.join(v['products'])}]
|
||||
|
||||
output_techniques.append(d)
|
||||
|
||||
return output_techniques
|
||||
|
||||
|
||||
def generate_technique_administration_file(filename):
|
||||
"""
|
||||
Generate a technique administration file based on the data source administration yaml file
|
||||
:param filename: the filename of the yaml file containing the data sources administration
|
||||
:return:
|
||||
"""
|
||||
my_data_sources, name, platform, exceptions = _load_data_sources(filename)
|
||||
|
||||
techniques = load_attack_data(DATATYPE_ALL_TECH)
|
||||
|
||||
# This is part of the techniques administration YAML file and is used as a template
|
||||
dict_tech = {'technique_id': '', 'detection': {'date_registered': None, 'date_implemented': None, 'score': -1,
|
||||
'location': [''], 'comment': ''},
|
||||
'visibility': {'score': 0, 'comment': ''}}
|
||||
|
||||
yaml_file = {}
|
||||
yaml_file['version'] = 1.0
|
||||
yaml_file['file_type'] = FILE_TYPE_TECHNIQUE_ADMINISTRATION
|
||||
yaml_file['name'] = name
|
||||
yaml_file['platform'] = platform
|
||||
yaml_file['techniques'] = []
|
||||
|
||||
# Score visibility based on the number of available data sources and the exceptions
|
||||
for t in techniques:
|
||||
if t['matrix'] == 'mitre-attack':
|
||||
platforms_lower = list(map(lambda x: x.lower(), t['platform']))
|
||||
if platform in platforms_lower:
|
||||
if t['data_sources']:
|
||||
total_ds_count = len(t['data_sources'])
|
||||
ds_count = 0
|
||||
for ds in t['data_sources']:
|
||||
if ds in my_data_sources.keys():
|
||||
ds_count += 1
|
||||
if total_ds_count > 0:
|
||||
result = (float(ds_count) / float(total_ds_count)) * 100
|
||||
|
||||
score = 0 if result == 0 else 1 if result <= 49 else 2 if result <= 74 else 3 if result <= 99 else 4
|
||||
else:
|
||||
score = 0
|
||||
|
||||
# Do not add technique if score == 0 or part of the exception list\
|
||||
techniques_upper = list(map(lambda x: x.upper(), exceptions))
|
||||
if score > 0 and t['technique_id'] not in techniques_upper:
|
||||
tech = copy.deepcopy(dict_tech)
|
||||
tech['technique_id'] = t['technique_id']
|
||||
tech['visibility']['score'] = score
|
||||
yaml_file['techniques'].append(tech)
|
||||
|
||||
yaml_string = '%YAML 1.2\n---\n' + yaml.dump(yaml_file, sort_keys=False).replace('null', '')
|
||||
output_filename = 'output/techniques-administration-' + normalize_name_to_filename(name+'-'+platform) + '.yaml'
|
||||
suffix = 1
|
||||
while os.path.exists(output_filename):
|
||||
output_filename = 'output/techniques-administration-' + normalize_name_to_filename(name + '-' + platform) + \
|
||||
'_' + str(suffix) + '.yaml'
|
||||
suffix += 1
|
||||
|
||||
with open(output_filename, 'w') as f:
|
||||
f.write(yaml_string)
|
||||
print("File written: " + output_filename)
|
|
@ -0,0 +1,369 @@
|
|||
import os
|
||||
import pickle
|
||||
from datetime import datetime as dt
|
||||
import yaml
|
||||
# Import for attackcti is because of performance reasons in the function that uses this library.
|
||||
|
||||
APP_NAME = 'Blue ATT&CK'
|
||||
APP_DESC = 'Mapping your blue team to ATT&CK'
|
||||
VERSION = '1.0'
|
||||
|
||||
EXPIRE_TIME = 60*60*24
|
||||
|
||||
DATATYPE_TECH_BY_GROUP = 'mitre_techniques_used_by_group'
|
||||
DATATYPE_ALL_TECH = 'mitre_all_techniques'
|
||||
DATATYPE_ALL_GROUPS = 'mitre_all_groups'
|
||||
DATATYPE_ALL_SOFTWARE = 'mitre_all_software'
|
||||
DATATYPE_TECH_BY_SOFTWARE = 'mitre_techniques_used_by_software'
|
||||
DATATYPE_SOFTWARE_BY_GROUP = 'mitre_software_used_by_group'
|
||||
|
||||
# Group colors
|
||||
COLOR_GROUP_OVERLAY_MATCH = '#f9a825' # orange
|
||||
COLOR_GROUP_OVERLAY_NO_MATCH = '#ffee58' # yellow
|
||||
COLOR_SOFTWARE = '#0d47a1 ' # dark blue
|
||||
COLOR_GROUP_AND_SOFTWARE = '#64b5f6 ' # light blue
|
||||
COLOR_GRADIENT_MIN = '#ffcece' # light red
|
||||
COLOR_GRADIENT_MAX = '#ff0000' # red
|
||||
COLOR_TACTIC_ROW_BACKGRND = '#dddddd' # light grey
|
||||
COLOR_GROUP_OVERLAY_ONLY_DETECTION = '#8BC34A' # green
|
||||
COLOR_GROUP_OVERLAY_ONLY_VISIBILITY = '#1976D2' # blue
|
||||
|
||||
# data source colors (purple range)
|
||||
COLOR_DS_25p = '#E1BEE7'
|
||||
COLOR_DS_50p = '#CE93D8'
|
||||
COLOR_DS_75p = '#AB47BC'
|
||||
COLOR_DS_99p = '#7B1FA2'
|
||||
COLOR_DS_100p = '#4A148C'
|
||||
|
||||
# data source colors HAPPY (green range)
|
||||
COLOR_DS_25p_HAPPY = '#DCEDC8'
|
||||
COLOR_DS_50p_HAPPY = '#AED581'
|
||||
COLOR_DS_75p_HAPPY = '#8BC34A'
|
||||
COLOR_DS_99p_HAPPY = '#689F38'
|
||||
COLOR_DS_100p_HAPPY = '#33691E'
|
||||
|
||||
# Detection colors (green range)
|
||||
COLOR_D_0 = '#64B5F6' # Blue: Forensics/Context
|
||||
COLOR_D_1 = '#DCEDC8'
|
||||
COLOR_D_2 = '#AED581'
|
||||
COLOR_D_3 = '#8BC34A'
|
||||
COLOR_D_4 = '#689F38'
|
||||
COLOR_D_5 = '#33691E'
|
||||
|
||||
# Visibility colors (blue range)
|
||||
COLOR_V_1 = '#BBDEFB'
|
||||
COLOR_V_2 = '#64B5F6'
|
||||
COLOR_V_3 = '#1976D2'
|
||||
COLOR_V_4 = '#0D47A1'
|
||||
|
||||
# Detection and visibility overlay color:
|
||||
COLOR_OVERLAY_VISIBILITY = COLOR_V_3
|
||||
COLOR_OVERLAY_DETECTION = COLOR_D_3
|
||||
COLOR_OVERLAY_BOTH = COLOR_GROUP_OVERLAY_MATCH
|
||||
|
||||
FILE_TYPE_DATA_SOURCE_ADMINISTRATION = 'data-source-administration'
|
||||
FILE_TYPE_TECHNIQUE_ADMINISTRATION = 'technique-administration'
|
||||
FILE_TYPE_GROUP_ADMINISTRATION = 'group-administration'
|
||||
|
||||
|
||||
def save_attack_data(data, path):
|
||||
"""
|
||||
Save ATT&CK data to disk for the purpose of caching.
|
||||
:param data: the MITRE ATT&CK data to save
|
||||
:param path: file path to write to, including filename
|
||||
:return:
|
||||
"""
|
||||
|
||||
if not os.path.exists('cache/'):
|
||||
os.mkdir('cache/')
|
||||
with open(path, 'wb') as f:
|
||||
pickle.dump([data, dt.now()], f)
|
||||
|
||||
|
||||
def load_attack_data(data_type):
|
||||
"""
|
||||
Load the cached ATT&CK data from disk, if not expired (data file on disk is older then EXPIRE_TIME seconds).
|
||||
:param data_type: the desired data type, see DATATYPE_XX constants.
|
||||
:return: MITRE ATT&CK data object
|
||||
"""
|
||||
if os.path.exists("cache/" + data_type):
|
||||
with open("cache/" + data_type, 'rb') as f:
|
||||
cached = pickle.load(f)
|
||||
write_time = cached[1]
|
||||
if not (dt.now() - write_time).total_seconds() >= EXPIRE_TIME:
|
||||
return cached[0]
|
||||
|
||||
from attackcti import attack_client
|
||||
mitre = attack_client()
|
||||
|
||||
json_data = None
|
||||
if data_type == DATATYPE_TECH_BY_GROUP:
|
||||
json_data = mitre.get_techniques_used_by_group()
|
||||
elif data_type == DATATYPE_ALL_TECH:
|
||||
json_data = mitre.get_all_techniques()
|
||||
elif data_type == DATATYPE_ALL_GROUPS:
|
||||
json_data = mitre.get_all_groups()
|
||||
elif data_type == DATATYPE_ALL_SOFTWARE:
|
||||
json_data = mitre.get_all_software()
|
||||
elif data_type == DATATYPE_TECH_BY_SOFTWARE:
|
||||
json_data = mitre.get_techniques_used_by_software()
|
||||
elif data_type == DATATYPE_SOFTWARE_BY_GROUP:
|
||||
json_data = mitre.get_software_used_by_group()
|
||||
|
||||
save_attack_data(json_data, "cache/" + data_type)
|
||||
|
||||
return json_data
|
||||
|
||||
|
||||
def _get_base_template(name, description, stage, platform, sorting):
|
||||
"""
|
||||
Prepares a base template for the json layer file that can be loaded into the MITRE ATT&CK Navigator.
|
||||
More information on the version 2.1 layer format:
|
||||
https://github.com/mitre/attack-navigator/blob/master/layers/LAYERFORMATv2_1.md
|
||||
:param name: name
|
||||
:param description: description
|
||||
:param stage: stage (act | prepare)
|
||||
:param platform: platform
|
||||
:param sorting: sorting
|
||||
:return: layer template dictionary
|
||||
"""
|
||||
layer = {}
|
||||
layer['name'] = name
|
||||
layer['version'] = '2.1'
|
||||
layer['domain'] = 'mitre-enterprise'
|
||||
layer['description'] = description
|
||||
|
||||
if platform == 'all':
|
||||
platform = ['windows', 'linux', 'mac']
|
||||
else:
|
||||
platform = [platform.lower()]
|
||||
|
||||
if stage == 'attack':
|
||||
layer['filters'] = {'stages': ['act'], 'platforms': platform}
|
||||
else:
|
||||
layer['filters'] = {'stages': ['prepare'], 'platforms': platform}
|
||||
|
||||
layer['sorting'] = sorting
|
||||
layer['viewMode'] = 0
|
||||
layer['hideDisable'] = False
|
||||
layer['techniques'] = []
|
||||
|
||||
layer['showTacticRowBackground'] = False
|
||||
layer['tacticRowBackground'] = COLOR_TACTIC_ROW_BACKGRND
|
||||
layer['selectTechniquesAcrossTactics'] = True
|
||||
return layer
|
||||
|
||||
|
||||
def get_layer_template_groups(name, max_score, description, stage, platform):
|
||||
"""
|
||||
Prepares a base template for the json layer file that can be loaded into the MITRE ATT&CK Navigator.
|
||||
More information on the version 2.1 layer format:
|
||||
https://github.com/mitre/attack-navigator/blob/master/layers/LAYERFORMATv2_1.md
|
||||
:param name: name
|
||||
:param max_score: max_score
|
||||
:param description: description
|
||||
:param stage: stage (act | prepare)
|
||||
:param platform: platform
|
||||
:return: layer template dictionary
|
||||
"""
|
||||
layer = _get_base_template(name, description, stage, platform, 3)
|
||||
layer['gradient'] = {'colors': [COLOR_GRADIENT_MIN, COLOR_GRADIENT_MAX], 'minValue': 0, 'maxValue': max_score}
|
||||
layer['legendItems'] = \
|
||||
[
|
||||
{'label': 'Tech. ref. for ' + str(1) + ' group', 'color': COLOR_GRADIENT_MIN},
|
||||
{'label': 'Tech. ref. for ' + str(max_score) + ' groups', 'color': COLOR_GRADIENT_MAX},
|
||||
{'label': 'Groups overlay: tech. in group + overlay', 'color': COLOR_GROUP_OVERLAY_MATCH},
|
||||
{'label': 'Groups overlay: tech. in overlay', 'color': COLOR_GROUP_OVERLAY_NO_MATCH},
|
||||
{'label': 'Src. of tech. is only software', 'color': COLOR_SOFTWARE},
|
||||
{'label': 'Src. of tech. is group(s)/overlay + software', 'color': COLOR_GROUP_AND_SOFTWARE}
|
||||
]
|
||||
return layer
|
||||
|
||||
|
||||
def get_layer_template_detections(name, description, stage, platform):
|
||||
"""
|
||||
Prepares a base template for the json layer file that can be loaded into the MITRE ATT&CK Navigator.
|
||||
More information on the version 2.1 layer format:
|
||||
https://github.com/mitre/attack-navigator/blob/master/layers/LAYERFORMATv2_1.md
|
||||
:param name: name
|
||||
:param description: description
|
||||
:param stage: stage (act | prepare)
|
||||
:param platform: platform
|
||||
:return: layer template dictionary
|
||||
"""
|
||||
layer = _get_base_template(name, description, stage, platform, 0)
|
||||
layer['gradient'] = {'colors': ['#ff6666', '#ffe766', '#8ec843'], 'minValue': 0, 'maxValue': 100}
|
||||
layer['legendItems'] = \
|
||||
[
|
||||
{'label': 'Detection score 0: Forensics/Context', 'color': COLOR_D_0},
|
||||
{'label': 'Detection score 1: Basic', 'color': COLOR_D_1},
|
||||
{'label': 'Detection score 2: Fair', 'color': COLOR_D_2},
|
||||
{'label': 'Detection score 3: Good', 'color': COLOR_D_3},
|
||||
{'label': 'Detection score 4: Very good', 'color': COLOR_D_4},
|
||||
{'label': 'Detection score 5: Excellent', 'color': COLOR_D_5}
|
||||
]
|
||||
return layer
|
||||
|
||||
|
||||
def get_layer_template_data_sources(name, description, stage, platform):
|
||||
"""
|
||||
Prepares a base template for the json layer file that can be loaded into the MITRE ATT&CK Navigator.
|
||||
More information on the version 2.1 layer format:
|
||||
https://github.com/mitre/attack-navigator/blob/master/layers/LAYERFORMATv2_1.md
|
||||
:param name: name
|
||||
:param description: description
|
||||
:param stage: stage (act | prepare)
|
||||
:param platform: platform
|
||||
:return: layer template dictionary
|
||||
"""
|
||||
layer = _get_base_template(name, description, stage, platform, 0)
|
||||
layer['gradient'] = {'colors': ['#ff6666', '#ffe766', '#8ec843'], 'minValue': 0, 'maxValue': 100}
|
||||
layer['legendItems'] = \
|
||||
[
|
||||
{'label': '1-25% of data sources available', 'color': COLOR_DS_25p},
|
||||
{'label': '26-50% of data sources available', 'color': COLOR_DS_50p},
|
||||
{'label': '51-75% of data sources available', 'color': COLOR_DS_75p},
|
||||
{'label': '76-99% of data sources available', 'color': COLOR_DS_99p},
|
||||
{'label': '100% of data sources available', 'color': COLOR_DS_100p}
|
||||
]
|
||||
return layer
|
||||
|
||||
|
||||
def get_layer_template_visibility(name, description, stage, platform):
|
||||
"""
|
||||
Prepares a base template for the json layer file that can be loaded into the MITRE ATT&CK Navigator.
|
||||
More information on the version 2.1 layer format:
|
||||
https://github.com/mitre/attack-navigator/blob/master/layers/LAYERFORMATv2_1.md
|
||||
:param name: name
|
||||
:param description: description
|
||||
:param stage: stage (act | prepare)
|
||||
:param platform: platform
|
||||
:return: layer template dictionary
|
||||
"""
|
||||
layer = _get_base_template(name, description, stage, platform, 0)
|
||||
layer['gradient'] = {'colors': ['#ff6666', '#ffe766', '#8ec843'], 'minValue': 0, 'maxValue': 100}
|
||||
layer['legendItems'] = \
|
||||
[
|
||||
{'label': 'Visibility score 1: Minimal', 'color': COLOR_V_1},
|
||||
{'label': 'Visibility score 2: Medium', 'color': COLOR_V_2},
|
||||
{'label': 'Visibility score 3: Good', 'color': COLOR_V_3},
|
||||
{'label': 'Visibility score 4: Excellent', 'color': COLOR_V_4}
|
||||
]
|
||||
return layer
|
||||
|
||||
|
||||
def get_layer_template_layered(name, description, stage, platform):
|
||||
"""
|
||||
Prepares a base template for the json layer file that can be loaded into the MITRE ATT&CK Navigator.
|
||||
More information on the version 2.1 layer format:
|
||||
https://github.com/mitre/attack-navigator/blob/master/layers/LAYERFORMATv2_1.md
|
||||
:param name: name
|
||||
:param description: description
|
||||
:param stage: stage (act | prepare)
|
||||
:param platform: platform
|
||||
:return: layer template dictionary
|
||||
"""
|
||||
layer = _get_base_template(name, description, stage, platform, 0)
|
||||
layer['gradient'] = {'colors': ['#ff6666', '#ffe766', '#8ec843'], 'minValue': 0, 'maxValue': 100}
|
||||
layer['legendItems'] = \
|
||||
[
|
||||
{'label': 'Visibility', 'color': COLOR_OVERLAY_VISIBILITY},
|
||||
{'label': 'Detection', 'color': COLOR_OVERLAY_DETECTION},
|
||||
{'label': 'Visibility and detection', 'color': COLOR_OVERLAY_BOTH}
|
||||
]
|
||||
return layer
|
||||
|
||||
|
||||
def get_technique(techniques, technique_id):
|
||||
"""
|
||||
Generic function to lookup a specific technique_id in a list of dictionaries with techniques.
|
||||
:param techniques: list with all techniques
|
||||
:param technique_id: technique_id to look for
|
||||
:return: the technique you're searching for. None if not found.
|
||||
"""
|
||||
for t in techniques:
|
||||
if technique_id == t['technique_id']:
|
||||
return t
|
||||
return None
|
||||
|
||||
|
||||
def normalize_name_to_filename(name):
|
||||
"""
|
||||
Normalize the input filename to a lowercase filename and replace spaces with dashes.
|
||||
:param name: input filename
|
||||
:return: normalized filename
|
||||
"""
|
||||
return name.lower().replace(' ', '-')
|
||||
|
||||
|
||||
def map_techniques_to_data_sources(techniques, my_data_sources):
|
||||
"""
|
||||
This function maps the MITRE ATT&CK techniques to your data sources.
|
||||
:param techniques: list with all MITRE ATT&CK techniques
|
||||
:param my_data_sources: your configured data sources
|
||||
:return: a dictionary containing techniques that can be used in the layer output file.
|
||||
"""
|
||||
my_techniques = {}
|
||||
for i_ds in my_data_sources.keys():
|
||||
# Loop through all techniques, to find techniques using that data source:
|
||||
for t in techniques:
|
||||
# If your data source is in the list of data sources for this technique AND if the
|
||||
# technique isn't added yet (by an other data source):
|
||||
if t['data_sources'] and i_ds in t['data_sources'] and t['technique_id'] not in my_techniques.keys():
|
||||
my_techniques[t['technique_id']] = {}
|
||||
my_techniques[t['technique_id']]['my_data_sources'] = [i_ds, ]
|
||||
my_techniques[t['technique_id']]['data_sources'] = t['data_sources']
|
||||
my_techniques[t['technique_id']]['tactics'] = t['tactic']
|
||||
my_techniques[t['technique_id']]['products'] = my_data_sources[i_ds]['products']
|
||||
elif t['data_sources'] and i_ds in t['data_sources'] and t['technique_id'] in my_techniques.keys():
|
||||
my_techniques[t['technique_id']]['my_data_sources'].append(i_ds)
|
||||
return my_techniques
|
||||
|
||||
|
||||
def get_all_mitre_data_sources():
|
||||
"""
|
||||
Gets all the data sources from the techniques and make a unique sorted list of it.
|
||||
:return: a sorted list with all data sources
|
||||
"""
|
||||
techniques = load_attack_data(DATATYPE_ALL_TECH)
|
||||
data_sources = set()
|
||||
for t in techniques:
|
||||
if t['data_sources']:
|
||||
for ds in t['data_sources']:
|
||||
if ds not in data_sources:
|
||||
data_sources.add(ds)
|
||||
return sorted(data_sources)
|
||||
|
||||
|
||||
def check_file_type(filename, file_type=None):
|
||||
"""
|
||||
Check if the provided YAML file has the key 'file_type' and possible if that key matches a specific value.
|
||||
:param filename: path to a YAML file
|
||||
:param file_type: value to check against the 'file_type' key in the YAML file
|
||||
:return: the file_type if present, else None is returned.
|
||||
"""
|
||||
if not os.path.exists(filename):
|
||||
print('[!] File: \'' + filename + '\' does not exist')
|
||||
return None
|
||||
with open(filename, 'r') as yaml_file:
|
||||
try:
|
||||
yaml_content = yaml.load(yaml_file, Loader=yaml.FullLoader)
|
||||
except:
|
||||
print('[!] File: \'' + filename + '\' is not a valid YAML file.')
|
||||
return None
|
||||
|
||||
if not hasattr(yaml_content, 'keys'):
|
||||
print('[!] File: \'' + filename + '\' is not a valid YAML file.')
|
||||
return None
|
||||
|
||||
if 'file_type' not in yaml_content.keys():
|
||||
print('[!] File: \'' + filename + '\' does not contain a file_type key.')
|
||||
return None
|
||||
elif file_type:
|
||||
if file_type != yaml_content['file_type']:
|
||||
print('[!] File: \'' + filename + '\' is not a file type of: \'' + file_type + '\'')
|
||||
return None
|
||||
else:
|
||||
return yaml_content['file_type']
|
||||
else:
|
||||
return yaml_content['file_type']
|
|
@ -0,0 +1,571 @@
|
|||
import simplejson
|
||||
from generic import *
|
||||
from technique_mapping import _load_detections
|
||||
|
||||
CG_GROUPS = {}
|
||||
|
||||
|
||||
def is_in_group(json_groups, argument_groups):
|
||||
"""
|
||||
Check if the two dicts (json_groups and argument_groups) have any groups in common based on their names/aliases.
|
||||
:param json_groups: group aliases from ATT&CK
|
||||
:param argument_groups: group names provided via the command line by the user
|
||||
:return: true or false
|
||||
"""
|
||||
json_groups = list(map(lambda x: x.lower(), json_groups))
|
||||
|
||||
for group in argument_groups:
|
||||
if group in json_groups:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_group_found(groups_found, argument_groups):
|
||||
"""
|
||||
Check if a group that has been provided using '-g/--groups'/'-o/--overlay' is present within MITRE ATT&CK.
|
||||
:param groups_found: groups that are found in the ATT&CK data
|
||||
:param argument_groups: groups provided via the command line by the user
|
||||
:return: returns boolean that incidates if the group is found
|
||||
"""
|
||||
groups_json = load_attack_data(DATATYPE_ALL_GROUPS)
|
||||
|
||||
for group_arg in argument_groups:
|
||||
if group_arg == 'all': # this one will be ignored as it does not make any sense for this function
|
||||
return True
|
||||
|
||||
group_id = None
|
||||
|
||||
for group in groups_json: # is the group provided via the command line known in ATT&CK?
|
||||
if group['group_aliases']:
|
||||
group_aliases_lower = list(map(lambda x: x.lower(), group['group_aliases']))
|
||||
if group_arg in group_aliases_lower or group_arg == group['group_id'].lower():
|
||||
group_id = group['group_id']
|
||||
|
||||
if not group_id: # the group that has been provided through the command line cannot be found in ATT&CK
|
||||
print('[!] Unknown group: ' + group_arg)
|
||||
return False
|
||||
elif group_id not in groups_found: # group not present in filtered data sate (i.e. platform and stage)
|
||||
print('[!] Group not part of the data set: ' + group_arg)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def get_software_techniques(groups, stage, platform):
|
||||
"""
|
||||
Get all techniques (in a dict) from the provided list of groups in relation to the software these groups use,
|
||||
and hence techniques they support.
|
||||
:param groups: ATT&CK groups
|
||||
:param stage: attack or pre-attack
|
||||
:param platform: the applicable platform
|
||||
:return: dictionary with info on groups
|
||||
"""
|
||||
# { group_id: {group_name: NAME, techniques: set{id, ...} } }
|
||||
groups_dict = {}
|
||||
|
||||
tech_by_software_json = load_attack_data(DATATYPE_TECH_BY_SOFTWARE)
|
||||
|
||||
# { software_id: [technique, ...] }
|
||||
software_dict = {}
|
||||
for tech in tech_by_software_json:
|
||||
if tech['software_id'] not in software_dict:
|
||||
software_dict[tech['software_id']] = set([tech['technique_id']])
|
||||
else:
|
||||
software_dict[tech['software_id']].add(tech['technique_id'])
|
||||
|
||||
# groups is a YAML file
|
||||
if os.path.isfile(str(groups)):
|
||||
with open(groups, 'r') as yaml_file:
|
||||
config = yaml.load(yaml_file, Loader=yaml.FullLoader)
|
||||
|
||||
for group in config['groups']:
|
||||
if group['enabled']:
|
||||
group_id = get_group_id(group['group_name'], group['campaign'])
|
||||
groups_dict[group_id] = dict()
|
||||
|
||||
groups_dict[group_id]['group_name'] = group['group_name']
|
||||
groups_dict[group_id]['techniques'] = set()
|
||||
groups_dict[group_id]['campaign'] = group['campaign']
|
||||
groups_dict[group_id]['software'] = group['software_id']
|
||||
|
||||
if group['software_id']:
|
||||
for soft_id in group['software_id']:
|
||||
try:
|
||||
groups_dict[group_id]['techniques'].update(software_dict[soft_id])
|
||||
except KeyError:
|
||||
print('[!] unknown ATT&CK software ID: ' + soft_id)
|
||||
|
||||
# groups are provided as arguments via the command line
|
||||
else:
|
||||
software_by_group_json = load_attack_data(DATATYPE_SOFTWARE_BY_GROUP)
|
||||
|
||||
for s in software_by_group_json:
|
||||
# group matches the: matrix/stage, platform and the group(s) we are interested in
|
||||
if s['software_platform']: # their is some software that do not have a platform, skip those
|
||||
if s['matrix'] == 'mitre-'+stage and (platform in s['software_platform'] or platform == 'all') and \
|
||||
(groups[0] == 'all' or s['group_id'].lower() in groups or is_in_group(s['group_aliases'], groups)):
|
||||
if s['group_id'] not in groups_dict:
|
||||
groups_dict[s['group_id']] = {'group_name': s['group']}
|
||||
groups_dict[s['group_id']]['techniques'] = set()
|
||||
groups_dict[s['group_id']]['techniques'].update(software_dict[s['software_id']])
|
||||
|
||||
return groups_dict
|
||||
|
||||
|
||||
def get_group_id(group_name, campaign):
|
||||
# CG_GROUPS = { group_name+campaign: id } }
|
||||
"""
|
||||
Generate a custom group id.
|
||||
:param group_name: group name as used within the YAML file
|
||||
:param campaign: campaign as used within the YAML file
|
||||
:return: custom group identifier string (e.g. CG0001)
|
||||
"""
|
||||
global CG_GROUPS
|
||||
|
||||
if not CG_GROUPS:
|
||||
new_id = 1
|
||||
elif group_name + campaign not in CG_GROUPS:
|
||||
new_id = len(CG_GROUPS) + 1
|
||||
|
||||
if group_name + campaign not in CG_GROUPS:
|
||||
length = len(str(new_id))
|
||||
if length > 9:
|
||||
cg_id = 'CG00' + str(new_id)
|
||||
elif length > 99:
|
||||
cg_id = 'CG0' + str(new_id)
|
||||
elif length > 999:
|
||||
cg_id = 'CG' + str(new_id)
|
||||
else:
|
||||
cg_id = 'CG000' + str(new_id)
|
||||
|
||||
CG_GROUPS[group_name + campaign] = cg_id
|
||||
|
||||
return CG_GROUPS[group_name + campaign]
|
||||
|
||||
|
||||
def get_group_techniques(groups, stage, platform, file_type):
|
||||
"""
|
||||
Get all techniques (in a dict) from the provided list of groups
|
||||
:param groups: group ID, group name/alias or a YAML file with group(s) data
|
||||
:param stage: attack or pre-attack
|
||||
:param platform: all, Linux, macOS, Windows
|
||||
:param file_type: the file type of the YAML file as present in the key 'file_type'
|
||||
:return: returns dictionary with all techniques from the provided list of groups or -1 when group is not found
|
||||
"""
|
||||
# { group_id: {group_name: NAME, techniques: set{id, ...} } }
|
||||
groups_dict = {}
|
||||
groups_found = set()
|
||||
|
||||
# groups is a YAML file
|
||||
if file_type == FILE_TYPE_GROUP_ADMINISTRATION:
|
||||
with open(groups, 'r') as yaml_file:
|
||||
config = yaml.load(yaml_file, Loader=yaml.FullLoader)
|
||||
|
||||
for group in config['groups']:
|
||||
if group['enabled']:
|
||||
group_id = get_group_id(group['group_name'], group['campaign'])
|
||||
groups_dict[group_id] = dict()
|
||||
|
||||
groups_dict[group_id]['group_name'] = group['group_name']
|
||||
groups_dict[group_id]['techniques'] = set(group['technique_id'])
|
||||
groups_dict[group_id]['campaign'] = group['campaign']
|
||||
groups_dict[group_id]['software'] = group['software_id']
|
||||
else:
|
||||
# groups are provided as arguments via the command line
|
||||
groups_json = load_attack_data(DATATYPE_TECH_BY_GROUP)
|
||||
|
||||
for e in groups_json:
|
||||
json_platform = e['platform']
|
||||
if not json_platform:
|
||||
# we just set this to an random legit value, because for pre-attack 'platform' is not used
|
||||
json_platform = 'Windows'
|
||||
|
||||
# group matches the: matrix/stage, platform and the group(s) we are interested in
|
||||
if e['matrix'] == 'mitre-'+stage and (platform in json_platform or platform == 'all') and \
|
||||
(groups[0] == 'all' or e['group_id'].lower() in groups or is_in_group(e['group_aliases'], groups)):
|
||||
if e['group_id'] not in groups_dict:
|
||||
groups_found.add(e['group_id'])
|
||||
groups_dict[e['group_id']] = {'group_name': e['group']}
|
||||
groups_dict[e['group_id']]['techniques'] = set()
|
||||
|
||||
groups_dict[e['group_id']]['techniques'].add(e['technique_id'])
|
||||
|
||||
# do not call 'is_group_found' when groups is a YAML file
|
||||
# (this could contain groups that do not exists within ATT&CK)
|
||||
if not os.path.isfile(str(groups)):
|
||||
found = is_group_found(groups_found, groups)
|
||||
if not found:
|
||||
return -1
|
||||
|
||||
return groups_dict
|
||||
|
||||
|
||||
def get_detection_techniques(filename):
|
||||
"""
|
||||
Get all techniques (in a dict) from the detection administration
|
||||
:param filename: path to the YAML technique administration file
|
||||
:return: dictionary
|
||||
"""
|
||||
# { group_id: {group_name: NAME, techniques: set{id, ...} } }
|
||||
groups_dict = {}
|
||||
|
||||
detection_techniques, name, platform = _load_detections(filename)
|
||||
|
||||
group_id = 'DETECTION'
|
||||
groups_dict[group_id] = {}
|
||||
groups_dict[group_id]['group_name'] = 'Detection'
|
||||
groups_dict[group_id]['techniques'] = set()
|
||||
for t, v in detection_techniques.items():
|
||||
if 'detection' in v.keys() and v['detection']['score'] > 0:
|
||||
groups_dict[group_id]['techniques'].add(t)
|
||||
|
||||
return groups_dict
|
||||
|
||||
|
||||
def get_visibility_techniques(filename):
|
||||
"""
|
||||
Get all techniques (in a dict) from the detections administration
|
||||
:param filename: path to the YAML technique administration file
|
||||
:return: dictionary
|
||||
"""
|
||||
# { group_id: {group_name: NAME, techniques: set{id, ...} } }
|
||||
groups_dict = {}
|
||||
|
||||
detection_techniques, name, platform = _load_detections(filename)
|
||||
|
||||
group_id = 'VISIBILITY'
|
||||
groups_dict[group_id] = {}
|
||||
groups_dict[group_id]['group_name'] = 'Visibility'
|
||||
groups_dict[group_id]['techniques'] = set()
|
||||
for t, v in detection_techniques.items():
|
||||
if 'visibility' in v.keys() and v['visibility']['score'] > 0:
|
||||
groups_dict[group_id]['techniques'].add(t)
|
||||
|
||||
return groups_dict
|
||||
|
||||
|
||||
def get_technique_count(groups, groups_overlay, groups_software):
|
||||
"""
|
||||
Create a dict with all involved techniques and their relevant count/score
|
||||
:param groups: a dict with data on groups
|
||||
:param groups_overlay: a dict with data on the groups to overlay
|
||||
:param groups_software: a dict with with data on which techniques are used within related software
|
||||
:return: dictionary
|
||||
"""
|
||||
# { technique_id: {count: ..., groups: set{} }
|
||||
techniques_dict = {}
|
||||
|
||||
for group, v in groups.items():
|
||||
for tech in v['techniques']:
|
||||
if tech not in techniques_dict:
|
||||
techniques_dict[tech] = dict()
|
||||
techniques_dict[tech]['groups'] = set()
|
||||
techniques_dict[tech]['count'] = 1
|
||||
else:
|
||||
techniques_dict[tech]['count'] += 1
|
||||
techniques_dict[tech]['groups'].add(group)
|
||||
|
||||
for group, v in groups_overlay.items():
|
||||
for tech in v['techniques']:
|
||||
if tech not in techniques_dict:
|
||||
techniques_dict[tech] = dict()
|
||||
techniques_dict[tech]['groups'] = set()
|
||||
techniques_dict[tech]['count'] = 1
|
||||
elif group in groups:
|
||||
if tech not in groups[group]['techniques']:
|
||||
techniques_dict[tech]['count'] += 1
|
||||
# Only to this when it was not already counted by being part of 'groups'.
|
||||
# Meaning the group in 'groups_overlay' was also part of 'groups' (match on Group ID) and the
|
||||
# technique was already counted for that group / it is not a new technique for that group coming
|
||||
# from a YAML file
|
||||
else:
|
||||
techniques_dict[tech]['count'] += 1
|
||||
# increase count when the group in the YAML file is a custom group
|
||||
techniques_dict[tech]['groups'].add(group)
|
||||
|
||||
for group, v in groups_software.items():
|
||||
for tech in v['techniques']:
|
||||
if tech not in techniques_dict:
|
||||
techniques_dict[tech] = dict()
|
||||
techniques_dict[tech]['count'] = 0
|
||||
# we will not adjust the scoring for groups_software. We will just set the the score to 0.
|
||||
# This will later be used for the colouring of the heat map.
|
||||
if 'groups' not in techniques_dict[tech]:
|
||||
techniques_dict[tech]['groups'] = set()
|
||||
techniques_dict[tech]['groups'].add(group)
|
||||
|
||||
return techniques_dict
|
||||
|
||||
|
||||
def get_technique_layer(techniques, groups, overlay, groups_software, overlay_file_type, overlay_type):
|
||||
"""
|
||||
Create the technique layer that will be part of the ATT&CK navigator json file
|
||||
:param techniques: involved techniques with count (to be used within the scores)
|
||||
:param groups: a dict with data on groups
|
||||
:param overlay: a dict with data on the groups to overlay
|
||||
:param groups_software: a dict with with data on which techniques are used within related software
|
||||
:param overlay_file_type: the file type of the YAML file as present in the key 'file_type'
|
||||
:param overlay_type: group, visibility or detection
|
||||
:return: dictionary
|
||||
"""
|
||||
techniques_layer = []
|
||||
|
||||
# { technique_id: {count: ..., groups: set{} }
|
||||
# add the technique count/scoring
|
||||
for tech, v in techniques.items():
|
||||
t = dict()
|
||||
t['techniqueID'] = tech
|
||||
t['score'] = v['count']
|
||||
t['metadata'] = []
|
||||
metadata_dict = dict()
|
||||
|
||||
for group, values in groups.items():
|
||||
if tech in values['techniques']: # we do not color this one because that's done using the scoring
|
||||
if 'Groups' not in metadata_dict:
|
||||
metadata_dict['Groups'] = set()
|
||||
metadata_dict['Groups'].add(values['group_name'])
|
||||
|
||||
# this will only be effective when loading a YAML files that has a value for the key 'campaign'
|
||||
if 'Campaign' in values and values['campaign'] is not None:
|
||||
if 'CAMPAIGN' not in metadata_dict:
|
||||
metadata_dict['Campaign'] = set()
|
||||
metadata_dict['Campaign'].add(values['campaign'])
|
||||
|
||||
# change the color and add metadata to make the groups overlay visible
|
||||
for group, values in overlay.items():
|
||||
if tech in values['techniques']:
|
||||
if len(v['groups'].intersection(set(groups.keys()))) > 0:
|
||||
# if the technique is both present in the group (-g/--groups) and the groups overlay (-o/--overlay)
|
||||
t['color'] = COLOR_GROUP_OVERLAY_MATCH
|
||||
else:
|
||||
# the technique is only present in the overlay and not in the provided groups (-g/--groups)
|
||||
if overlay_file_type == FILE_TYPE_TECHNIQUE_ADMINISTRATION:
|
||||
if overlay_type == 'visibility':
|
||||
t['color'] = COLOR_GROUP_OVERLAY_ONLY_VISIBILITY
|
||||
elif overlay_type == 'detection':
|
||||
t['color'] = COLOR_GROUP_OVERLAY_ONLY_DETECTION
|
||||
else:
|
||||
t['color'] = COLOR_GROUP_OVERLAY_NO_MATCH
|
||||
|
||||
if 'Overlay' not in metadata_dict:
|
||||
metadata_dict['Overlay'] = set()
|
||||
metadata_dict['Overlay'].add(values['group_name'])
|
||||
|
||||
# this will only be effective when loading a YAML files that has a value for the key 'campaign'
|
||||
if 'campaign' in values and values['campaign'] is not None:
|
||||
if 'Campaign' not in metadata_dict:
|
||||
metadata_dict['Campaign'] = set()
|
||||
metadata_dict['Campaign'].add(values['campaign'])
|
||||
|
||||
# change the color and add metadata to make the groups software overlay visible
|
||||
for group, values in groups_software.items(): # TODO add support for campaign info in layer metadata
|
||||
if tech in values['techniques']:
|
||||
if t['score'] > 0:
|
||||
t['color'] = COLOR_GROUP_AND_SOFTWARE
|
||||
else:
|
||||
t['color'] = COLOR_SOFTWARE
|
||||
|
||||
if 'Software groups' not in metadata_dict:
|
||||
metadata_dict['Software groups'] = set()
|
||||
metadata_dict['Software groups'].add(values['group_name'])
|
||||
|
||||
# create the metadata based on the dict 'metadata_dict'
|
||||
for metadata, values in metadata_dict.items():
|
||||
tmp_dict = {'name': '-' + metadata, 'value': ', '.join(values)}
|
||||
t['metadata'].append(tmp_dict)
|
||||
|
||||
techniques_layer.append(t)
|
||||
|
||||
return techniques_layer
|
||||
|
||||
|
||||
def get_group_list(groups, file_type):
|
||||
"""
|
||||
Make a list of group names for the involved groups.
|
||||
:param groups: a dict with data on groups
|
||||
:param file_type: the file type of the YAML file as present in the key 'file_type'
|
||||
:return: list
|
||||
"""
|
||||
if file_type == FILE_TYPE_GROUP_ADMINISTRATION:
|
||||
groups_list = []
|
||||
for group, values in groups.items():
|
||||
# if YAML file contains campaign key with a legit value
|
||||
if 'campaign' in values and values['campaign'] is not None:
|
||||
groups_list.append(values['group_name'] + ' (' + values['campaign'] + ')')
|
||||
else:
|
||||
groups_list.append(values['group_name'])
|
||||
|
||||
return groups_list
|
||||
else:
|
||||
return groups
|
||||
|
||||
|
||||
def generate_group_heat_map(groups, overlay, overlay_type, stage, platform, software_groups):
|
||||
"""
|
||||
Calls all functions that are necessary for the generation of the heat map and write a json layer to disk.
|
||||
:param groups: threat actor groups
|
||||
:param overlay: group(s), visibility or detections to overlay (group ID, group name/alias, YAML file with
|
||||
group(s), detections or visibility)
|
||||
:param overlay_type: group, visibility or detection
|
||||
:param stage: attack or pre-attack
|
||||
:param platform: all, Linux, macOS, Windows
|
||||
:param software_groups: specify if techniques from related software should be included.
|
||||
:return: returns nothing when something's wrong
|
||||
"""
|
||||
overlay_dict = {}
|
||||
groups_software_dict = {}
|
||||
|
||||
groups_file_type = None
|
||||
if os.path.isfile(groups):
|
||||
groups_file_type = check_file_type(groups, file_type=FILE_TYPE_GROUP_ADMINISTRATION)
|
||||
if not groups_file_type:
|
||||
return
|
||||
else:
|
||||
# remove whitespaces (leading and trailing), convert to lower case and put in a list
|
||||
groups = groups.split(',')
|
||||
groups = list(map(lambda x: x.strip().lower(), groups))
|
||||
|
||||
overlay_file_type = None
|
||||
if overlay:
|
||||
if os.path.isfile(overlay):
|
||||
expected_file_type = FILE_TYPE_GROUP_ADMINISTRATION if overlay_type == 'group' else FILE_TYPE_TECHNIQUE_ADMINISTRATION if overlay_type in ['visibility', 'detection'] else None
|
||||
overlay_file_type = check_file_type(overlay, expected_file_type)
|
||||
if not overlay_file_type:
|
||||
return
|
||||
else:
|
||||
overlay = overlay.split(',')
|
||||
overlay = list(map(lambda x: x.strip().lower(), overlay))
|
||||
else:
|
||||
overlay = []
|
||||
|
||||
if overlay_file_type == FILE_TYPE_TECHNIQUE_ADMINISTRATION:
|
||||
if overlay_type == 'visibility':
|
||||
overlay_dict = get_visibility_techniques(overlay)
|
||||
elif overlay_type == 'detection':
|
||||
overlay_dict = get_detection_techniques(overlay)
|
||||
elif len(overlay) > 0:
|
||||
overlay_dict = get_group_techniques(overlay, stage, platform, overlay_file_type)
|
||||
if not overlay_dict:
|
||||
return
|
||||
|
||||
groups_dict = get_group_techniques(groups, stage, platform, groups_file_type)
|
||||
if groups_dict == -1:
|
||||
return
|
||||
if len(groups_dict) == 0:
|
||||
print('[!] Empty layer.') # the provided groups dit not result in any techniques
|
||||
return
|
||||
|
||||
# check if we are doing an software group overlay
|
||||
if software_groups and overlay: # TODO add support for campaign info in layer metadata
|
||||
# if a group overlay is provided, get the software techniques for the overlay
|
||||
groups_software_dict = get_software_techniques(overlay, stage, platform)
|
||||
elif software_groups:
|
||||
groups_software_dict = get_software_techniques(groups, stage, platform)
|
||||
|
||||
technique_count = get_technique_count(groups_dict, overlay_dict, groups_software_dict)
|
||||
technique_layer = get_technique_layer(technique_count, groups_dict, overlay_dict, groups_software_dict,
|
||||
overlay_file_type, overlay_type)
|
||||
max_technique_count = max(technique_count.values(), key=lambda v: v['count'])['count']
|
||||
|
||||
# make a list group names for the involved groups.
|
||||
if groups == ['all']:
|
||||
groups_list = ['all']
|
||||
else:
|
||||
groups_list = get_group_list(groups_dict, groups_file_type)
|
||||
overlay_list = get_group_list(overlay_dict, overlay_file_type)
|
||||
|
||||
desc = 'stage: ' + stage + ' | platform: ' + platform + ' | group(s): ' + ', '.join(groups_list) + \
|
||||
' | overlay group(s): ' + ', '.join(overlay_list)
|
||||
|
||||
layer = get_layer_template_groups(stage[0].upper() + stage[1:] + ' ' + platform, max_technique_count, desc, stage, platform)
|
||||
layer['techniques'] = technique_layer
|
||||
|
||||
json_string = simplejson.dumps(layer).replace('}, ', '},\n')
|
||||
|
||||
if overlay:
|
||||
filename = "output/" + stage + '_' + platform.lower() + '_' + '_'.join(groups_list) + '-overlay_' + '_'.join(overlay_list) + '.json'
|
||||
else:
|
||||
filename = "output/" + stage + '_' + platform.lower() + '_' + '_'.join(groups_list) + '.json'
|
||||
with open(filename, 'w') as f: # write layer file to disk
|
||||
f.write(json_string)
|
||||
print('Written layer: ' + filename)
|
||||
|
||||
|
||||
def get_updates(update_type, sort='modified'):
|
||||
"""
|
||||
Print a list of updates for a techniques, groups or software. Sort by modified or creation date.
|
||||
:param update_type: the type of update: techniques, groups or software
|
||||
:param sort: sort the list by modified or creation date
|
||||
:return:
|
||||
"""
|
||||
if update_type[:-1] == 'technique':
|
||||
techniques = load_attack_data(DATATYPE_ALL_TECH)
|
||||
sorted_techniques = sorted(techniques, key=lambda k: k[sort])
|
||||
|
||||
for t in sorted_techniques:
|
||||
print(t['technique_id'] + ' ' + t['technique'])
|
||||
print(' ' * 6 + 'created: ' + t['created'].split(' ')[0])
|
||||
print(' ' * 6 + 'modified: ' + t['modified'][:10])
|
||||
print(' ' * 6 + 'matrix: ' + t['matrix'][6:])
|
||||
if t['tactic']:
|
||||
print(' ' * 6 + 'tactic: ' + ' '.join(t['tactic']))
|
||||
else:
|
||||
print(' ' * 6 + 'tactic: None')
|
||||
print('')
|
||||
|
||||
elif update_type[:-1] == 'group':
|
||||
groups = load_attack_data(DATATYPE_ALL_GROUPS)
|
||||
sorted_groups = sorted(groups, key=lambda k: k[sort])
|
||||
|
||||
for t in sorted_groups:
|
||||
print(t['group_id'] + ' ' + t['group'])
|
||||
print(' ' * 6 + 'created: ' + t['created'].split(' ')[0])
|
||||
print(' ' * 6 + 'modified: ' + t['modified'][:10])
|
||||
print('')
|
||||
|
||||
elif update_type == 'software':
|
||||
software = load_attack_data(DATATYPE_ALL_SOFTWARE)
|
||||
sorted_software = sorted(software, key=lambda k: k[sort])
|
||||
|
||||
for t in sorted_software:
|
||||
print(t['software_id'] + ' ' + t['software'])
|
||||
print(' ' * 6 + 'created: ' + t['created'].split(' ')[0])
|
||||
print(' ' * 6 + 'modified: ' + t['modified'][:10])
|
||||
print(' ' * 6 + 'matrix: ' + t['matrix'][6:])
|
||||
print(' ' * 6 + 'type: ' + t['type'])
|
||||
if t['software_platform']:
|
||||
print(' ' * 6 + 'platform: ' + ', '.join(t['software_platform']))
|
||||
else:
|
||||
print(' ' * 6 + 'platform: None')
|
||||
print('')
|
||||
|
||||
|
||||
def get_statistics():
|
||||
"""
|
||||
Print out statistics related to data sources and how many techniques they cover.
|
||||
:return:
|
||||
"""
|
||||
techniques = load_attack_data(DATATYPE_ALL_TECH)
|
||||
|
||||
# {data_source: {techniques: [T0001, ...}, count: ...}
|
||||
data_sources_dict = {}
|
||||
for tech in techniques:
|
||||
tech_id = tech['technique_id']
|
||||
data_sources = tech['data_sources']
|
||||
|
||||
if data_sources:
|
||||
for ds in data_sources:
|
||||
if ds not in data_sources_dict:
|
||||
data_sources_dict[ds] = {'techniques': [tech_id], 'count': 1}
|
||||
else:
|
||||
data_sources_dict[ds]['techniques'].append(tech_id)
|
||||
data_sources_dict[ds]['count'] += 1
|
||||
|
||||
# sort the dict on the value of 'count'
|
||||
data_sources_dict_sorted = dict(sorted(data_sources_dict.items(), key=lambda kv: kv[1]['count'], reverse=True))
|
||||
str_format = '{:<6s} {:s}'
|
||||
print(str_format.format('Count', 'Data Source'))
|
||||
print('-'*50)
|
||||
for k, v in data_sources_dict_sorted.items():
|
||||
print(str_format.format(str(v['count']), k))
|
|
@ -0,0 +1,382 @@
|
|||
import sys
|
||||
import glob
|
||||
from data_source_mapping import *
|
||||
from technique_mapping import *
|
||||
from group_mapping import *
|
||||
|
||||
|
||||
groups = 'all'
|
||||
software_group = False
|
||||
platform = 'Windows'
|
||||
stage = 'attack'
|
||||
groups_overlay = ''
|
||||
overlay_type = ''
|
||||
|
||||
MENU_NAME_DATA_SOURCE_MAPPING = 'Data source mapping'
|
||||
MENU_NAME_VISIBILITY_MAPPING = 'Visibility coverage mapping'
|
||||
MENU_NAME_DETECTION_COVERAGE_MAPPING = 'Detection coverage mapping'
|
||||
MENU_NAME_THREAT_ACTOR_GROUP_MAPPING = 'Threat actor group mapping'
|
||||
|
||||
|
||||
def clear():
|
||||
"""
|
||||
Clears the terminal screen and prints the title and version of the application.
|
||||
:return:
|
||||
"""
|
||||
if sys.platform.startswith('linux') or sys.platform == 'darwin':
|
||||
os.system('clear')
|
||||
elif sys.platform == 'win32':
|
||||
os.system('cls')
|
||||
name = '-= %s =-' % APP_NAME
|
||||
desc = '-- %s --' % APP_DESC
|
||||
version = 'version %s' % VERSION
|
||||
print(' ' * int((len(desc)-len(name))/2) + name)
|
||||
print(desc)
|
||||
print(' ' * int((len(desc)-len(version))/2) + version)
|
||||
print('')
|
||||
|
||||
|
||||
def ask_input():
|
||||
"""
|
||||
Waits for input from the terminal.
|
||||
:return:
|
||||
"""
|
||||
return input(' >> ')
|
||||
|
||||
|
||||
def wait():
|
||||
"""
|
||||
Prints wait statement and wait for pressing ENTER key.
|
||||
:return:
|
||||
"""
|
||||
print('')
|
||||
print('Press a key to return to the last menu')
|
||||
input('')
|
||||
|
||||
|
||||
def interactive_menu():
|
||||
"""
|
||||
Main menu for interactive mode.
|
||||
:return:
|
||||
"""
|
||||
clear()
|
||||
print('Select a mode:')
|
||||
print('1. %s' % MENU_NAME_DATA_SOURCE_MAPPING)
|
||||
print('2. %s' % MENU_NAME_VISIBILITY_MAPPING)
|
||||
print('3. %s' % MENU_NAME_DETECTION_COVERAGE_MAPPING)
|
||||
print('4. %s' % MENU_NAME_THREAT_ACTOR_GROUP_MAPPING)
|
||||
print('5. Updates')
|
||||
print('6. Statistics')
|
||||
print('9. Quit')
|
||||
choice = ask_input()
|
||||
if choice == '1':
|
||||
menu_data_source(select_file(MENU_NAME_DATA_SOURCE_MAPPING, 'data sources', FILE_TYPE_DATA_SOURCE_ADMINISTRATION))
|
||||
elif choice == '2':
|
||||
menu_visibility(select_file(MENU_NAME_VISIBILITY_MAPPING, 'techniques (used to score the level of visibility)', FILE_TYPE_TECHNIQUE_ADMINISTRATION),
|
||||
select_file(MENU_NAME_VISIBILITY_MAPPING, 'data sources (used to add metadata on the involved data sources to the heat map)', FILE_TYPE_DATA_SOURCE_ADMINISTRATION, False))
|
||||
elif choice == '3':
|
||||
menu_detection(select_file(MENU_NAME_DETECTION_COVERAGE_MAPPING, 'techniques', FILE_TYPE_TECHNIQUE_ADMINISTRATION))
|
||||
elif choice == '4':
|
||||
menu_groups()
|
||||
elif choice == '5':
|
||||
menu_updates()
|
||||
elif choice == '6':
|
||||
menu_statistics()
|
||||
elif choice in ['9', 'q']:
|
||||
quit()
|
||||
else:
|
||||
interactive_menu()
|
||||
|
||||
|
||||
def select_file(title, what, expected_file_type, b_clear=True, path='sample-data/'):
|
||||
"""
|
||||
Prints and handles the file selection in the terminal. It shows just .yaml files.
|
||||
:param title: title to print on top of this menu
|
||||
:param what: print for what purpose the file is selected
|
||||
:param expected_file_type: the expected file type of the YAML file
|
||||
:param b_clear: clear the terminal before showing this memu
|
||||
:param path: the path to look in
|
||||
:return: filename of the selected file
|
||||
"""
|
||||
if b_clear:
|
||||
clear()
|
||||
print('Menu: %s' % title)
|
||||
print('')
|
||||
print('Select the YAML file with %s:' % what)
|
||||
print('')
|
||||
print('Path: %s' % path)
|
||||
n = 1
|
||||
files = []
|
||||
for f in glob.glob(path + '*.yaml'):
|
||||
files.append(f)
|
||||
print('%d. %s' % (n, f))
|
||||
n += 1
|
||||
|
||||
change_path_nr = 8 if n < 8 else n + (5 - n % 5) - 1
|
||||
print('%d. Change path' % change_path_nr)
|
||||
|
||||
back_nr = 9 if n < 9 else n + (5 - n % 5)
|
||||
print('%d. Back to main menu.' % back_nr)
|
||||
|
||||
choice = ask_input()
|
||||
if choice == str(change_path_nr):
|
||||
print("Supply full or relative path:")
|
||||
choice = ask_input()
|
||||
choice = choice if choice.endswith('/') else choice + '/'
|
||||
if os.path.exists(choice):
|
||||
return select_file(title, what, expected_file_type, b_clear, choice)
|
||||
else:
|
||||
print("[!] Path doesn't exist")
|
||||
wait()
|
||||
return select_file(title, what, expected_file_type, b_clear, path)
|
||||
elif choice == str(back_nr):
|
||||
interactive_menu()
|
||||
elif choice == 'q':
|
||||
quit()
|
||||
else:
|
||||
if choice.isdigit() and int(choice) < n:
|
||||
filename = files[int(choice) - 1]
|
||||
file_type = check_file_type(filename, file_type=expected_file_type)
|
||||
if file_type:
|
||||
return filename
|
||||
else:
|
||||
print("[!] Invalid choice")
|
||||
|
||||
wait()
|
||||
return select_file(title, what, expected_file_type, b_clear, path)
|
||||
|
||||
|
||||
def menu_updates():
|
||||
"""
|
||||
Prints and handles the menu for the Updates functionality.
|
||||
:return:
|
||||
"""
|
||||
clear()
|
||||
|
||||
print('Menu: Updates')
|
||||
print('')
|
||||
print('Select for what you want to see updates:')
|
||||
print('1. Techniques (sorted by modified date)')
|
||||
print('1s. Techniques (sorted by creation date)')
|
||||
print('2. Groups (sorted by modified date)')
|
||||
print('2s. Groups (sorted by creation date)')
|
||||
print('3. Software (sorted by modified date)')
|
||||
print('3s. Software (sorted by creation date)')
|
||||
print('9. Back to main menu.')
|
||||
choice = ask_input()
|
||||
if choice == '1':
|
||||
get_updates('techniques')
|
||||
wait()
|
||||
if choice == '1s':
|
||||
get_updates('techniques', 'created')
|
||||
wait()
|
||||
elif choice == '2':
|
||||
get_updates('groups')
|
||||
wait()
|
||||
elif choice == '2s':
|
||||
get_updates('groups', 'created')
|
||||
wait()
|
||||
elif choice == '3':
|
||||
get_updates('software')
|
||||
wait()
|
||||
elif choice == '3s':
|
||||
get_updates('software', 'created')
|
||||
wait()
|
||||
elif choice == '9':
|
||||
interactive_menu()
|
||||
elif choice == 'q':
|
||||
quit()
|
||||
menu_updates()
|
||||
|
||||
|
||||
def menu_statistics():
|
||||
"""
|
||||
Handles the Statistics functionality.
|
||||
:return:
|
||||
"""
|
||||
clear()
|
||||
print('Menu: Statistics')
|
||||
print('')
|
||||
get_statistics()
|
||||
wait()
|
||||
interactive_menu()
|
||||
|
||||
|
||||
def menu_data_source(filename):
|
||||
"""
|
||||
Prints and handles the Data source mapping functionality.
|
||||
:param filename:
|
||||
:return:
|
||||
"""
|
||||
clear()
|
||||
print('Menu: %s' % MENU_NAME_DATA_SOURCE_MAPPING)
|
||||
print('')
|
||||
print('Selected data source YAML file: %s' % filename)
|
||||
print('')
|
||||
print('Select what you want to do:')
|
||||
print('1. Generate a data source layer for the ATT&CK Navigator.')
|
||||
print('2. Generate a graph with data sources added through time.')
|
||||
print('3. Generate an Excel sheet with all data sources.')
|
||||
print('4. Generate a technique administration YAML file with visibility scores, based on the number of available '
|
||||
'data sources')
|
||||
print('9. Back to main menu.')
|
||||
choice = ask_input()
|
||||
if choice == '1':
|
||||
print('Writing data sources layer...')
|
||||
generate_data_sources_layer(filename)
|
||||
wait()
|
||||
elif choice == '2':
|
||||
print('Drawing the graph...')
|
||||
plot_data_sources_graph(filename)
|
||||
wait()
|
||||
elif choice == '3':
|
||||
print('Generating Excel file...')
|
||||
export_data_source_list_to_excel(filename)
|
||||
wait()
|
||||
elif choice == '4':
|
||||
print('Generating YAML file...')
|
||||
generate_technique_administration_file(filename)
|
||||
wait()
|
||||
elif choice == '9':
|
||||
interactive_menu()
|
||||
elif choice == 'q':
|
||||
quit()
|
||||
menu_data_source(filename)
|
||||
|
||||
|
||||
def menu_detection(filename_t):
|
||||
"""
|
||||
Prints and handles the Detection coverage mapping functionality.
|
||||
:param filename_t:
|
||||
:return:
|
||||
"""
|
||||
clear()
|
||||
print('Menu: %s' % MENU_NAME_DETECTION_COVERAGE_MAPPING)
|
||||
print('')
|
||||
print('Selected techniques YAML file: %s' % filename_t)
|
||||
print('')
|
||||
print('Select what you want to do:')
|
||||
print('1. Generate a layer for detection coverage for the ATT&CK Navigator.')
|
||||
print('2. Generate a layer for detection coverage overlayed with visibility for the ATT&CK Navigator.')
|
||||
print('3. Generate a graph with detections added through time.')
|
||||
print('9. Back to main menu.')
|
||||
choice = ask_input()
|
||||
if choice == '1':
|
||||
print('Writing detection coverage layer...')
|
||||
generate_detection_layer(filename_t, None, False)
|
||||
wait()
|
||||
elif choice == '2':
|
||||
filename_ds = select_file(MENU_NAME_DETECTION_COVERAGE_MAPPING, 'data sources (used to add metadata on the involved data sources to the heat map)', FILE_TYPE_DATA_SOURCE_ADMINISTRATION, False)
|
||||
print('Writing detection coverage layer with visibility as overlay...')
|
||||
generate_detection_layer(filename_t, filename_ds, True)
|
||||
wait()
|
||||
elif choice == '3':
|
||||
print('Drawing the graph...')
|
||||
plot_detection_graph(filename_t)
|
||||
wait()
|
||||
elif choice == '9':
|
||||
interactive_menu()
|
||||
elif choice == 'q':
|
||||
quit()
|
||||
menu_detection(filename_t)
|
||||
|
||||
|
||||
def menu_visibility(filename_t, filename_ds):
|
||||
"""
|
||||
Prints and handles the Visibility coverage mappin functionality.
|
||||
:param filename_t:
|
||||
:param filename_ds:
|
||||
:return:
|
||||
"""
|
||||
clear()
|
||||
print('Menu: %s' % MENU_NAME_VISIBILITY_MAPPING)
|
||||
print('')
|
||||
print('Selected techniques YAML file: %s' % filename_t)
|
||||
print('Selected data source YAML file: %s' % filename_ds)
|
||||
print('')
|
||||
print('Select what you want to do:')
|
||||
print('1. Generate a layer for visibility for the ATT&CK Navigator.')
|
||||
print('2. Generate a layers for visibility overlayed with detection coverage for the ATT&CK Navigator.')
|
||||
print('9. Back to main menu.')
|
||||
choice = ask_input()
|
||||
if choice == '1':
|
||||
print('Writing visibility coverage layer...')
|
||||
generate_visibility_layer(filename_t, filename_ds, False)
|
||||
wait()
|
||||
elif choice == '2':
|
||||
print('Writing visibility coverage layers overlayed with detections...')
|
||||
generate_visibility_layer(filename_t, filename_ds, True)
|
||||
wait()
|
||||
elif choice == '9':
|
||||
interactive_menu()
|
||||
elif choice == 'q':
|
||||
quit()
|
||||
menu_visibility(filename_t, filename_ds)
|
||||
|
||||
|
||||
def menu_groups():
|
||||
"""
|
||||
Prints and handles the Threat actor group mapping functionality.
|
||||
:return:
|
||||
"""
|
||||
global groups, software_group, platform, stage, groups_overlay, overlay_type
|
||||
clear()
|
||||
print('Menu: %s' % MENU_NAME_THREAT_ACTOR_GROUP_MAPPING)
|
||||
print('')
|
||||
print('Options:')
|
||||
print('1. Software group: %s' % str(software_group))
|
||||
print('2. Platform: %s' % platform)
|
||||
print('3. Stage: %s' % stage)
|
||||
print('4. Groups: %s' % groups)
|
||||
print('5. Overlay: ')
|
||||
print(' - %s: %s' % ('File' if os.path.exists(groups_overlay) else 'Groups', groups_overlay))
|
||||
print(' - Type: %s' % overlay_type)
|
||||
print('')
|
||||
print('6. Generate a heat map layer.')
|
||||
print('9. Back to main menu.')
|
||||
choice = ask_input()
|
||||
if choice == '1':
|
||||
print('Specify True or False for software group:')
|
||||
software_group = True if ask_input().lower() == 'true' else False
|
||||
elif choice == '2':
|
||||
print('Specify platform (all, Linux, macOS, Windows):')
|
||||
p = ask_input().lower()
|
||||
platform = 'Windows' if p == 'windows' else 'Linux' if p == 'linux' else 'macOS' if p == 'macos' else 'all'
|
||||
elif choice == '3':
|
||||
print('Specify stage (pre-attack, attack):')
|
||||
s = ask_input().lower()
|
||||
stage = 'pre-attack' if s == 'pre-attack' else 'attack'
|
||||
elif choice == '4':
|
||||
print('Specify the groups to include separated using commas. Group can be their ID, name or alias '
|
||||
'(default is all groups). Other option is to provide a YAML file with a custom group(s)')
|
||||
groups = ask_input()
|
||||
elif choice == '5':
|
||||
print('')
|
||||
print('1. Overlay with groups.')
|
||||
print('2. Overlay with detections.')
|
||||
print('3. Overlay with visibility.')
|
||||
print('4. No overlay.')
|
||||
choice = ask_input()
|
||||
if choice == '1':
|
||||
print('Specify the group(s) to overlay (in a different color) on the one specified in the Groups option. '
|
||||
'A group can be their ID, name or alias separated using commas. Other option is to provide a YAML '
|
||||
'file with a custom group(s).')
|
||||
overlay_type = 'group'
|
||||
groups_overlay = ask_input()
|
||||
elif choice == '2':
|
||||
overlay_type = 'detection'
|
||||
groups_overlay = select_file(MENU_NAME_THREAT_ACTOR_GROUP_MAPPING, 'techniques', FILE_TYPE_TECHNIQUE_ADMINISTRATION, False)
|
||||
elif choice == '3':
|
||||
overlay_type = 'visibility'
|
||||
groups_overlay = select_file(MENU_NAME_THREAT_ACTOR_GROUP_MAPPING, 'techniques', FILE_TYPE_TECHNIQUE_ADMINISTRATION, False)
|
||||
elif choice == '4':
|
||||
overlay_type = ''
|
||||
groups_overlay = ''
|
||||
elif choice == '6':
|
||||
generate_group_heat_map(groups, groups_overlay, overlay_type, stage, platform, software_group)
|
||||
wait()
|
||||
elif choice == '9':
|
||||
interactive_menu()
|
||||
elif choice == 'q':
|
||||
quit()
|
||||
menu_groups()
|
|
@ -0,0 +1,6 @@
|
|||
attackcti==0.2.1
|
||||
simplejson==3.16.0
|
||||
PyYAML==5.1
|
||||
plotly==3.7.1
|
||||
pandas==0.24.2
|
||||
xlsxwriter==1.1.5
|
|
@ -0,0 +1,626 @@
|
|||
%YAML 1.2
|
||||
---
|
||||
version: 1.0
|
||||
file_type: data-source-administration
|
||||
name: endpoints-example
|
||||
platform: windows
|
||||
data_sources:
|
||||
# A data source is treated as not available when all dimensions of the data quality have a score of 0.
|
||||
# If desired you are free to add any key-value pairs.
|
||||
- data_source_name: Process monitoring
|
||||
date_registered: 2019-03-01
|
||||
date_connected: 2017-01-01
|
||||
products: [Windows event log]
|
||||
available_for_data_analytics: True
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 5
|
||||
data_field_completeness: 5
|
||||
timeliness: 5
|
||||
consistency: 5
|
||||
retention: 5
|
||||
- data_source_name: File monitoring
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Process command-line parameters
|
||||
date_registered: 2019-03-01
|
||||
date_connected: 2017-01-01
|
||||
products: [Windows event log]
|
||||
available_for_data_analytics: True
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: API monitoring
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Process use of network
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Windows Registry
|
||||
date_registered: 2019-03-01
|
||||
date_connected: 2017-02-01
|
||||
products: [Windows event log]
|
||||
available_for_data_analytics: True
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 5
|
||||
data_field_completeness: 5
|
||||
timeliness: 5
|
||||
consistency: 5
|
||||
retention: 5
|
||||
- data_source_name: Packet capture
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Authentication logs
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Netflow/Enclave netflow
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Windows event logs
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Binary file metadata
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Network protocol analysis
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: DLL monitoring
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Loaded DLLs
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: System calls
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Malware reverse engineering
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: SSL/TLS inspection
|
||||
date_registered: 2019-01-10
|
||||
date_connected: 2000-01-01
|
||||
products: [Proxy Product]
|
||||
available_for_data_analytics: True
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 5
|
||||
data_field_completeness: 5
|
||||
timeliness: 5
|
||||
consistency: 5
|
||||
retention: 5
|
||||
- data_source_name: Anti-virus
|
||||
date_registered: 2019-01-10
|
||||
date_connected: 2000-01-01
|
||||
products: [AV Product]
|
||||
available_for_data_analytics: True
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 4
|
||||
data_field_completeness: 2
|
||||
timeliness: 3
|
||||
consistency: 5
|
||||
retention: 5
|
||||
- data_source_name: Network intrusion detection system
|
||||
date_registered: 2019-01-10
|
||||
date_connected: 2016-01-01
|
||||
products: [NIDS]
|
||||
available_for_data_analytics: True
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 4
|
||||
data_field_completeness: 3
|
||||
timeliness: 3
|
||||
consistency: 4
|
||||
retention: 4
|
||||
- data_source_name: Data loss prevention
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Application logs
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Email gateway
|
||||
date_registered: 2019-01-10
|
||||
date_connected: 2000-01-01
|
||||
products: [Email Gateway Product]
|
||||
available_for_data_analytics: True
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 4
|
||||
data_field_completeness: 3
|
||||
timeliness: 3
|
||||
consistency: 5
|
||||
retention: 3
|
||||
- data_source_name: Network device logs
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Web proxy
|
||||
date_registered: 2019-01-10
|
||||
date_connected: 2000-01-01
|
||||
products: [Proxy Product]
|
||||
available_for_data_analytics: True
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 5
|
||||
data_field_completeness: 5
|
||||
timeliness: 5
|
||||
consistency: 5
|
||||
retention: 5
|
||||
- data_source_name: Windows Error Reporting
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Kernel drivers
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: User interface
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Host network interface
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Third-party application logs
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Services
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Web logs
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Detonation chamber
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Mail server
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Environment variable
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: MBR
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: BIOS
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Web application firewall logs
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Asset management
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: DHCP
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: True
|
||||
comment: 'At the time of writing: unknown data source within ATT&CK'
|
||||
data_quality:
|
||||
device_completeness: 5
|
||||
data_field_completeness: 5
|
||||
timeliness: 5
|
||||
consistency: 5
|
||||
retention: 5
|
||||
- data_source_name: DNS records
|
||||
date_registered: 2019-03-01
|
||||
date_connected: 2017-04-01
|
||||
products: [Windows DNS server]
|
||||
available_for_data_analytics: True
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 5
|
||||
data_field_completeness: 5
|
||||
timeliness: 5
|
||||
consistency: 5
|
||||
retention: 5
|
||||
- data_source_name: Browser extensions
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Access tokens
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Digital certificate logs
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Disk forensics
|
||||
date_registered: 2019-01-10
|
||||
date_connected: 2019-01-01
|
||||
products: [Manual, Commercial tool]
|
||||
available_for_data_analytics: True
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 5
|
||||
data_field_completeness: 5
|
||||
timeliness: 1
|
||||
consistency: 5
|
||||
retention: 0
|
||||
- data_source_name: Component firmware
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: WMI Objects
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: VBR
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Named Pipes
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: Sensor health and status
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: EFI
|
||||
date_registered:
|
||||
date_connected:
|
||||
products: [None]
|
||||
available_for_data_analytics: False
|
||||
comment: ''
|
||||
data_quality:
|
||||
device_completeness: 0
|
||||
data_field_completeness: 0
|
||||
timeliness: 0
|
||||
consistency: 0
|
||||
retention: 0
|
||||
- data_source_name: PowerShell logs
|
||||
date_registered: 2019-03-01
|
||||
date_connected: 2018-05-01
|
||||
products: [Windows event log]
|
||||
available_for_data_analytics: True
|
||||
comment: 'Script block logging is enabled'
|
||||
data_quality:
|
||||
device_completeness: 5
|
||||
data_field_completeness: 5
|
||||
timeliness: 5
|
||||
consistency: 5
|
||||
retention: 4
|
||||
exceptions:
|
||||
# Adding a technique ID below will result in removing that technique in the heat map (meaning not enough data source are available for proper detection).
|
||||
# Please note that the below is just an example, many more can exists.
|
||||
- technique_id: T1130
|
||||
name: Install Root Certificate
|
|
@ -0,0 +1,17 @@
|
|||
%YAML 1.2
|
||||
---
|
||||
version: 1.0
|
||||
file_type: group-administration
|
||||
groups:
|
||||
-
|
||||
group_name: Red team
|
||||
campaign: Scenario 1
|
||||
technique_id: [T1086, T1053, T1193, T1204, T1003, T1055, T1027, T1085, T1099, T1082, T1016, T1033, T1087, T1075, T1057, T1039, T1041, T1071, T1043, T1001, T1114, T1002]
|
||||
software_id: [S0002]
|
||||
enabled: True
|
||||
-
|
||||
group_name: APT3 (MITRE ATT&CK evaluation)
|
||||
campaign: First Scenario
|
||||
technique_id: [T1204, T1064, T1085, T1060, T1043, T1071, T1132, T1016, T1059, T1033, T1057, T1106, T1007, T1082, T1069, T1087, T1012, T1088, T1134, T1055, T1018, T1049, T1003, T1026, T1076, T1136, T1061, T1105, T1053, T1083, T1056, T1010, T1113, T1039, T1041, T1078]
|
||||
software_id: [S0154]
|
||||
enabled: False
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -0,0 +1,273 @@
|
|||
import simplejson
|
||||
from generic import *
|
||||
# Imports for pandas and plotly are because of performance reasons in the function that uses these libraries.
|
||||
|
||||
|
||||
def generate_detection_layer(filename_techniques, filename_data_sources, overlay):
|
||||
"""
|
||||
Generates layer for detection coverage and optionally an overlayed version with visibility coverage.
|
||||
:param filename_techniques: the filename of the yaml file containing the techniques administration
|
||||
:param filename_data_sources: the filename of the yaml file containing the data sources administration
|
||||
:param overlay: boolean value to specify if an overlay between detection and visibility should be generated
|
||||
:return:
|
||||
"""
|
||||
my_techniques, name, platform = _load_detections(filename_techniques)
|
||||
|
||||
if not overlay:
|
||||
mapped_techniques_detection = _map_and_colorize_techniques_for_detections(my_techniques)
|
||||
layer_detection = get_layer_template_detections('Detections ' + name, 'description', 'attack', platform)
|
||||
_write_layer(layer_detection, mapped_techniques_detection, 'detection', name)
|
||||
else:
|
||||
my_data_sources = _load_data_sources(filename_data_sources)
|
||||
mapped_techniques_both = _map_and_colorize_techniques_for_overlayed(my_techniques, my_data_sources)
|
||||
layer_both = get_layer_template_layered('Visibility and Detection ' + name, 'description', 'attack', platform)
|
||||
_write_layer(layer_both, mapped_techniques_both, 'visibility_and_detection', name)
|
||||
|
||||
|
||||
def generate_visibility_layer(filename_techniques, filename_data_sources, overlay):
|
||||
"""
|
||||
Generates layer for visibility coverage and optionally an overlayed version with detection coverage.
|
||||
:param filename_techniques: the filename of the yaml file containing the techniques administration
|
||||
:param filename_data_sources: the filename of the yaml file containing the data sources administration
|
||||
:param overlay: boolean value to specify if an overlay between detection and visibility should be generated
|
||||
:return:
|
||||
"""
|
||||
my_techniques, name, platform = _load_detections(filename_techniques)
|
||||
my_data_sources = _load_data_sources(filename_data_sources)
|
||||
|
||||
if not overlay:
|
||||
mapped_techniques_visibility = _map_and_colorize_techniques_for_visibility(my_techniques, my_data_sources)
|
||||
layer_visibility = get_layer_template_visibility('Visibility ' + name, 'description', 'attack', platform)
|
||||
_write_layer(layer_visibility, mapped_techniques_visibility, 'visibility', name)
|
||||
else:
|
||||
mapped_techniques_both = _map_and_colorize_techniques_for_overlayed(my_techniques, my_data_sources)
|
||||
layer_both = get_layer_template_layered('Visibility and Detection ' + name, 'description', 'attack', platform)
|
||||
_write_layer(layer_both, mapped_techniques_both, 'visibility_and_detection', name)
|
||||
|
||||
|
||||
def plot_detection_graph(filename):
|
||||
"""
|
||||
Generates a line graph which shows the improvements on detections through the time.
|
||||
:param filename: the filename of the yaml file containing the techniques administration
|
||||
:return:
|
||||
"""
|
||||
my_techniques, name, platform = _load_detections(filename)
|
||||
|
||||
graph_values = []
|
||||
for t in my_techniques.values():
|
||||
if 'detection' in t.keys() and t['detection']['date_implemented']:
|
||||
yyyymm = t['detection']['date_implemented'].strftime('%Y-%m')
|
||||
graph_values.append({'date': yyyymm, 'count': 1})
|
||||
|
||||
import pandas as pd
|
||||
df = pd.DataFrame(graph_values).groupby('date', as_index=False)[['count']].sum()
|
||||
df['cumcount'] = df.ix[::1, 'count'].cumsum()[::1]
|
||||
|
||||
output_filename = 'output/graph_detection.html'
|
||||
import plotly
|
||||
import plotly.graph_objs as go
|
||||
plotly.offline.plot(
|
||||
{'data': [go.Scatter(x=df['date'], y=df['cumcount'])],
|
||||
'layout': go.Layout(title="# of detections for " + name)},
|
||||
filename=output_filename, auto_open=False
|
||||
)
|
||||
print("File written: " + output_filename)
|
||||
|
||||
|
||||
def _load_detections(filename):
|
||||
"""
|
||||
Loads the techniques (including detection and visibility properties) from the given yaml file.
|
||||
:param filename: the filename of the yaml file containing the techniques administration
|
||||
:return: dictionary with techniques (incl. properties), name and platform
|
||||
"""
|
||||
my_techniques = {}
|
||||
with open(filename, 'r') as yaml_file:
|
||||
yaml_content = yaml.load(yaml_file, Loader=yaml.FullLoader)
|
||||
for d in yaml_content['techniques']:
|
||||
my_techniques[d['technique_id']] = d
|
||||
name = yaml_content['name']
|
||||
platform = yaml_content['platform']
|
||||
return my_techniques, name, platform
|
||||
|
||||
|
||||
def _load_data_sources(filename):
|
||||
"""
|
||||
Loads the data sources (including all properties) from the given yaml file.
|
||||
:param filename: the filename of the yaml file containing the data sources administration
|
||||
:return: dictionary with data sources (including properties)
|
||||
"""
|
||||
my_data_sources = {}
|
||||
with open(filename, 'r') as yaml_file:
|
||||
yaml_content = yaml.load(yaml_file, Loader=yaml.FullLoader)
|
||||
for d in yaml_content['data_sources']:
|
||||
dq = d['data_quality']
|
||||
if dq['device_completeness'] > 0 and dq['data_field_completeness'] > 0 and dq['timeliness'] > 0 and dq['consistency'] > 0:
|
||||
my_data_sources[d['data_source_name']] = d
|
||||
return my_data_sources
|
||||
|
||||
|
||||
def _write_layer(layer, mapped_techniques, filename_prefix, name):
|
||||
"""
|
||||
Writes the json layer file to disk.
|
||||
:param layer: the prepped layer dictionary
|
||||
:param mapped_techniques: the techniques section that will be included in the layer
|
||||
:param filename_prefix: the prefix for the output filename
|
||||
:param name: the name that will be used in the filename together with the prefix
|
||||
:return:
|
||||
"""
|
||||
|
||||
layer['techniques'] = mapped_techniques
|
||||
json_string = simplejson.dumps(layer).replace('}, ', '},\n')
|
||||
output_filename = 'output/%s_%s.json' % (filename_prefix, normalize_name_to_filename(name))
|
||||
with open(output_filename, 'w') as f:
|
||||
f.write(json_string)
|
||||
print("File written: " + output_filename)
|
||||
|
||||
|
||||
def _map_and_colorize_techniques_for_detections(my_techniques):
|
||||
"""
|
||||
Determine the color of the techniques based on the detection score in the given yaml file.
|
||||
:param my_techniques: the configured techniques
|
||||
:return: a dictionary with techniques that can be used in the layer's output file
|
||||
"""
|
||||
techniques = load_attack_data(DATATYPE_ALL_TECH)
|
||||
|
||||
# Color the techniques based on how the coverage defined in the detections definition and generate a list with
|
||||
# techniques to be used in the layer output file.
|
||||
mapped_techniques = []
|
||||
try:
|
||||
for d, c in my_techniques.items():
|
||||
s = -1 if 'detection' not in c.keys() else c['detection']['score']
|
||||
color = COLOR_D_0 if s == 0 else COLOR_D_1 if s == 1 else COLOR_D_2 if s == 2 else COLOR_D_3 \
|
||||
if s == 3 else COLOR_D_4 if s == 4 else COLOR_D_5 if s == 5 else ''
|
||||
technique = get_technique(techniques, d)
|
||||
for tactic in technique['tactic']:
|
||||
location = ', '.join(c['detection']['location']) if 'detection' in c.keys() else '-'
|
||||
location = location if location != '' else '-'
|
||||
x = {}
|
||||
x['techniqueID'] = d
|
||||
x['color'] = color
|
||||
x['comment'] = ''
|
||||
x['enabled'] = True
|
||||
x['tactic'] = tactic.lower().replace(' ', '-')
|
||||
x['metadata'] = [{'name': '-Detection score', 'value': str(s)},
|
||||
{'name': '-Detection location', 'value': location}]
|
||||
|
||||
mapped_techniques.append(x)
|
||||
except Exception:
|
||||
print('[!] Possible error in YAML file at: ' + d)
|
||||
quit()
|
||||
|
||||
return mapped_techniques
|
||||
|
||||
|
||||
def _map_and_colorize_techniques_for_visibility(my_techniques, my_data_sources):
|
||||
"""
|
||||
Determine the color of the techniques based on the visibility score in the given yaml file.
|
||||
:param my_techniques: the configured techniques
|
||||
:param my_data_sources: the configured data sources
|
||||
:return: a dictionary with techniques that can be used in the layer's output file
|
||||
"""
|
||||
techniques = load_attack_data(DATATYPE_ALL_TECH)
|
||||
|
||||
technique_ds_mapping = map_techniques_to_data_sources(techniques, my_data_sources)
|
||||
|
||||
# Color the techniques based on how the coverage defined in the detections definition and generate a list with
|
||||
# techniques to be used in the layer output file.
|
||||
mapped_techniques = []
|
||||
for d, c in my_techniques.items():
|
||||
s = 0 if 'visibility' not in c.keys() else c['visibility']['score']
|
||||
if 'visibility' in c.keys():
|
||||
comment = str(c['visibility']['comment']) if str(c['visibility']['comment']) != '' else '-'
|
||||
else:
|
||||
comment = '-'
|
||||
my_ds = ', '.join(technique_ds_mapping[d]['my_data_sources']) if d in technique_ds_mapping.keys() and technique_ds_mapping[d]['my_data_sources'] else '-'
|
||||
color = COLOR_V_1 if s == 1 else COLOR_V_2 if s == 2 else COLOR_V_3 if s == 3 else COLOR_V_4 if s == 4 else ''
|
||||
technique = get_technique(techniques, d)
|
||||
for tactic in technique['tactic']:
|
||||
x = {}
|
||||
x['techniqueID'] = d
|
||||
x['color'] = color
|
||||
x['comment'] = ''
|
||||
x['enabled'] = True
|
||||
x['tactic'] = tactic.lower().replace(' ', '-')
|
||||
x['metadata'] = [{'name': '-Visibility score', 'value': str(s)},
|
||||
{'name': '-Comment', 'value': comment},
|
||||
{'name': '-Available data sources', 'value': my_ds},
|
||||
{'name': '-ATT&CK data sources', 'value': ', '.join(technique['data_sources'])}]
|
||||
|
||||
mapped_techniques.append(x)
|
||||
|
||||
for t in techniques:
|
||||
if t['technique_id'] not in my_techniques.keys():
|
||||
if t['tactic']:
|
||||
for tactic in t['tactic']:
|
||||
x = {}
|
||||
x['techniqueID'] = t['technique_id']
|
||||
x['comment'] = ''
|
||||
x['enabled'] = True
|
||||
x['tactic'] = tactic.lower().replace(' ', '-')
|
||||
ds = ', '.join(t['data_sources']) if t['data_sources'] else '-'
|
||||
x['metadata'] = [{'name': '-ATT&CK data sources', 'value': ds}]
|
||||
|
||||
mapped_techniques.append(x)
|
||||
|
||||
return mapped_techniques
|
||||
|
||||
|
||||
def _map_and_colorize_techniques_for_overlayed(my_techniques, my_data_sources):
|
||||
"""
|
||||
Determine the color of the techniques based on both detection and visibility.
|
||||
:param my_techniques: the configured techniques
|
||||
:param my_data_sources: the configured data sources
|
||||
:return: a dictionary with techniques that can be used in the layer's output file
|
||||
"""
|
||||
techniques = load_attack_data(DATATYPE_ALL_TECH)
|
||||
|
||||
technique_ds_mapping = map_techniques_to_data_sources(techniques, my_data_sources)
|
||||
|
||||
# Color the techniques based on how the coverage defined in the detections definition and generate a list with
|
||||
# techniques to be used in the layer output file.
|
||||
mapped_techniques = []
|
||||
for d, c in my_techniques.items():
|
||||
detection_score = 0 if 'detection' not in c.keys() else c['detection']['score']
|
||||
visibility_score = 0 if 'visibility' not in c.keys() else c['visibility']['score']
|
||||
|
||||
detection = True if detection_score > 0 else False
|
||||
visibility = True if visibility_score > 0 else False
|
||||
|
||||
if detection and visibility:
|
||||
color = COLOR_OVERLAY_BOTH
|
||||
elif detection and not visibility:
|
||||
color = COLOR_OVERLAY_DETECTION
|
||||
elif not detection and visibility:
|
||||
color = COLOR_OVERLAY_VISIBILITY
|
||||
|
||||
location = ', '.join(c['detection']['location']) if 'detection' in c.keys() else '-'
|
||||
location = location if location != '' else '-'
|
||||
|
||||
if 'visibility' in c.keys():
|
||||
comment = str(c['visibility']['comment']) if str(c['visibility']['comment']) != '' else '-'
|
||||
else:
|
||||
comment = '-'
|
||||
|
||||
my_ds = ', '.join(technique_ds_mapping[d]['my_data_sources']) if d in technique_ds_mapping.keys() and technique_ds_mapping[d]['my_data_sources'] else '-'
|
||||
|
||||
technique = get_technique(techniques, d)
|
||||
for tactic in technique['tactic']:
|
||||
x = {}
|
||||
x['techniqueID'] = d
|
||||
x['color'] = color
|
||||
x['comment'] = ''
|
||||
x['enabled'] = True
|
||||
x['tactic'] = tactic.lower().replace(' ', '-')
|
||||
x['metadata'] = [{'name': '-Visibility score', 'value': str(visibility_score)},
|
||||
{'name': '-Comment', 'value': comment},
|
||||
{'name': '-Available data sources', 'value': my_ds},
|
||||
{'name': '-ATT&CK data sources', 'value': ', '.join(technique['data_sources'])},
|
||||
{'name': '-Detection score', 'value': str(detection_score)},
|
||||
{'name': '-Detection location', 'value': location}]
|
||||
|
||||
mapped_techniques.append(x)
|
||||
|
||||
return mapped_techniques
|
Loading…
Reference in New Issue