diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..5172429
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ba6805a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,38 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Virtual Environment
+venv/
+ENV/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Application specific
+.frida_gui/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0ad25db
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are 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.
+
+ 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.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ 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 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 work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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 Affero 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 Affero 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 Affero 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.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ 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 AGPL, see
+.
diff --git a/README.md b/README.md
index 5729f98..47ba9b8 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,122 @@
-# Frida GUI Injector
-
-A graphical user interface for Frida script injection and app launching on Android devices, with special support for Nox emulator.
-
-
-
-## Features
-
-- Device scanning and selection
-- Application listing with package names
-- Automatic Frida server setup and management
-- Script injection during app launch
-- Support for Nox emulator
-- Real-time output logging
-
-## Prerequisites
-
-- Python 3.7+
-- ADB (Android Debug Bridge)
-- Rooted Android device or emulator
-- USB debugging enabled on target device
-
-## Installation
-
-1. Clone the repository:
+
+
+# FridaGUI
+
+A modern and powerful GUI tool for Frida script management and injection, created by **Oliver Stankiewicz**.
+
+## 🚀 Features
+- 🔍 **Script Injection with Live Preview**
+- 🌐 **CodeShare Browser & Integration**
+- ⭐ **Favorites System**
+- 📱 **Android/iOS Device Support**
+- 💻 **Process Management**
+- 🎨 **Modern Dark Theme UI**
+- 📊 **Real-time Process Monitoring**
+- 📝 **Script History Tracking**
+- 🔄 **Auto-injection Support**
+
+---
+
+## 📥 Installation
+
+### Prerequisites
+- **Python 3.8+**
+- **Frida**
+- **ADB** (for Android device support)
+
+### Steps
+1. Clone the repository:
+ ```bash
+ git clone https://github.com/oliverstankiewicz/FridaGUI.git
+ cd FridaGUI
+ ```
+
+2. Install dependencies:
+ ```bash
+ pip install -r requirements.txt
+ ```
+
+3. Run the application:
+ ```bash
+ python src/main.py
+ ```
+
+---
+
+## 📂 Project Structure
+```
+FridaGUI/
+├── src/
+│ ├── gui/
+│ │ ├── widgets/
+│ │ │ ├── device_panel.py
+│ │ │ ├── data_visualizer.py
+│ │ │ ├── history_page.py
+│ │ │ ├── injection_panel.py
+│ │ │ ├── output_panel.py
+│ │ │ ├── process_monitor.py
+│ │ │ ├── process_panel.py
+│ │ │ └── script_editor.py
+│ │ └── main_window.py
+│ ├── utils/
+│ │ └── themes.py
+│ └── main.py
+├── requirements.txt
+├── requirements-dev.txt
+├── LICENSE
+└── README.md
+```
+
+---
+
+## 🧩 Core Components
+
+### **Device Panel**
+- USB/Network device support
+- Android device detection
+- Frida server management
+- Process listing
+
+### **Script Editor**
+- Code editing with syntax highlighting
+- Script management and injection controls
+- Real-time output monitoring
+
+### **Process Monitor**
+- Real-time process list with filtering
+- Memory tracking and auto-refresh
+
+### **Data Visualizer**
+- Process data visualization
+- Memory usage graphs and performance metrics
+- Real-time updates
+
+### **History Page**
+- Script history and injection logs
+- Quick re-injection functionality
+- Session tracking
+
+---
+
+## 📜 Dependencies
+- Refer to the `requirements.txt` file for a complete list of dependencies.
+
+---
+
+## 📄 License
+This project is licensed under the **agplv3 License**. See the [LICENSE](LICENSE) file for details.
+
+---
+
+## 👤 Author
+**Oliver Stankiewicz**
+
+---
+
+## 🤝 Contributing
+Pull requests are welcome! For major changes, please open an issue first to discuss your ideas.
+
+---
+
+## 🛠️ Support
+If you encounter any issues or have questions, please file an issue on [GitHub](https://github.com/oliverstankiewicz/FridaGUI/issues).
diff --git a/image.png b/image.png
deleted file mode 100644
index 29321e8..0000000
Binary files a/image.png and /dev/null differ
diff --git a/main.py b/main.py
deleted file mode 100644
index ca10da9..0000000
--- a/main.py
+++ /dev/null
@@ -1,406 +0,0 @@
-import sys
-from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
- QHBoxLayout, QComboBox, QPushButton, QTextEdit,
- QLabel, QMessageBox, QCheckBox)
-from PyQt5.QtCore import Qt, QThread, pyqtSignal
-import frida
-import subprocess
-import time
-import os
-import requests
-import platform
-import lzma
-
-class FridaWorker(QThread):
- output_signal = pyqtSignal(str)
- error_signal = pyqtSignal(str)
-
- def __init__(self, action, **kwargs):
- super().__init__()
- self.action = action
- self.kwargs = kwargs
-
- def run(self):
- try:
- if self.action == "scan_devices":
- self._scan_devices()
- elif self.action == "inject":
- self._inject_script()
- elif self.action == "launch":
- self._launch_app()
- except Exception as e:
- self.error_signal.emit(str(e))
-
- def _scan_devices(self):
- try:
- # Kill and restart ADB server
- subprocess.run(['adb', 'kill-server'], capture_output=True)
- time.sleep(1)
- subprocess.run(['adb', 'start-server'], capture_output=True)
- time.sleep(2)
-
- # Try connecting to Nox
- nox_ports = ['62001', '62025', '62026', '62027', '62028', '62029']
- for port in nox_ports:
- try:
- subprocess.run(['adb', 'connect', f'127.0.0.1:{port}'],
- capture_output=True)
- except:
- continue
-
- time.sleep(2)
- self.output_signal.emit("Device scan completed")
- except Exception as e:
- self.error_signal.emit(f"Error scanning devices: {str(e)}")
-
- def _inject_script(self):
- try:
- device_id = self.kwargs['device_id']
- process_id = self.kwargs['process_id']
- script_content = self.kwargs['script']
-
- # Special handling for Nox
- if '127.0.0.1' in device_id:
- # Ensure frida-server is running
- try:
- subprocess.run(['adb', '-s', device_id, 'shell', 'su -c "killall -9 frida-server"'],
- capture_output=True)
- time.sleep(1)
- subprocess.Popen(['adb', '-s', device_id, 'shell', 'su -c "/data/local/tmp/frida-server &"'],
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- self.output_signal.emit("Restarted frida-server on Nox")
- time.sleep(3)
- except Exception as e:
- self.error_signal.emit(f"Error restarting frida-server: {str(e)}")
-
- # Try to attach multiple times
- max_retries = 3
- last_error = None
-
- for i in range(max_retries):
- try:
- device = frida.get_device(device_id)
- session = device.attach(int(process_id))
- script = session.create_script(script_content)
-
- def on_message(message, data):
- if message['type'] == 'send':
- self.output_signal.emit(f"Script message: {message['payload']}")
- elif message['type'] == 'error':
- self.error_signal.emit(f"Script error: {message['description']}")
-
- script.on('message', on_message)
- script.load()
- self.output_signal.emit("Script injected successfully")
- return
- except Exception as e:
- last_error = str(e)
- self.output_signal.emit(f"Injection attempt {i+1} failed, retrying...")
- time.sleep(2)
- continue
-
- raise Exception(f"Failed after {max_retries} attempts. Last error: {last_error}")
-
- except Exception as e:
- self.error_signal.emit(f"Injection error: {str(e)}")
-
- def _launch_app(self):
- try:
- device_id = self.kwargs['device_id']
- package_name = self.kwargs['package_name']
- script_content = self.kwargs['script']
-
- # Special handling for Nox
- if '127.0.0.1' in device_id:
- try:
- # Setup frida-server first
- if not self.setup_frida_server(device_id):
- raise Exception("Failed to setup frida-server")
-
- # Ensure connection to Nox
- subprocess.run(['adb', 'connect', device_id], capture_output=True)
- time.sleep(1)
-
- # Start frida-server
- subprocess.run(['adb', '-s', device_id, 'shell', 'su -c "killall -9 frida-server"'],
- capture_output=True)
- time.sleep(1)
- subprocess.Popen(['adb', '-s', device_id, 'shell', 'su -c "/data/local/tmp/frida-server &"'],
- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- self.output_signal.emit("Started frida-server on Nox")
- time.sleep(3)
-
- # Kill existing app
- subprocess.run(['adb', '-s', device_id, 'shell', 'am force-stop ' + package_name],
- capture_output=True)
- time.sleep(1)
-
- # Get device and spawn app
- device = frida.get_device(device_id)
- pid = device.spawn([package_name])
- self.output_signal.emit(f"Spawned app with PID: {pid}")
-
- # Attach to the spawned process
- session = device.attach(pid)
-
- # Create and load script
- script = session.create_script(script_content)
-
- def on_message(message, data):
- if message['type'] == 'send':
- self.output_signal.emit(f"Script message: {message['payload']}")
- elif message['type'] == 'error':
- self.error_signal.emit(f"Script error: {message['description']}")
-
- script.on('message', on_message)
- script.load()
- self.output_signal.emit("Script loaded")
-
- # Resume the app
- device.resume(pid)
- self.output_signal.emit("App resumed with injected script")
-
- except Exception as e:
- raise Exception(f"Nox launch failed: {str(e)}")
-
- self.output_signal.emit("App launched successfully with script")
- except Exception as e:
- self.error_signal.emit(f"Launch error: {str(e)}")
-
- def download_frida_server(self):
- try:
- # Get device architecture
- device_id = self.kwargs.get('device_id', '')
- abi = subprocess.check_output(
- ['adb', '-s', device_id, 'shell', 'getprop', 'ro.product.cpu.abi'],
- text=True
- ).strip()
-
- # Map Android ABI to Frida architecture
- abi_to_arch = {
- 'arm64-v8a': 'arm64',
- 'armeabi-v7a': 'arm',
- 'x86': 'x86',
- 'x86_64': 'x86_64'
- }
- arch = abi_to_arch.get(abi, 'arm64')
-
- # Get latest Frida release version
- response = requests.get('https://api.github.com/repos/frida/frida/releases/latest')
- latest_version = response.json()['tag_name']
-
- # Construct download URL
- download_url = f'https://github.com/frida/frida/releases/download/{latest_version}/frida-server-{latest_version[0:]}-android-{arch}.xz'
- self.output_signal.emit(f"Downloading Frida server from: {download_url}")
-
- # Download frida-server
- response = requests.get(download_url)
- response.raise_for_status()
-
- # Decompress XZ file
- decompressed_data = lzma.decompress(response.content)
-
- # Save as frida-server in current directory
- frida_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'frida-server')
- with open(frida_path, 'wb') as f:
- f.write(decompressed_data)
-
- # Make executable
- os.chmod(frida_path, 0o755)
-
- self.output_signal.emit(f"Successfully downloaded frida-server {latest_version}")
- return frida_path
- except Exception as e:
- self.error_signal.emit(f"Error downloading frida-server: {str(e)}")
- return None
-
- def setup_frida_server(self, device_id):
- try:
- # Check if frida-server exists locally
- frida_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'frida-server')
- if not os.path.exists(frida_path):
- self.output_signal.emit("Downloading frida-server...")
- frida_path = self.download_frida_server()
- if not frida_path:
- raise Exception("Failed to download frida-server")
-
- # Push frida-server to device
- self.output_signal.emit("Pushing frida-server to device...")
- result = subprocess.run(
- ['adb', '-s', device_id, 'push', frida_path, '/data/local/tmp/'],
- capture_output=True,
- text=True
- )
-
- if "error" in result.stderr.lower():
- # Try with su if normal push fails
- self.output_signal.emit("Trying with root permissions...")
- subprocess.run(['adb', '-s', device_id, 'shell', 'su -c "mount -o rw,remount /system"'])
- subprocess.run(['adb', '-s', device_id, 'shell', 'su -c "chmod 777 /data/local/tmp"'])
- subprocess.run(['adb', '-s', device_id, 'push', frida_path, '/data/local/tmp/'])
-
- # Set permissions
- subprocess.run(['adb', '-s', device_id, 'shell', 'su -c "chmod 755 /data/local/tmp/frida-server"'])
-
- self.output_signal.emit("Frida server setup completed")
- return True
- except Exception as e:
- self.error_signal.emit(f"Error setting up frida-server: {str(e)}")
- return False
-
-class FridaGUI(QMainWindow):
- def __init__(self):
- super().__init__()
- self.setWindowTitle("Frida Script Injector")
- self.setMinimumSize(800, 600)
-
- # Create main widget and layout
- main_widget = QWidget()
- self.setCentralWidget(main_widget)
- layout = QVBoxLayout(main_widget)
-
- # Device selection
- device_layout = QHBoxLayout()
- self.device_combo = QComboBox()
- scan_button = QPushButton("Scan Devices")
- scan_button.clicked.connect(self.scan_devices)
- device_layout.addWidget(QLabel("Select Device:"))
- device_layout.addWidget(self.device_combo)
- device_layout.addWidget(scan_button)
- layout.addLayout(device_layout)
-
- # Process selection
- process_layout = QHBoxLayout()
- self.process_combo = QComboBox()
- list_button = QPushButton("List Processes")
- list_button.clicked.connect(self.list_processes)
- process_layout.addWidget(QLabel("Select Process:"))
- process_layout.addWidget(self.process_combo)
- process_layout.addWidget(list_button)
- layout.addLayout(process_layout)
-
- # Launch button
- launch_button = QPushButton("Launch App")
- launch_button.clicked.connect(self.launch_app)
- layout.addWidget(launch_button)
-
- # Launch with inject checkbox
- self.inject_on_launch = QCheckBox("Inject on Launch")
- self.inject_on_launch.setChecked(True) # Enable by default
- layout.addWidget(self.inject_on_launch)
-
- # Script editor
- layout.addWidget(QLabel("Frida Script:"))
- self.script_editor = QTextEdit()
- self.script_editor.setPlainText('''Java.perform(function() {
- console.log("Loaded!");
- // Enable SSL logging
- var modules = Process.enumerateModules();
- modules.forEach(function(module) {
- if (module.name.indexOf(".so") !== -1) {
- console.log("Module " + module.name + " SSL logging started.");
- }
- });
-});''')
- layout.addWidget(self.script_editor)
-
- # Output area
- layout.addWidget(QLabel("Output:"))
- self.output_area = QTextEdit()
- self.output_area.setReadOnly(True)
- layout.addWidget(self.output_area)
-
- # Initial device scan
- self.scan_devices()
-
- def log_output(self, message):
- self.output_area.append(message)
-
- def scan_devices(self):
- self.worker = FridaWorker("scan_devices")
- self.worker.output_signal.connect(self.log_output)
- self.worker.error_signal.connect(self.log_output)
- self.worker.finished.connect(self._update_devices)
- self.worker.start()
-
- def _update_devices(self):
- try:
- self.device_combo.clear()
- devices = frida.enumerate_devices()
- for device in devices:
- self.device_combo.addItem(f"{device.name} ({device.type})", device.id)
- except Exception as e:
- self.log_output(f"Error updating devices: {str(e)}")
-
- def list_processes(self):
- try:
- device_id = self.device_combo.currentData()
- if not device_id:
- raise Exception("Please select a device first")
-
- device = frida.get_device(device_id)
-
- # Get installed packages instead of processes
- packages = []
- adb_output = subprocess.check_output(
- ['adb', '-s', device_id, 'shell', 'pm', 'list', 'packages', '-f'],
- text=True
- ).strip().split('\n')
-
- self.process_combo.clear()
- for line in adb_output:
- if line:
- # Extract package name from line
- package = line.split('=')[-1]
- # Get app name using aapt
- try:
- app_path = line.split(':')[1].split('=')[0]
- aapt_output = subprocess.check_output(
- ['adb', '-s', device_id, 'shell', 'dumpsys', 'package', package],
- text=True
- )
- app_name = package # Default to package name
- for line in aapt_output.split('\n'):
- if 'applicationInfo' in line and 'label=' in line:
- app_name = line.split('label=')[1].split(' ')[0]
- break
-
- self.process_combo.addItem(f"{app_name} ({package})", package)
- except:
- # If we can't get the app name, just show package name
- self.process_combo.addItem(package, package)
-
- self.log_output("Applications listed successfully")
- except Exception as e:
- self.log_output(f"Error listing applications: {str(e)}")
-
- def launch_app(self):
- try:
- device_id = self.device_combo.currentData()
- package_name = self.process_combo.currentData()
- script = self.script_editor.toPlainText() if self.inject_on_launch.isChecked() else ""
-
- if not device_id or not package_name:
- raise Exception("Please select device and application first")
-
- if self.inject_on_launch.isChecked() and not script:
- raise Exception("Please provide a script to inject")
-
- self.worker = FridaWorker("launch",
- device_id=device_id,
- package_name=package_name,
- script=script)
- self.worker.output_signal.connect(self.log_output)
- self.worker.error_signal.connect(self.log_output)
- self.worker.start()
- except Exception as e:
- self.log_output(f"Error launching app: {str(e)}")
-
-def main():
- app = QApplication(sys.argv)
- window = FridaGUI()
- window.show()
- sys.exit(app.exec())
-
-if __name__ == '__main__':
- main()
diff --git a/requirements.txt b/requirements.txt
index 1821c8b..b8374a7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,7 @@
-frida-tools>=11.0.0
-frida>=16.0.19
-PyQt5==5.15.9
-requests>=2.26.0
\ No newline at end of file
+frida
+PyQt5
+qtawesome
+requests
+beautifulsoup4
+psutil
+aiohttp
\ No newline at end of file
diff --git a/src/core/__pycache__/android_helper.cpython-311.pyc b/src/core/__pycache__/android_helper.cpython-311.pyc
new file mode 100644
index 0000000..2a9ae0d
Binary files /dev/null and b/src/core/__pycache__/android_helper.cpython-311.pyc differ
diff --git a/src/core/__pycache__/history_manager.cpython-311.pyc b/src/core/__pycache__/history_manager.cpython-311.pyc
new file mode 100644
index 0000000..0dd5d41
Binary files /dev/null and b/src/core/__pycache__/history_manager.cpython-311.pyc differ
diff --git a/src/core/__pycache__/process_monitor.cpython-311.pyc b/src/core/__pycache__/process_monitor.cpython-311.pyc
new file mode 100644
index 0000000..4eddac0
Binary files /dev/null and b/src/core/__pycache__/process_monitor.cpython-311.pyc differ
diff --git a/src/core/__pycache__/script_history.cpython-311.pyc b/src/core/__pycache__/script_history.cpython-311.pyc
new file mode 100644
index 0000000..572ebb6
Binary files /dev/null and b/src/core/__pycache__/script_history.cpython-311.pyc differ
diff --git a/src/core/__pycache__/script_templates.cpython-311.pyc b/src/core/__pycache__/script_templates.cpython-311.pyc
new file mode 100644
index 0000000..b495ef9
Binary files /dev/null and b/src/core/__pycache__/script_templates.cpython-311.pyc differ
diff --git a/src/core/android_helper.py b/src/core/android_helper.py
new file mode 100644
index 0000000..cc4ecb9
--- /dev/null
+++ b/src/core/android_helper.py
@@ -0,0 +1,175 @@
+import subprocess
+import os
+import time
+import requests
+import platform
+
+class AndroidHelper:
+ FRIDA_SERVER_URL = "https://github.com/frida/frida/releases/download/16.1.4/frida-server-16.1.4-android-arm64.xz"
+
+ @staticmethod
+ def get_adb_path():
+ """Get the ADB executable path"""
+ if platform.system() == "Windows":
+ return "adb.exe"
+ return "adb"
+
+ @staticmethod
+ def is_device_connected(device_id):
+ """Check if device is connected"""
+ try:
+ output = subprocess.check_output([AndroidHelper.get_adb_path(), 'devices'], text=True)
+ return device_id in output
+ except:
+ return False
+
+ @staticmethod
+ def get_device_arch(device_id):
+ """Get device architecture"""
+ try:
+ output = subprocess.check_output([
+ AndroidHelper.get_adb_path(), '-s', device_id, 'shell', 'getprop ro.product.cpu.abi'
+ ], text=True).strip()
+
+ if 'arm64' in output:
+ return 'arm64'
+ elif 'arm' in output:
+ return 'arm'
+ elif 'x86_64' in output:
+ return 'x86_64'
+ elif 'x86' in output:
+ return 'x86'
+ return 'arm64' # Default to arm64
+ except:
+ return 'arm64'
+
+ @staticmethod
+ def start_frida_server(device_id):
+ """Start frida-server on device"""
+ try:
+ adb = AndroidHelper.get_adb_path()
+
+ # Get device architecture
+ arch = AndroidHelper.get_device_arch(device_id)
+ server_url = f"https://github.com/frida/frida/releases/download/16.1.4/frida-server-16.1.4-android-{arch}.xz"
+
+ # First, try to get root access
+ subprocess.run([adb, '-s', device_id, 'root'])
+ time.sleep(1) # Wait for root to take effect
+
+ # Remount system as read-write
+ subprocess.run([adb, '-s', device_id, 'remount'])
+
+ # Download and push frida-server (always get fresh copy)
+ print(f"Downloading frida-server for {arch}...")
+ response = requests.get(server_url)
+ server_path = os.path.join(os.path.expanduser('~'), '.frida_gui', f'frida-server-{arch}')
+
+ # Create directory if it doesn't exist
+ os.makedirs(os.path.dirname(server_path), exist_ok=True)
+
+ # Save and extract
+ with open(server_path + '.xz', 'wb') as f:
+ f.write(response.content)
+
+ try:
+ subprocess.run(['xz', '-d', '-f', server_path + '.xz']) # Force extraction
+ except:
+ print("Error extracting with xz, trying alternative method...")
+ import lzma
+ with lzma.open(server_path + '.xz') as f:
+ with open(server_path, 'wb') as out:
+ out.write(f.read())
+
+ # Push to device
+ print("Pushing frida-server to device...")
+ subprocess.run([
+ adb, '-s', device_id, 'push',
+ server_path, '/data/local/tmp/frida-server'
+ ])
+
+ # Kill any existing frida-server processes
+ kill_commands = [
+ 'pkill -f frida-server',
+ 'killall -9 frida-server',
+ 'kill $(pidof frida-server)',
+ ]
+
+ for cmd in kill_commands:
+ subprocess.run([adb, '-s', device_id, 'shell', cmd], stderr=subprocess.PIPE)
+
+ # Set permissions and start server
+ start_commands = [
+ 'chmod 755 /data/local/tmp/frida-server',
+ 'su -c "chmod 755 /data/local/tmp/frida-server"',
+ 'su -c "setenforce 0"',
+ 'su -c "/data/local/tmp/frida-server -D"', # Run in daemon mode
+ '/data/local/tmp/frida-server -D' # Fallback without su
+ ]
+
+ for cmd in start_commands:
+ try:
+ subprocess.run([
+ adb, '-s', device_id, 'shell', cmd
+ ], stderr=subprocess.PIPE, timeout=5)
+ time.sleep(1)
+ if AndroidHelper.is_frida_running(device_id):
+ print("Frida server started successfully")
+ return True
+ except subprocess.TimeoutExpired:
+ # This might actually be good - server could be running
+ if AndroidHelper.is_frida_running(device_id):
+ print("Frida server started successfully")
+ return True
+ except:
+ continue
+
+ print("Failed to start frida-server")
+ return False
+
+ except Exception as e:
+ print(f"Error starting frida-server: {e}")
+ return False
+
+ @staticmethod
+ def is_frida_running(device_id):
+ """Check if frida-server is running on device"""
+ try:
+ # Try different ps commands as they vary by Android version
+ commands = [
+ 'ps -A | grep frida-server',
+ 'ps -ef | grep frida-server',
+ 'ps | grep frida-server',
+ 'top -n 1 | grep frida-server',
+ 'pidof frida-server'
+ ]
+
+ for cmd in commands:
+ try:
+ output = subprocess.check_output(
+ [AndroidHelper.get_adb_path(), '-s', device_id, 'shell', cmd],
+ text=True,
+ stderr=subprocess.PIPE,
+ timeout=2
+ )
+ if ('frida-server' in output and 'grep' not in output) or output.strip().isdigit():
+ return True
+ except:
+ continue
+
+ # Try netstat as last resort
+ try:
+ output = subprocess.check_output(
+ [AndroidHelper.get_adb_path(), '-s', device_id, 'shell', 'netstat -tlnp'],
+ text=True,
+ stderr=subprocess.PIPE,
+ timeout=2
+ )
+ if ':27042' in output: # Default frida port
+ return True
+ except:
+ pass
+
+ return False
+ except:
+ return False
\ No newline at end of file
diff --git a/src/core/history_manager.py b/src/core/history_manager.py
new file mode 100644
index 0000000..1c3fd02
--- /dev/null
+++ b/src/core/history_manager.py
@@ -0,0 +1,53 @@
+from datetime import datetime
+import json
+import os
+from collections import deque
+import weakref
+
+class HistoryManager:
+ def __init__(self):
+ self.history_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'history.json')
+ self._history = deque(maxlen=1000) # Limit history size
+ self.load_history()
+
+ def load_history(self):
+ try:
+ if os.path.exists(self.history_file):
+ with open(self.history_file, 'r') as f:
+ # Load directly into deque with max size
+ data = json.load(f)
+ self._history.extend(data[-1000:]) # Only keep last 1000 entries
+ except Exception as e:
+ print(f"Error loading history: {e}")
+
+ def save_history(self):
+ try:
+ os.makedirs(os.path.dirname(self.history_file), exist_ok=True)
+ with open(self.history_file, 'w') as f:
+ # Convert deque to list for JSON serialization
+ json.dump(list(self._history), f, indent=2)
+ except Exception as e:
+ print(f"Error saving history: {e}")
+
+ def add_entry(self, action_type, details):
+ entry = {
+ 'timestamp': datetime.now().isoformat(),
+ 'type': action_type,
+ 'details': details
+ }
+ self._history.appendleft(entry) # Use deque's appendleft
+
+ # Periodically save to prevent memory buildup
+ if len(self._history) % 10 == 0: # Save every 10 entries
+ self.save_history()
+
+ def clear_history(self):
+ self._history.clear()
+ self.save_history()
+
+ @property
+ def history(self):
+ return list(self._history) # Return a copy to prevent memory leaks
+
+ def __del__(self):
+ self.save_history()
\ No newline at end of file
diff --git a/src/core/process_monitor.py b/src/core/process_monitor.py
new file mode 100644
index 0000000..deedb96
--- /dev/null
+++ b/src/core/process_monitor.py
@@ -0,0 +1,67 @@
+from PyQt5.QtCore import QObject, pyqtSignal, QTimer
+import psutil
+import frida
+import weakref
+
+class ProcessMonitor(QObject):
+ process_started = pyqtSignal(str, int) # name, pid
+ process_ended = pyqtSignal(str, int) # name, pid
+ memory_updated = pyqtSignal(str, float) # pid, memory_usage
+
+ def __init__(self, refresh_rate=1000):
+ super().__init__()
+ self.refresh_rate = refresh_rate
+ self.monitored_processes = {}
+ self.timer = QTimer()
+ self.timer.timeout.connect(self.check_processes)
+ self._stopped = False
+
+ def start_monitoring(self):
+ self._stopped = False
+ self.timer.start(self.refresh_rate)
+
+ def stop_monitoring(self):
+ self._stopped = True
+ self.timer.stop()
+ self.monitored_processes.clear() # Clear the dictionary
+
+ def check_processes(self):
+ if self._stopped:
+ return
+
+ current_processes = {}
+
+ try:
+ device = frida.get_local_device()
+ # Use iterator instead of list comprehension
+ for process in device.enumerate_processes():
+ if self._stopped:
+ return
+
+ current_processes[process.pid] = process.name
+
+ # New process
+ if process.pid not in self.monitored_processes:
+ self.process_started.emit(process.name, process.pid)
+
+ # Update memory usage
+ try:
+ p = psutil.Process(process.pid)
+ memory_mb = p.memory_info().rss / 1024 / 1024
+ self.memory_updated.emit(str(process.pid), memory_mb)
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ continue
+
+ # Check for ended processes - use iterator
+ for pid in list(self.monitored_processes.keys()):
+ if pid not in current_processes:
+ name = self.monitored_processes[pid]
+ self.process_ended.emit(name, pid)
+
+ self.monitored_processes = current_processes
+
+ except Exception as e:
+ print(f"Error monitoring processes: {str(e)}")
+
+ def __del__(self):
+ self.stop_monitoring()
\ No newline at end of file
diff --git a/src/core/script_history.py b/src/core/script_history.py
new file mode 100644
index 0000000..bff4756
--- /dev/null
+++ b/src/core/script_history.py
@@ -0,0 +1,76 @@
+import json
+import os
+from datetime import datetime
+
+class ScriptHistory:
+ def __init__(self):
+ self.base_dir = os.path.join(os.path.expanduser('~'), '.frida_gui')
+ self.history_file = os.path.join(self.base_dir, 'script_history.json')
+ self.favorites_file = os.path.join(self.base_dir, 'favorites.json')
+ self.ensure_dirs()
+ self.load_history()
+
+ def ensure_dirs(self):
+ os.makedirs(self.base_dir, exist_ok=True)
+
+ def load_history(self):
+ try:
+ if os.path.exists(self.history_file):
+ with open(self.history_file, 'r') as f:
+ self.history = json.load(f)
+ else:
+ self.history = {
+ 'local': [],
+ 'codeshare': [],
+ 'favorites': []
+ }
+ except Exception as e:
+ print(f"Error loading history: {e}")
+ self.history = {'local': [], 'codeshare': [], 'favorites': []}
+
+ def save_history(self):
+ try:
+ with open(self.history_file, 'w') as f:
+ json.dump(self.history, f, indent=2)
+ except Exception as e:
+ print(f"Error saving history: {e}")
+
+ def add_to_history(self, script_type, script_info):
+ """Add script to history with timestamp"""
+ entry = {
+ 'timestamp': datetime.now().isoformat(),
+ 'info': script_info
+ }
+
+ # Keep only last 50 entries
+ self.history[script_type] = ([entry] +
+ [x for x in self.history[script_type]
+ if x['info'].get('id') != script_info.get('id')])[:50]
+ self.save_history()
+
+ def add_to_favorites(self, script_info):
+ """Add script to favorites"""
+ if script_info not in self.history['favorites']:
+ self.history['favorites'].append(script_info)
+ self.save_history()
+
+ def remove_from_favorites(self, script_id):
+ """Remove script from favorites"""
+ self.history['favorites'] = [
+ x for x in self.history['favorites']
+ if x.get('id') != script_id
+ ]
+ self.save_history()
+
+ def get_recent_scripts(self, script_type, limit=10):
+ """Get recent scripts of specified type"""
+ return self.history[script_type][:limit]
+
+ def get_favorites(self):
+ """Get all favorite scripts"""
+ return self.history['favorites']
+
+ def is_favorite(self, script_id):
+ """Check if script is in favorites"""
+ return any(x.get('id') == script_id
+ for x in self.history['favorites'])
\ No newline at end of file
diff --git a/src/core/script_manager.py b/src/core/script_manager.py
new file mode 100644
index 0000000..9607ab2
--- /dev/null
+++ b/src/core/script_manager.py
@@ -0,0 +1,74 @@
+from PyQt5.QtCore import QObject, pyqtSignal
+from pygments import highlight
+from pygments.lexers import JavascriptLexer
+from pygments.formatters import HtmlFormatter
+from cryptography.fernet import Fernet
+import json
+import os
+
+class ScriptManager(QObject):
+ script_loaded = pyqtSignal(str, str) # name, content
+ script_saved = pyqtSignal(str) # name
+
+ def __init__(self):
+ super().__init__()
+ self.scripts_dir = os.path.join(os.path.expanduser('~'), '.frida_gui', 'scripts')
+ self.key = Fernet.generate_key()
+ self.cipher_suite = Fernet(self.key)
+ self._ensure_dirs()
+
+ def _ensure_dirs(self):
+ os.makedirs(self.scripts_dir, exist_ok=True)
+
+ def save_script(self, name, content, encrypt=False):
+ """Save script with optional encryption"""
+ script_path = os.path.join(self.scripts_dir, f"{name}.js")
+ metadata_path = f"{script_path}.meta"
+
+ if encrypt:
+ content = self.cipher_suite.encrypt(content.encode()).decode()
+
+ with open(script_path, 'w') as f:
+ f.write(content)
+
+ metadata = {
+ 'name': name,
+ 'encrypted': encrypt,
+ 'tags': [],
+ 'description': '',
+ 'version': '1.0'
+ }
+
+ with open(metadata_path, 'w') as f:
+ json.dump(metadata, f)
+
+ self.script_saved.emit(name)
+
+ def load_script(self, name):
+ """Load script and handle decryption if needed"""
+ script_path = os.path.join(self.scripts_dir, f"{name}.js")
+ metadata_path = f"{script_path}.meta"
+
+ try:
+ with open(metadata_path, 'r') as f:
+ metadata = json.load(f)
+
+ with open(script_path, 'r') as f:
+ content = f.read()
+
+ if metadata.get('encrypted', False):
+ content = self.cipher_suite.decrypt(content.encode()).decode()
+
+ self.script_loaded.emit(name, content)
+ return content
+ except Exception as e:
+ print(f"Error loading script: {str(e)}")
+ return None
+
+ def get_highlighted_script(self, content):
+ """Return HTML-formatted highlighted script"""
+ return highlight(
+ content,
+ JavascriptLexer(),
+ HtmlFormatter(style='monokai')
+ )
\ No newline at end of file
diff --git a/src/core/script_templates.py b/src/core/script_templates.py
new file mode 100644
index 0000000..840b9f3
--- /dev/null
+++ b/src/core/script_templates.py
@@ -0,0 +1,94 @@
+SCRIPT_TEMPLATES = {
+ 'API_LOGGING': '''
+Java.perform(function() {
+ // Common Android API hooks
+ var HttpURLConnection = Java.use('java.net.HttpURLConnection');
+ var OkHttpClient = Java.use('okhttp3.OkHttpClient');
+ var Retrofit = Java.use('retrofit2.Retrofit');
+
+ // HTTP URL Connection
+ HttpURLConnection.connect.implementation = function() {
+ console.log('[+] HttpURLConnection.connect() called');
+ console.log('URL: ' + this.getURL().toString());
+ console.log('Method: ' + this.getRequestMethod());
+ this.connect();
+ };
+
+ // OkHttp
+ OkHttpClient.newCall.implementation = function(request) {
+ console.log('[+] OkHttpClient.newCall() intercepted');
+ console.log('URL: ' + request.url().toString());
+ console.log('Method: ' + request.method());
+ console.log('Headers: ' + request.headers().toString());
+ return this.newCall(request);
+ };
+
+ // Retrofit
+ Retrofit.create.implementation = function(service) {
+ console.log('[+] Retrofit API Service created');
+ console.log('Service: ' + service.toString());
+ return this.create(service);
+ };
+});
+''',
+
+ 'SSL_PINNING_BYPASS': '''
+Java.perform(function() {
+ var TrustManager = Java.registerClass({
+ name: 'com.custom.TrustManager',
+ implements: [Java.use('javax.net.ssl.X509TrustManager')],
+ methods: {
+ checkClientTrusted: function(chain, authType) {},
+ checkServerTrusted: function(chain, authType) {},
+ getAcceptedIssuers: function() { return []; }
+ }
+ });
+
+ var SSLContext = Java.use('javax.net.ssl.SSLContext');
+ SSLContext.init.implementation = function(keyManager, trustManager, secureRandom) {
+ console.log('[+] Bypassing SSL Pinning');
+ var trustManagers = [TrustManager.$new()];
+ this.init(keyManager, trustManagers, secureRandom);
+ };
+});
+''',
+
+ 'APP_INFO': '''
+Java.perform(function() {
+ var ActivityThread = Java.use('android.app.ActivityThread');
+ var Context = Java.use('android.content.Context');
+
+ var currentApplication = ActivityThread.currentApplication();
+ var context = currentApplication.getApplicationContext();
+
+ console.log('\\n[App Information]');
+ console.log('Package Name:', context.getPackageName());
+ console.log('Process Name:', ActivityThread.currentProcessName());
+ console.log('App Version:', context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName.value);
+ console.log('Target SDK:', context.getApplicationInfo().targetSdkVersion.value);
+
+ // List all activities
+ var packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),
+ Java.use('android.content.pm.PackageManager').GET_ACTIVITIES.value);
+ console.log('\\n[Activities]');
+ packageInfo.activities.value.forEach(function(activity) {
+ console.log(activity.name.value);
+ });
+});
+''',
+
+ 'CUSTOM_API_LOGGER': '''
+Java.perform(function() {
+ // Add your custom API class/method hooks here
+ var targetClass = Java.use('com.example.api.ServiceClass');
+
+ targetClass.apiMethod.implementation = function() {
+ console.log('[+] API Call Intercepted');
+ console.log('Arguments:', arguments);
+ var result = this.apiMethod.apply(this, arguments);
+ console.log('Result:', result);
+ return result;
+ };
+});
+'''
+}
\ No newline at end of file
diff --git a/src/gui/__init__.py b/src/gui/__init__.py
new file mode 100644
index 0000000..166ce0b
--- /dev/null
+++ b/src/gui/__init__.py
@@ -0,0 +1 @@
+# Empty file to make the directory a Python package
\ No newline at end of file
diff --git a/src/gui/__pycache__/__init__.cpython-311.pyc b/src/gui/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..9f4b74e
Binary files /dev/null and b/src/gui/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/gui/__pycache__/main_window.cpython-311.pyc b/src/gui/__pycache__/main_window.cpython-311.pyc
new file mode 100644
index 0000000..8226c06
Binary files /dev/null and b/src/gui/__pycache__/main_window.cpython-311.pyc differ
diff --git a/src/gui/main_window.py b/src/gui/main_window.py
new file mode 100644
index 0000000..9714485
--- /dev/null
+++ b/src/gui/main_window.py
@@ -0,0 +1,811 @@
+from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
+ QPushButton, QStackedWidget, QLabel, QListWidget, QTableWidget, QGroupBox, QCheckBox, QSpinBox, QMessageBox, QScrollArea, QGridLayout, QLineEdit, QTextEdit, QFrame, QDialog, QFileDialog)
+from PyQt5.QtCore import Qt, QSize
+import qtawesome as qta
+from .widgets.device_panel import DevicePanel
+from .widgets.process_panel import ProcessPanel
+from .widgets.script_editor import ScriptEditorPanel
+from .widgets.output_panel import OutputPanel
+from .widgets.codeshare_browser import CodeShareBrowser
+from .widgets.app_launcher import AppLauncher
+from .widgets.process_monitor import ProcessMonitor
+from .widgets.injection_panel import InjectionPanel
+from .widgets.device_selector import DeviceSelector
+from .widgets.history_page import HistoryPage
+from core.history_manager import HistoryManager
+from core.android_helper import AndroidHelper
+import frida
+import subprocess
+import os
+import json
+import requests
+
+class FridaInjectorMainWindow(QMainWindow):
+ def __init__(self):
+ super().__init__()
+ self.setWindowTitle("Oliver Stankiewicz's | Frida Script Manager")
+ self.setMinimumSize(1400, 800)
+ self.history_manager = HistoryManager()
+ self.favorites = [] # Initialize favorites list
+ self.load_favorites() # Load favorites on startup
+ self.setup_ui()
+
+ # Connect codeshare and favorites browsers
+ self.codeshare_browser.favorites_updated.connect(self.refresh_favorites)
+
+ def setup_ui(self):
+ central_widget = QWidget()
+ self.setCentralWidget(central_widget)
+
+ # Main horizontal layout
+ layout = QHBoxLayout(central_widget)
+
+ # Left sidebar for navigation
+ sidebar = self.create_sidebar()
+ layout.addWidget(sidebar)
+
+ # Stacked widget for main content
+ self.stack = QStackedWidget()
+ layout.addWidget(self.stack)
+
+ # Set layout ratio (1:4)
+ layout.setStretch(0, 1)
+ layout.setStretch(1, 4)
+
+ # Initialize pages
+ self.init_pages()
+
+ def create_sidebar(self):
+ sidebar = QWidget()
+ sidebar.setObjectName("sidebar")
+ sidebar.setStyleSheet("""
+ QWidget#sidebar {
+ background-color: #2f3136;
+ border-right: 1px solid #202225;
+ min-width: 180px;
+ max-width: 180px;
+ }
+ QPushButton {
+ text-align: left;
+ padding: 6px 8px;
+ border: none;
+ border-radius: 4px;
+ margin: 1px 4px;
+ min-height: 32px;
+ max-height: 32px;
+ font-size: 13px;
+ }
+ QPushButton:hover {
+ background-color: #36393f;
+ }
+ QPushButton:checked {
+ background-color: #404249;
+ }
+ """)
+
+ layout = QVBoxLayout(sidebar)
+ layout.setSpacing(1)
+ layout.setContentsMargins(0, 5, 0, 5)
+
+ # Add navigation buttons
+ self.nav_buttons = {}
+
+ nav_items = [
+ ("home", "Home", "fa5s.home"),
+ ("inject", "Script Injection", "fa5s.syringe"),
+ ("codeshare", "CodeShare", "fa5s.cloud-download-alt"),
+ ("favorites", "Favorites", "fa5s.star"),
+ ("history", "History", "fa5s.history"),
+ ("monitor", "Process Monitor", "fa5s.desktop"),
+ ("settings", "Settings", "fa5s.cog")
+ ]
+
+ for id_, text, icon in nav_items:
+ btn = QPushButton(qta.icon(icon, color='#b9bbbe'), f" {text}")
+ btn.setCheckable(True)
+ btn.clicked.connect(lambda checked, x=id_: self.switch_page(x))
+ # Set icon size
+ btn.setIconSize(QSize(14, 14))
+ self.nav_buttons[id_] = btn
+ layout.addWidget(btn)
+
+ layout.addStretch()
+
+ # Add status indicator at bottom
+ status_layout = QHBoxLayout()
+ status_layout.setContentsMargins(8, 4, 8, 4)
+ self.status_icon = QLabel()
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(8, 8))
+ self.status_text = QLabel("Ready")
+ self.status_text.setStyleSheet("color: #b9bbbe; font-size: 12px;")
+ status_layout.addWidget(self.status_icon)
+ status_layout.addWidget(self.status_text)
+ layout.addLayout(status_layout)
+
+ return sidebar
+
+ def init_pages(self):
+ # Create pages
+ self.pages = {
+ 'home': self.create_home_page(),
+ 'inject': self.create_injection_page(),
+ 'codeshare': self.create_codeshare_page(),
+ 'favorites': self.create_favorites_page(),
+ 'history': self.create_history_page(),
+ 'monitor': self.create_monitor_page(),
+ 'settings': self.create_settings_page()
+ }
+
+ # Add pages to stack
+ for page in self.pages.values():
+ self.stack.addWidget(page)
+
+ # Set initial page
+ self.switch_page('home')
+
+ def switch_page(self, page_id):
+ # Update button states
+ for btn in self.nav_buttons.values():
+ btn.setChecked(False)
+ self.nav_buttons[page_id].setChecked(True)
+
+ # Switch to page
+ self.stack.setCurrentWidget(self.pages[page_id])
+
+ def create_home_page(self):
+ page = QWidget()
+ layout = QVBoxLayout(page)
+ layout.setSpacing(20)
+
+ # Welcome header
+ header = QFrame()
+ header.setStyleSheet("""
+ QFrame {
+ background-color: #2f3136;
+ border-radius: 10px;
+ padding: 20px;
+ }
+ QLabel {
+ color: white;
+ }
+ """)
+ header_layout = QVBoxLayout(header)
+
+ title = QLabel("Welcome to Frida Script Manager")
+ title.setStyleSheet("font-size: 24px; font-weight: bold;")
+
+ subtitle = QLabel("A powerful GUI tool for Frida script management and injection")
+ subtitle.setStyleSheet("font-size: 16px; color: #b9bbbe;")
+
+ author = QLabel("Created by Oliver Stankiewicz")
+ author.setStyleSheet("font-size: 14px; color: #7289da;")
+
+ header_layout.addWidget(title)
+ header_layout.addWidget(subtitle)
+ header_layout.addWidget(author)
+
+ # Quick actions section
+ actions = QFrame()
+ actions.setStyleSheet("""
+ QFrame {
+ background-color: #2f3136;
+ border-radius: 10px;
+ padding: 20px;
+ }
+ QLabel {
+ color: white;
+ }
+ QPushButton {
+ background-color: #7289da;
+ border-radius: 5px;
+ padding: 10px;
+ color: white;
+ text-align: left;
+ font-size: 14px;
+ }
+ QPushButton:hover {
+ background-color: #677bc4;
+ }
+ """)
+ actions_layout = QVBoxLayout(actions)
+
+ actions_title = QLabel("Quick Actions")
+ actions_title.setStyleSheet("font-size: 18px; font-weight: bold; margin-bottom: 10px;")
+
+ # Create action buttons
+ inject_btn = QPushButton(qta.icon('fa5s.syringe'), " Script Injection")
+ inject_btn.clicked.connect(lambda: self.switch_page('inject'))
+
+ browse_btn = QPushButton(qta.icon('fa5s.cloud-download-alt'), " Browse CodeShare")
+ browse_btn.clicked.connect(lambda: self.switch_page('codeshare'))
+
+ favorites_btn = QPushButton(qta.icon('fa5s.star'), " View Favorites")
+ favorites_btn.clicked.connect(lambda: self.switch_page('favorites'))
+
+ monitor_btn = QPushButton(qta.icon('fa5s.desktop'), " Process Monitor")
+ monitor_btn.clicked.connect(lambda: self.switch_page('monitor'))
+
+ actions_layout.addWidget(actions_title)
+ actions_layout.addWidget(inject_btn)
+ actions_layout.addWidget(browse_btn)
+ actions_layout.addWidget(favorites_btn)
+ actions_layout.addWidget(monitor_btn)
+
+ # Add sections to main layout
+ layout.addWidget(header)
+ layout.addWidget(actions)
+ layout.addStretch()
+
+ return page
+
+ def create_injection_page(self):
+ page = QWidget()
+ layout = QVBoxLayout(page)
+
+ # Add device selector
+ self.device_selector = DeviceSelector()
+ self.script_editor = ScriptEditorPanel()
+ self.injection_panel = InjectionPanel()
+ self.injection_panel.script_editor = self.script_editor
+ self.output_panel = OutputPanel()
+
+ layout.addWidget(self.device_selector)
+ layout.addWidget(self.script_editor)
+ layout.addWidget(self.injection_panel)
+ layout.addWidget(self.output_panel)
+
+ # Connect signals - ensure we're passing both device_id and pid
+ self.device_selector.process_selected.connect(
+ lambda device_id, pid: self.injection_panel.set_process(device_id, pid)
+ )
+ self.injection_panel.injection_started.connect(self.inject_script)
+ self.injection_panel.injection_stopped.connect(self.stop_injection)
+
+ return page
+
+ def create_codeshare_page(self):
+ page = QWidget()
+ layout = QVBoxLayout(page)
+
+ self.codeshare_browser = CodeShareBrowser()
+ # Connect codeshare signals here, after creating the browser
+ self.codeshare_browser.open_in_injector.connect(self.open_script_in_injector)
+ layout.addWidget(self.codeshare_browser)
+
+ return page
+
+ def create_favorites_page(self):
+ page = QWidget()
+ layout = QVBoxLayout(page)
+
+ # Toolbar
+ toolbar = QHBoxLayout()
+
+ # Search bar
+ search_input = QLineEdit()
+ search_input.setPlaceholderText("⌕ Search favorites...")
+ search_input.textChanged.connect(self.filter_favorites)
+
+ # Upload button
+ upload_btn = QPushButton(qta.icon('fa5s.file-upload'), "Upload Script")
+ upload_btn.clicked.connect(self.upload_script)
+
+ toolbar.addWidget(search_input)
+ toolbar.addWidget(upload_btn)
+
+ # Grid for favorite scripts
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setStyleSheet("""
+ QScrollArea {
+ border: none;
+ background-color: #36393f;
+ }
+ """)
+
+ self.favorites_grid = QWidget()
+ self.favorites_grid_layout = QGridLayout(self.favorites_grid)
+ self.favorites_grid_layout.setSpacing(10)
+ scroll.setWidget(self.favorites_grid)
+
+ # Add components to layout
+ layout.addLayout(toolbar)
+ layout.addWidget(scroll)
+
+ # Initial population
+ self.refresh_favorites()
+
+ return page
+
+ def filter_favorites(self, text):
+ """Filter favorite scripts by search text"""
+ search_text = text.lower()
+ for i in range(self.favorites_grid_layout.count()):
+ widget = self.favorites_grid_layout.itemAt(i).widget()
+ if widget:
+ title = widget.findChild(QLabel).text().lower()
+ desc = widget.findChildren(QLabel)[-2].text().lower()
+ widget.setVisible(search_text in title or search_text in desc)
+
+ def upload_script(self):
+ """Upload a custom script to favorites"""
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ "Upload Script",
+ "",
+ "JavaScript Files (*.js);;All Files (*.*)"
+ )
+
+ if file_path:
+ try:
+ with open(file_path, 'r') as f:
+ script_content = f.read()
+
+ # Create script info
+ script_name = os.path.basename(file_path)
+ script_info = {
+ 'id': f"custom/{script_name}",
+ 'title': script_name,
+ 'author': 'Custom Script',
+ 'description': 'Uploaded custom script',
+ 'likes': 0,
+ 'seen': 0,
+ 'content': script_content
+ }
+
+ # Add to favorites
+ self.add_to_favorites(script_info)
+
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"Failed to upload script: {str(e)}")
+
+ def add_to_favorites(self, script_info):
+ """Add a script to favorites"""
+ # Add to favorites list if not already present
+ if not any(s['id'] == script_info['id'] for s in self.favorites):
+ self.favorites.append(script_info)
+ self.save_favorites()
+
+ # Create card widget
+ card = self.create_favorite_card(script_info)
+
+ # Add to grid
+ count = self.favorites_grid_layout.count()
+ row = count // 3
+ col = count % 3
+ self.favorites_grid_layout.addWidget(card, row, col)
+
+ def create_favorite_card(self, script_info):
+ """Create a card widget for a favorite script"""
+ card = QFrame()
+ card.setStyleSheet("""
+ QFrame {
+ background-color: #2f3136;
+ border-radius: 8px;
+ padding: 10px;
+ }
+ QFrame:hover {
+ background-color: #40444b;
+ }
+ """)
+
+ layout = QVBoxLayout(card)
+
+ # Title and metadata
+ title = QLabel(script_info['title'])
+ title.setStyleSheet("font-size: 14px; font-weight: bold; color: white;")
+ author = QLabel(f"by {script_info['author']}")
+ author.setStyleSheet("color: #b9bbbe;")
+
+ # Description
+ desc = QLabel(script_info.get('description', '')[:100] + '...')
+ desc.setWordWrap(True)
+ desc.setStyleSheet("color: #b9bbbe;")
+
+ # Action buttons
+ buttons = QHBoxLayout()
+
+ view_btn = QPushButton("View")
+ view_btn.clicked.connect(lambda: self.view_favorite(script_info))
+
+ inject_btn = QPushButton("⚡ Inject")
+ inject_btn.clicked.connect(lambda: self.open_script_in_injector(script_info.get('content', '')))
+
+ remove_btn = QPushButton("✕ Remove")
+ remove_btn.clicked.connect(lambda: self.remove_from_favorites(script_info, card))
+
+ buttons.addWidget(view_btn)
+ buttons.addWidget(inject_btn)
+ buttons.addWidget(remove_btn)
+ buttons.addStretch()
+
+ # Add all components
+ layout.addWidget(title)
+ layout.addWidget(author)
+ layout.addWidget(desc)
+ layout.addLayout(buttons)
+
+ return card
+
+ def view_favorite(self, script_info):
+ """View a favorite script's details"""
+ dialog = QDialog(self)
+ dialog.setWindowTitle(f"View Script - {script_info['title']}")
+ dialog.resize(800, 600)
+
+ layout = QVBoxLayout(dialog)
+
+ # Script content
+ content = QTextEdit()
+ content.setReadOnly(True)
+ content.setFont(QFont('Consolas', 11))
+ content.setText(script_info.get('content', 'Script content not available'))
+
+ # Action buttons
+ buttons = QHBoxLayout()
+
+ copy_btn = QPushButton(" Copy")
+ copy_btn.clicked.connect(lambda: self.copy_to_clipboard(content.toPlainText()))
+
+ inject_btn = QPushButton("⚡ Inject")
+ inject_btn.clicked.connect(lambda: self.open_script_in_injector(content.toPlainText()))
+
+ buttons.addWidget(copy_btn)
+ buttons.addWidget(inject_btn)
+ buttons.addStretch()
+
+ layout.addWidget(content)
+ layout.addLayout(buttons)
+
+ dialog.exec_()
+
+ def remove_from_favorites(self, script_info, card):
+ """Remove a script from favorites"""
+ reply = QMessageBox.question(
+ self,
+ "Remove Favorite",
+ f"Remove {script_info['title']} from favorites?",
+ QMessageBox.Yes | QMessageBox.No
+ )
+
+ if reply == QMessageBox.Yes:
+ # Remove from grid
+ card.setParent(None)
+
+ # Remove from favorites list
+ if script_info['id'].startswith('custom/'):
+ self.favorites = [s for s in self.favorites if s['id'] != script_info['id']]
+ self.save_favorites()
+ elif hasattr(self.codeshare_browser, 'favorites'):
+ self.codeshare_browser.favorites.remove(script_info['id'])
+ self.codeshare_browser.save_favorites()
+
+ # Refresh display
+ self.refresh_favorites()
+
+ def copy_to_clipboard(self, text):
+ """Copy text to clipboard"""
+ QApplication.clipboard().setText(text)
+ QMessageBox.information(self, "✓ Success", "📋 Copied to clipboard!")
+
+ def create_history_page(self):
+ page = QWidget()
+ layout = QVBoxLayout(page)
+
+ self.history_page = HistoryPage(self.history_manager)
+ self.history_page.script_selected.connect(self.open_script_in_injector)
+ layout.addWidget(self.history_page)
+
+ return page
+
+ def create_monitor_page(self):
+ page = QWidget()
+ layout = QVBoxLayout(page)
+
+ # Pass self (main window) to ProcessMonitor
+ self.process_monitor = ProcessMonitor(main_window=self)
+ layout.addWidget(self.process_monitor)
+
+ return page
+
+ def create_settings_page(self):
+ page = QWidget()
+ layout = QVBoxLayout(page)
+
+ # Add settings categories
+ settings_categories = [
+ ("General", [
+ ("Auto-inject on launch", "checkbox"),
+ ("Save script history", "checkbox"),
+ ("Dark theme", "checkbox")
+ ]),
+ ("Script Editor", [
+ ("Font size", "spinbox"),
+ ("Show line numbers", "checkbox"),
+ ("Auto-completion", "checkbox")
+ ]),
+ ("Monitoring", [
+ ("Update interval", "spinbox"),
+ ("Show memory usage", "checkbox"),
+ ("Log to file", "checkbox")
+ ])
+ ]
+
+ for category, settings in settings_categories:
+ group = QGroupBox(category)
+ group_layout = QVBoxLayout()
+
+ for setting_name, setting_type in settings:
+ setting_layout = QHBoxLayout()
+ setting_layout.addWidget(QLabel(setting_name))
+
+ if setting_type == "checkbox":
+ widget = QCheckBox()
+ elif setting_type == "spinbox":
+ widget = QSpinBox()
+
+ setting_layout.addWidget(widget)
+ group_layout.addLayout(setting_layout)
+
+ group.setLayout(group_layout)
+ layout.addWidget(group)
+
+ layout.addStretch()
+
+ return page
+
+ def on_process_started(self, name, pid):
+ self.status_text.setText(f"Process started: {name} ({pid})")
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(12, 12))
+
+ def on_process_ended(self, name, pid):
+ self.status_text.setText(f"Process ended: {name} ({pid})")
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#f04747').pixmap(12, 12))
+
+ def on_memory_updated(self, pid, memory_mb):
+ # Update memory usage in process monitor
+ pass
+
+ def inject_script(self, script_content, pid):
+ """Inject script into process"""
+ try:
+ if not script_content:
+ QMessageBox.warning(self, "Error", "No script to inject!")
+ return
+
+ # Update status
+ self.status_text.setText(f"Injecting into PID: {pid}")
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#faa61a').pixmap(12, 12))
+
+ # Get device and process info
+ device_id = self.device_selector.current_device
+ process_info = self.device_selector.get_selected_process_info()
+
+ if not process_info:
+ raise Exception("No process selected")
+
+ device = frida.get_device(device_id)
+
+ # Check if Android device needs frida-server
+ if device.type == 'usb':
+ if not AndroidHelper.is_frida_running(device_id):
+ self.output_panel.append_output("[*] Starting frida-server on device...")
+ if not AndroidHelper.start_frida_server(device_id):
+ raise Exception("Failed to start frida-server")
+ self.output_panel.append_output("[+] frida-server started")
+ # Re-get device after starting server
+ device = frida.get_device(device_id)
+
+ try:
+ # Try to attach first
+ session = device.attach(pid)
+ self.output_panel.append_output(f"[+] Successfully attached to PID: {pid}")
+ except frida.ProcessNotFoundError:
+ # If attach fails, try to spawn
+ try:
+ if device.type == 'local':
+ # For local processes, use executable path
+ import psutil
+ process = psutil.Process(pid)
+ executable = process.exe()
+ pid = device.spawn([executable])
+ self.output_panel.append_output(f"[+] Spawned process with PID: {pid}")
+ else:
+ # For Android/remote devices
+ if device.type == 'usb':
+ package_name = process_info['name']
+ pid = device.spawn([package_name])
+ self.output_panel.append_output(f"[+] Spawned Android app: {package_name}")
+ else:
+ pid = device.spawn([process_info['name']])
+
+ session = device.attach(pid)
+ device.resume(pid)
+ except Exception as e:
+ raise Exception(f"Failed to spawn process: {str(e)}")
+
+ # Create and load script
+ script = session.create_script(script_content)
+
+ def on_message(message, data):
+ if message['type'] == 'send':
+ self.output_panel.append_output(f"[*] {message['payload']}")
+ elif message['type'] == 'error':
+ self.output_panel.append_output(f"[!] {message['description']}")
+
+ script.on('message', on_message)
+ script.load()
+
+ # Update status on success
+ self.status_text.setText(f"Successfully injected into PID: {pid}")
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(12, 12))
+
+ # Store session and script
+ self.current_session = session
+ self.current_script = script
+
+ # Show success message
+ self.output_panel.append_output(f"[+] Script loaded successfully")
+
+ # Add history entry
+ self.history_manager.add_entry('script_injection', {
+ 'script': script_content,
+ 'pid': pid,
+ 'device': device_id,
+ 'status': 'success'
+ })
+
+ except Exception as e:
+ error_msg = f"Injection failed: {str(e)}"
+ self.output_panel.append_output(f"[-] {error_msg}")
+ QMessageBox.critical(self, "Error", error_msg)
+
+ # Add history entry
+ self.history_manager.add_entry('script_injection', {
+ 'script': script_content,
+ 'pid': pid,
+ 'device': device_id,
+ 'status': 'failed',
+ 'error': str(e)
+ })
+
+ finally:
+ if hasattr(self, 'injection_panel'):
+ self.injection_panel.reset_ui()
+
+ def stop_injection(self):
+ """Stop the current injection"""
+ try:
+ if hasattr(self, 'current_script') and self.current_script:
+ self.current_script.unload()
+ if hasattr(self, 'current_session') and self.current_session:
+ self.current_session.detach()
+
+ self.current_script = None
+ self.current_session = None
+
+ self.output_panel.append_output("[*] Script injection stopped")
+ self.status_text.setText("Ready")
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(12, 12))
+
+ except Exception as e:
+ error_msg = f"Error stopping injection: {str(e)}"
+ self.output_panel.append_output(f"[-] {error_msg}")
+ QMessageBox.critical(self, "Error", error_msg)
+
+ def on_process_selected(self, device_id, pid):
+ self.current_device = device_id
+ self.current_pid = pid
+ self.status_text.setText(f"Selected PID: {pid} on device: {device_id}")
+
+ def open_in_injector(self, device_id, pid):
+ """Open the selected process in the injector tab"""
+ # Switch to injector tab
+ self.switch_page('inject')
+
+ # Select the device and process
+ self.device_selector.select_device(device_id)
+ self.device_selector.select_process(pid)
+
+ def open_script_in_injector(self, code):
+ """Open a script in the injector page"""
+ # Switch to injector page
+ self.switch_page('inject')
+
+ # Set the script content
+ self.script_editor.set_script(code)
+
+ def load_favorites(self):
+ """Load favorites from file"""
+ try:
+ favorites_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'favorites.json')
+ if os.path.exists(favorites_file):
+ with open(favorites_file, 'r') as f:
+ data = json.load(f)
+ self.favorites = data.get('scripts', [])
+ except Exception as e:
+ print(f"Error loading favorites: {e}")
+ self.favorites = []
+
+ def save_favorites(self):
+ """Save favorites to file"""
+ try:
+ favorites_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'favorites.json')
+ os.makedirs(os.path.dirname(favorites_file), exist_ok=True)
+ with open(favorites_file, 'w') as f:
+ json.dump({'scripts': self.favorites}, f)
+ except Exception as e:
+ print(f"Error saving favorites: {e}")
+
+ def refresh_favorites(self):
+ """Refresh the favorites page"""
+ # Clear existing grid
+ for i in reversed(range(self.favorites_grid_layout.count())):
+ widget = self.favorites_grid_layout.itemAt(i).widget()
+ if widget:
+ widget.setParent(None)
+
+ # Get all favorites
+ try:
+ # Combine CodeShare favorites and custom scripts
+ all_favorites = []
+
+ # Add CodeShare favorites
+ if hasattr(self.codeshare_browser, 'favorites'):
+ response = requests.get(self.codeshare_browser.api_url)
+ codeshare_scripts = response.json()
+ for script in codeshare_scripts:
+ if script['id'] in self.codeshare_browser.favorites:
+ all_favorites.append(script)
+
+ # Add custom scripts from our favorites
+ all_favorites.extend([s for s in self.favorites if s['id'].startswith('custom/')])
+
+ if all_favorites:
+ # Add scripts to grid
+ for idx, script_info in enumerate(all_favorites):
+ row = idx // 3
+ col = idx % 3
+ card = self.create_favorite_card(script_info)
+ self.favorites_grid_layout.addWidget(card, row, col)
+ else:
+ # Show message if no favorites
+ msg = QLabel("No favorite scripts yet.\nBrowse scripts and click the ★ to add favorites!")
+ msg.setAlignment(Qt.AlignCenter)
+ msg.setStyleSheet("""
+ color: #b9bbbe;
+ font-size: 14px;
+ padding: 20px;
+ """)
+ self.favorites_grid_layout.addWidget(msg, 0, 0, 1, 3)
+
+ except Exception as e:
+ error_msg = QLabel(f"Error loading favorites: {str(e)}")
+ error_msg.setStyleSheet("color: #ff4444;")
+ self.favorites_grid_layout.addWidget(error_msg, 0, 0, 1, 3)
+
+ def cleanup(self):
+ """Clean up resources to prevent memory leaks"""
+ # Stop monitoring
+ if hasattr(self, 'process_monitor'):
+ self.process_monitor.stop_monitoring()
+
+ # Clear history
+ if hasattr(self, 'history_manager'):
+ self.history_manager.save_history()
+
+ # Clear device selector
+ if hasattr(self, 'device_selector'):
+ self.device_selector.cleanup()
+
+ # Clear any running scripts
+ self.stop_injection()
+
+ # Clear references
+ self.current_script = None
+ self.current_session = None
+
+ def closeEvent(self, event):
+ """Handle window close event"""
+ self.cleanup()
+ event.accept()
\ No newline at end of file
diff --git a/src/gui/widgets/__init__.py b/src/gui/widgets/__init__.py
new file mode 100644
index 0000000..166ce0b
--- /dev/null
+++ b/src/gui/widgets/__init__.py
@@ -0,0 +1 @@
+# Empty file to make the directory a Python package
\ No newline at end of file
diff --git a/src/gui/widgets/__pycache__/__init__.cpython-311.pyc b/src/gui/widgets/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..6cb549e
Binary files /dev/null and b/src/gui/widgets/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/gui/widgets/__pycache__/app_launcher.cpython-311.pyc b/src/gui/widgets/__pycache__/app_launcher.cpython-311.pyc
new file mode 100644
index 0000000..e3c9bb5
Binary files /dev/null and b/src/gui/widgets/__pycache__/app_launcher.cpython-311.pyc differ
diff --git a/src/gui/widgets/__pycache__/codeshare_browser.cpython-311.pyc b/src/gui/widgets/__pycache__/codeshare_browser.cpython-311.pyc
new file mode 100644
index 0000000..27b82b2
Binary files /dev/null and b/src/gui/widgets/__pycache__/codeshare_browser.cpython-311.pyc differ
diff --git a/src/gui/widgets/__pycache__/device_panel.cpython-311.pyc b/src/gui/widgets/__pycache__/device_panel.cpython-311.pyc
new file mode 100644
index 0000000..1a3dbcd
Binary files /dev/null and b/src/gui/widgets/__pycache__/device_panel.cpython-311.pyc differ
diff --git a/src/gui/widgets/__pycache__/device_selector.cpython-311.pyc b/src/gui/widgets/__pycache__/device_selector.cpython-311.pyc
new file mode 100644
index 0000000..248fa6b
Binary files /dev/null and b/src/gui/widgets/__pycache__/device_selector.cpython-311.pyc differ
diff --git a/src/gui/widgets/__pycache__/history_page.cpython-311.pyc b/src/gui/widgets/__pycache__/history_page.cpython-311.pyc
new file mode 100644
index 0000000..6b97a48
Binary files /dev/null and b/src/gui/widgets/__pycache__/history_page.cpython-311.pyc differ
diff --git a/src/gui/widgets/__pycache__/injection_panel.cpython-311.pyc b/src/gui/widgets/__pycache__/injection_panel.cpython-311.pyc
new file mode 100644
index 0000000..4883d17
Binary files /dev/null and b/src/gui/widgets/__pycache__/injection_panel.cpython-311.pyc differ
diff --git a/src/gui/widgets/__pycache__/output_panel.cpython-311.pyc b/src/gui/widgets/__pycache__/output_panel.cpython-311.pyc
new file mode 100644
index 0000000..108fb66
Binary files /dev/null and b/src/gui/widgets/__pycache__/output_panel.cpython-311.pyc differ
diff --git a/src/gui/widgets/__pycache__/process_monitor.cpython-311.pyc b/src/gui/widgets/__pycache__/process_monitor.cpython-311.pyc
new file mode 100644
index 0000000..3f50df9
Binary files /dev/null and b/src/gui/widgets/__pycache__/process_monitor.cpython-311.pyc differ
diff --git a/src/gui/widgets/__pycache__/process_panel.cpython-311.pyc b/src/gui/widgets/__pycache__/process_panel.cpython-311.pyc
new file mode 100644
index 0000000..6437ca7
Binary files /dev/null and b/src/gui/widgets/__pycache__/process_panel.cpython-311.pyc differ
diff --git a/src/gui/widgets/__pycache__/script_editor.cpython-311.pyc b/src/gui/widgets/__pycache__/script_editor.cpython-311.pyc
new file mode 100644
index 0000000..95c77df
Binary files /dev/null and b/src/gui/widgets/__pycache__/script_editor.cpython-311.pyc differ
diff --git a/src/gui/widgets/app_launcher.py b/src/gui/widgets/app_launcher.py
new file mode 100644
index 0000000..1738069
--- /dev/null
+++ b/src/gui/widgets/app_launcher.py
@@ -0,0 +1,306 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
+ QLineEdit, QComboBox, QLabel, QTableWidget,
+ QTableWidgetItem, QMenu, QAction, QCheckBox,
+ QFileDialog, QGroupBox)
+from PyQt5.QtCore import pyqtSignal, Qt
+import subprocess
+import json
+import os
+import qtawesome as qta
+import sys
+
+class AppLauncher(QWidget):
+ app_launched = pyqtSignal(str, int) # package_name, pid
+ script_selected = pyqtSignal(str) # script content
+
+ def __init__(self):
+ super().__init__()
+ self.favorites_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'favorites.json')
+ self.scripts_dir = os.path.join(os.path.expanduser('~'), '.frida_gui', 'scripts')
+ self.load_favorites()
+ self.setup_ui()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Quick Launch Section
+ quick_launch_group = QGroupBox("Quick Launch")
+ quick_launch_layout = QVBoxLayout()
+
+ # Package input
+ package_layout = QHBoxLayout()
+ self.package_input = QLineEdit()
+ self.package_input.setPlaceholderText("Enter package name or path...")
+ self.launch_button = QPushButton("Launch")
+ self.launch_button.setIcon(qta.icon('fa5s.play'))
+ self.launch_button.clicked.connect(self.launch_app)
+ package_layout.addWidget(self.package_input)
+ package_layout.addWidget(self.launch_button)
+
+ # Script Selection
+ script_layout = QHBoxLayout()
+ self.script_input = QLineEdit()
+ self.script_input.setPlaceholderText("Select Frida script file...")
+ self.script_input.setReadOnly(True)
+
+ self.browse_script_btn = QPushButton("Browse")
+ self.browse_script_btn.setIcon(qta.icon('fa5s.folder-open'))
+ self.browse_script_btn.clicked.connect(self.browse_script)
+
+ self.edit_script_btn = QPushButton("Edit")
+ self.edit_script_btn.setIcon(qta.icon('fa5s.edit'))
+ self.edit_script_btn.clicked.connect(self.edit_script)
+ self.edit_script_btn.setEnabled(False)
+
+ script_layout.addWidget(self.script_input)
+ script_layout.addWidget(self.browse_script_btn)
+ script_layout.addWidget(self.edit_script_btn)
+
+ # Launch Options
+ options_layout = QHBoxLayout()
+ self.debug_check = QCheckBox("Debug Mode")
+ self.wait_check = QCheckBox("Wait for Debugger")
+ self.inject_check = QCheckBox("Auto-Inject Script")
+ self.inject_check.toggled.connect(self.toggle_script_selection)
+
+ options_layout.addWidget(self.debug_check)
+ options_layout.addWidget(self.wait_check)
+ options_layout.addWidget(self.inject_check)
+
+ quick_launch_layout.addLayout(package_layout)
+ quick_launch_layout.addLayout(script_layout)
+ quick_launch_layout.addLayout(options_layout)
+ quick_launch_group.setLayout(quick_launch_layout)
+
+ # Favorites Section
+ favorites_group = QGroupBox("Favorites")
+ favorites_layout = QVBoxLayout()
+
+ self.favorites_table = QTableWidget(0, 4) # Added column for script
+ self.favorites_table.setHorizontalHeaderLabels(["Name", "Package", "Script", "Actions"])
+ self.favorites_table.horizontalHeader().setStretchLastSection(True)
+ self.favorites_table.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.favorites_table.customContextMenuRequested.connect(self.show_context_menu)
+
+ favorites_layout.addWidget(self.favorites_table)
+ favorites_group.setLayout(favorites_layout)
+
+ # Recent Apps Section
+ recent_group = QGroupBox("Recent Apps")
+ recent_layout = QHBoxLayout()
+ self.recent_combo = QComboBox()
+ self.recent_combo.setPlaceholderText("Recent Apps")
+ recent_launch_btn = QPushButton("Launch Recent")
+ recent_launch_btn.clicked.connect(self.launch_recent)
+
+ recent_layout.addWidget(self.recent_combo)
+ recent_layout.addWidget(recent_launch_btn)
+ recent_group.setLayout(recent_layout)
+
+ # Add all sections to main layout
+ layout.addWidget(quick_launch_group)
+ layout.addWidget(favorites_group)
+ layout.addWidget(recent_group)
+
+ # Populate favorites
+ self.update_favorites_table()
+
+ def browse_script(self):
+ file_name, _ = QFileDialog.getOpenFileName(
+ self,
+ "Select Frida Script",
+ self.scripts_dir,
+ "JavaScript Files (*.js);;All Files (*.*)"
+ )
+
+ if file_name:
+ self.script_input.setText(file_name)
+ self.edit_script_btn.setEnabled(True)
+
+ # Read script content
+ try:
+ with open(file_name, 'r') as f:
+ script_content = f.read()
+ self.script_selected.emit(script_content)
+ except Exception as e:
+ print(f"Error reading script: {str(e)}")
+
+ def edit_script(self):
+ script_path = self.script_input.text()
+ if script_path and os.path.exists(script_path):
+ # You can implement your own script editor or use system default
+ if sys.platform == 'win32':
+ os.startfile(script_path)
+ elif sys.platform == 'darwin':
+ subprocess.run(['open', script_path])
+ else:
+ subprocess.run(['xdg-open', script_path])
+
+ def toggle_script_selection(self, enabled):
+ self.script_input.setEnabled(enabled)
+ self.browse_script_btn.setEnabled(enabled)
+ self.edit_script_btn.setEnabled(enabled and bool(self.script_input.text()))
+
+ def add_to_favorites(self, name, package, script_path=None):
+ self.favorites[name] = {
+ 'package': package,
+ 'script': script_path
+ }
+ self.save_favorites()
+ self.update_favorites_table()
+
+ def update_favorites_table(self):
+ self.favorites_table.setRowCount(0)
+ for name, data in self.favorites.items():
+ row = self.favorites_table.rowCount()
+ self.favorites_table.insertRow(row)
+
+ name_item = QTableWidgetItem(name)
+ package_item = QTableWidgetItem(data['package'])
+ script_item = QTableWidgetItem(data.get('script', ''))
+
+ launch_btn = QPushButton("Launch")
+ launch_btn.clicked.connect(
+ lambda checked, p=data['package'], s=data.get('script'):
+ self.launch_favorite(p, s)
+ )
+
+ self.favorites_table.setItem(row, 0, name_item)
+ self.favorites_table.setItem(row, 1, package_item)
+ self.favorites_table.setItem(row, 2, script_item)
+ self.favorites_table.setCellWidget(row, 3, launch_btn)
+
+ def launch_favorite(self, package, script_path=None):
+ if script_path:
+ try:
+ with open(script_path, 'r') as f:
+ script_content = f.read()
+ self.script_selected.emit(script_content)
+ except Exception as e:
+ print(f"Error reading script: {str(e)}")
+ self.launch_app(package)
+
+ def show_context_menu(self, position):
+ menu = QMenu()
+ remove_action = QAction("Remove from Favorites", self)
+ remove_action.triggered.connect(self.remove_selected_favorite)
+
+ edit_script_action = QAction("Edit Script", self)
+ edit_script_action.triggered.connect(self.edit_selected_script)
+
+ menu.addAction(remove_action)
+ menu.addAction(edit_script_action)
+ menu.exec_(self.favorites_table.mapToGlobal(position))
+
+ def edit_selected_script(self):
+ current_row = self.favorites_table.currentRow()
+ if current_row >= 0:
+ script_path = self.favorites_table.item(current_row, 2).text()
+ if script_path:
+ if sys.platform == 'win32':
+ os.startfile(script_path)
+ elif sys.platform == 'darwin':
+ subprocess.run(['open', script_path])
+ else:
+ subprocess.run(['xdg-open', script_path])
+
+ def launch_app(self, package_name=None):
+ if not package_name:
+ package_name = self.package_input.text()
+
+ try:
+ cmd = ['adb', 'shell', 'am', 'start']
+
+ if self.debug_check.isChecked():
+ cmd.extend(['-D'])
+
+ if self.wait_check.isChecked():
+ cmd.extend(['-W'])
+
+ cmd.extend(['-n', f'{package_name}/{package_name}.MainActivity'])
+
+ process = subprocess.Popen(cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+
+ stdout, stderr = process.communicate()
+
+ if process.returncode == 0:
+ self.add_to_recent(package_name)
+ # Get PID of launched app
+ pid_cmd = ['adb', 'shell', 'pidof', package_name]
+ pid = subprocess.check_output(pid_cmd).decode().strip()
+ if pid:
+ self.app_launched.emit(package_name, int(pid))
+ else:
+ raise Exception(stderr.decode())
+
+ except Exception as e:
+ print(f"Error launching app: {str(e)}")
+
+ def add_to_favorites(self, name, package):
+ self.favorites[name] = package
+ self.save_favorites()
+ self.update_favorites_table()
+
+ def remove_from_favorites(self, name):
+ if name in self.favorites:
+ del self.favorites[name]
+ self.save_favorites()
+ self.update_favorites_table()
+
+ def load_favorites(self):
+ try:
+ if os.path.exists(self.favorites_file):
+ with open(self.favorites_file, 'r') as f:
+ self.favorites = json.load(f)
+ else:
+ self.favorites = {}
+ except:
+ self.favorites = {}
+
+ def save_favorites(self):
+ os.makedirs(os.path.dirname(self.favorites_file), exist_ok=True)
+ with open(self.favorites_file, 'w') as f:
+ json.dump(self.favorites, f)
+
+ def update_favorites_table(self):
+ self.favorites_table.setRowCount(0)
+ for name, package in self.favorites.items():
+ row = self.favorites_table.rowCount()
+ self.favorites_table.insertRow(row)
+
+ name_item = QTableWidgetItem(name)
+ package_item = QTableWidgetItem(package)
+
+ launch_btn = QPushButton("Launch")
+ launch_btn.clicked.connect(lambda checked, p=package: self.launch_app(p))
+
+ self.favorites_table.setItem(row, 0, name_item)
+ self.favorites_table.setItem(row, 1, package_item)
+ self.favorites_table.setCellWidget(row, 2, launch_btn)
+
+ def show_context_menu(self, position):
+ menu = QMenu()
+ remove_action = QAction("Remove from Favorites", self)
+ remove_action.triggered.connect(lambda: self.remove_selected_favorite())
+ menu.addAction(remove_action)
+ menu.exec_(self.favorites_table.mapToGlobal(position))
+
+ def remove_selected_favorite(self):
+ current_row = self.favorites_table.currentRow()
+ if current_row >= 0:
+ name = self.favorites_table.item(current_row, 0).text()
+ self.remove_from_favorites(name)
+
+ def add_to_recent(self, package_name):
+ current_text = self.recent_combo.currentText()
+ if current_text != package_name:
+ self.recent_combo.insertItem(0, package_name)
+ if self.recent_combo.count() > 10:
+ self.recent_combo.removeItem(10)
+
+ def launch_recent(self):
+ package_name = self.recent_combo.currentText()
+ if package_name:
+ self.launch_app(package_name)
\ No newline at end of file
diff --git a/src/gui/widgets/codeshare_browser.py b/src/gui/widgets/codeshare_browser.py
new file mode 100644
index 0000000..b195f4a
--- /dev/null
+++ b/src/gui/widgets/codeshare_browser.py
@@ -0,0 +1,539 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
+ QLineEdit, QPushButton, QListWidget,
+ QTextBrowser, QSplitter, QComboBox,
+ QLabel, QProgressBar, QMessageBox, QGroupBox, QDialog, QTabWidget, QMenu, QFrame, QTableWidget, QHeaderView, QFileDialog, QScrollArea, QGridLayout, QTextEdit)
+from PyQt5.QtCore import pyqtSignal, Qt, QThread, QUrl
+from PyQt5.QtGui import QFont, QDesktopServices, QIcon
+import aiohttp
+import asyncio
+import qtawesome as qta
+import json
+import os
+from bs4 import BeautifulSoup
+from core.script_templates import SCRIPT_TEMPLATES
+from core.script_history import ScriptHistory
+import time
+import requests
+import threading
+import re
+
+class CodeFetcher(QThread):
+ code_fetched = pyqtSignal(str)
+ error_occurred = pyqtSignal(str)
+
+ def __init__(self, url):
+ super().__init__()
+ self.url = url
+
+ def run(self):
+ try:
+ response = requests.get(self.url)
+ if response.status_code != 200:
+ self.error_occurred.emit(f"HTTP Error: {response.status_code}")
+ return
+
+ # Find the script content in the Vue.js data
+ script_match = re.search(r'projectSource: "(.*?)",', response.text, re.DOTALL)
+ if script_match:
+ # Unescape the JavaScript string
+ code = script_match.group(1).encode().decode('unicode_escape')
+ self.code_fetched.emit(code)
+ else:
+ # Try alternative method - look for the editor content
+ soup = BeautifulSoup(response.text, 'html.parser')
+ editor_div = soup.find('div', {'id': 'editor'})
+ if editor_div and editor_div.string:
+ self.code_fetched.emit(editor_div.string)
+ else:
+ self.error_occurred.emit("Could not find script content")
+
+ except Exception as e:
+ self.error_occurred.emit(f"Error fetching script: {str(e)}")
+
+class CodeShareBrowser(QWidget):
+ script_selected = pyqtSignal(str) # For injector
+ open_in_injector = pyqtSignal(str) # New signal for opening in injector
+ favorites_updated = pyqtSignal() # New signal for favorites updates
+
+ def __init__(self):
+ super().__init__()
+ self.scripts_cache = {}
+ self.api_url = "https://konsumer.js.org/frida-codeshare/codeshare.json"
+ self.favorites = [] # Initialize as empty list
+ self.load_favorites()
+ self.setup_ui()
+
+ def load_favorites(self):
+ """Load favorites from file"""
+ try:
+ favorites_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'favorites.json')
+ if os.path.exists(favorites_file):
+ with open(favorites_file, 'r') as f:
+ data = json.load(f)
+ # Make sure we get a list, even if loading from a dict
+ if isinstance(data, dict):
+ self.favorites = data.get('favorites', [])
+ else:
+ self.favorites = data if isinstance(data, list) else []
+ else:
+ self.favorites = []
+ except Exception as e:
+ print(f"Error loading favorites: {e}")
+ self.favorites = []
+
+ def save_favorites(self):
+ """Save favorites to file"""
+ try:
+ favorites_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'favorites.json')
+ os.makedirs(os.path.dirname(favorites_file), exist_ok=True)
+ with open(favorites_file, 'w') as f:
+ # Save as a simple list
+ json.dump(self.favorites, f)
+ except Exception as e:
+ print(f"Error saving favorites: {e}")
+
+ def is_favorite(self, script_id):
+ """Check if script is favorited"""
+ return script_id in self.favorites
+
+ def toggle_favorite(self, script_info):
+ """Toggle favorite status of script"""
+ script_id = script_info['id']
+ if script_id in self.favorites:
+ self.favorites.remove(script_id)
+ else:
+ self.favorites.append(script_id)
+ self.save_favorites()
+ self.refresh_favorites()
+ self.favorites_updated.emit() # Emit signal when favorites change
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Create tab widget
+ self.tab_widget = QTabWidget()
+
+ # Create tabs
+ self.browse_tab = QWidget()
+ self.favorites_tab = QWidget()
+
+ self.setup_browse_tab()
+ self.setup_favorites_tab()
+
+ # Add tabs
+ self.tab_widget.addTab(self.browse_tab, "Browse")
+ self.tab_widget.addTab(self.favorites_tab, "★ Favorites")
+
+ layout.addWidget(self.tab_widget)
+
+ self.refresh_scripts()
+
+ def setup_browse_tab(self):
+ """Setup the browse tab (existing functionality)"""
+ layout = QVBoxLayout(self.browse_tab)
+
+ # Move existing toolbar and grid here
+ toolbar = QHBoxLayout()
+
+ # Search bar
+ self.search_input = QLineEdit()
+ self.search_input.setPlaceholderText("⌕ Search scripts...")
+ self.search_input.textChanged.connect(self.filter_scripts)
+
+ # Category filter
+ self.category_combo = QComboBox()
+ self.category_combo.addItems(['All', 'Android', 'iOS', 'Windows', 'Linux', 'macOS'])
+ self.category_combo.currentTextChanged.connect(self.filter_scripts)
+
+ # Sort options
+ self.sort_combo = QComboBox()
+ self.sort_combo.addItems(['★ Most Popular', '👁 Most Viewed', '⏲ Latest'])
+ self.sort_combo.currentTextChanged.connect(self.refresh_scripts)
+
+ toolbar.addWidget(self.search_input)
+ toolbar.addWidget(self.category_combo)
+ toolbar.addWidget(self.sort_combo)
+
+ # Grid layout for scripts
+ self.grid_widget = QWidget()
+ self.grid_layout = QGridLayout(self.grid_widget)
+ self.grid_layout.setSpacing(10)
+
+ # Scroll area for grid
+ scroll = QScrollArea()
+ scroll.setWidget(self.grid_widget)
+ scroll.setWidgetResizable(True)
+ scroll.setStyleSheet("""
+ QScrollArea {
+ border: none;
+ background-color: #36393f;
+ }
+ """)
+
+ # Add all components
+ layout.addLayout(toolbar)
+ layout.addWidget(scroll)
+
+ def setup_favorites_tab(self):
+ """Setup the favorites tab"""
+ layout = QVBoxLayout(self.favorites_tab)
+
+ # Create grid for favorite scripts
+ self.favorites_grid = QWidget()
+ self.favorites_grid_layout = QGridLayout(self.favorites_grid)
+ self.favorites_grid_layout.setSpacing(10)
+
+ # Scroll area for favorites
+ scroll = QScrollArea()
+ scroll.setWidget(self.favorites_grid)
+ scroll.setWidgetResizable(True)
+ scroll.setStyleSheet("""
+ QScrollArea {
+ border: none;
+ background-color: #36393f;
+ }
+ """)
+
+ # Add to layout
+ layout.addWidget(scroll)
+
+ # Initial population of favorites
+ self.refresh_favorites()
+
+ def refresh_favorites(self):
+ """Refresh the favorites grid"""
+ # Clear existing favorites grid
+ for i in reversed(range(self.favorites_grid_layout.count())):
+ widget = self.favorites_grid_layout.itemAt(i).widget()
+ if widget:
+ widget.setParent(None)
+
+ # Get favorite scripts
+ try:
+ # Get all scripts
+ response = requests.get(self.api_url)
+ all_scripts = response.json()
+
+ # Filter to only favorited scripts
+ favorite_scripts = [s for s in all_scripts if s['id'] in self.favorites]
+
+ if favorite_scripts:
+ # Add scripts to grid
+ for idx, script_info in enumerate(favorite_scripts):
+ row = idx // 3
+ col = idx % 3
+ card = self.create_script_card(script_info)
+ self.favorites_grid_layout.addWidget(card, row, col)
+ else:
+ # Show message if no favorites
+ msg = QLabel("No favorite scripts yet.\nBrowse scripts and click the ★ to add favorites!")
+ msg.setAlignment(Qt.AlignCenter)
+ msg.setStyleSheet("""
+ color: #b9bbbe;
+ font-size: 14px;
+ padding: 20px;
+ """)
+ self.favorites_grid_layout.addWidget(msg, 0, 0, 1, 3)
+
+ except Exception as e:
+ print(f"Error refreshing favorites: {e}")
+ error_msg = QLabel(f"Error loading favorites: {str(e)}")
+ error_msg.setStyleSheet("color: #ff4444;")
+ self.favorites_grid_layout.addWidget(error_msg, 0, 0, 1, 3)
+
+ def fetch_scripts(self):
+ """Fetch scripts from API"""
+ try:
+ response = requests.get(self.api_url)
+ scripts = response.json()
+
+ # Sort scripts
+ sort_option = self.sort_combo.currentText()
+ if sort_option == 'Most Popular':
+ scripts.sort(key=lambda x: x.get('likes', 0), reverse=True)
+ elif sort_option == 'Most Viewed':
+ scripts.sort(key=lambda x: x.get('seen', 0), reverse=True)
+
+ return scripts
+ except Exception as e:
+ print(f"Error fetching scripts: {e}")
+ return []
+
+ def create_script_card(self, script_info):
+ """Create a card widget for a script"""
+ card = QFrame()
+ card.setStyleSheet("""
+ QFrame {
+ background-color: #2f3136;
+ border-radius: 8px;
+ padding: 10px;
+ }
+ QFrame:hover {
+ background-color: #40444b;
+ }
+ QLabel {
+ color: white;
+ }
+ """)
+
+ layout = QVBoxLayout(card)
+
+ # Title
+ title = QLabel(script_info['title'])
+ title.setStyleSheet("font-size: 14px; font-weight: bold;")
+ title.setWordWrap(True)
+
+ # Author
+ author = QLabel(f"by {script_info['author']}")
+ author.setStyleSheet("color: #b9bbbe;")
+
+ # Stats
+ stats = QHBoxLayout()
+ stars = QLabel(f"★ {script_info.get('likes', 0)}")
+ views = QLabel(f"👁 {script_info.get('seen', 0)}")
+ stats.addWidget(stars)
+ stats.addWidget(views)
+
+ # Description
+ desc = QLabel(script_info.get('description', '')[:100] + '...')
+ desc.setWordWrap(True)
+ desc.setStyleSheet("color: #b9bbbe;")
+
+ # Action buttons
+ buttons = QHBoxLayout()
+
+ view_btn = QPushButton("View")
+ view_btn.clicked.connect(lambda: self.fetch_script_code(script_info))
+
+ fav_btn = QPushButton()
+ if self.is_favorite(script_info['id']):
+ fav_btn.setIcon(QIcon())
+ fav_btn.setText("★")
+ else:
+ fav_btn.setIcon(QIcon())
+ fav_btn.setText("☆")
+ fav_btn.setStyleSheet("color: #b9bbbe;")
+ fav_btn.clicked.connect(lambda: self.toggle_favorite_ui(script_info, fav_btn))
+
+ buttons.addWidget(view_btn)
+ buttons.addWidget(fav_btn)
+ buttons.addStretch()
+
+ layout.addWidget(title)
+ layout.addWidget(author)
+ layout.addLayout(stats)
+ layout.addWidget(desc)
+ layout.addLayout(buttons)
+
+ return card
+
+ def fetch_script_code(self, script_info):
+ """Fetch and show script code"""
+ # Remove author name from ID if it's included
+ script_id = script_info['id'].replace(f"{script_info['author']}/", "")
+ url = f"https://codeshare.frida.re/@{script_info['author']}/{script_id}"
+ print(f"Fetching script from: {url}") # Debug print
+
+ # Create preview dialog
+ dialog = QDialog(self)
+ dialog.setWindowTitle(f"Frida CodeShare - {script_info['title']}")
+ dialog.resize(1000, 800)
+ dialog.setStyleSheet("""
+ QDialog {
+ background-color: #2f3136;
+ }
+ QLabel {
+ color: white;
+ }
+ QPushButton {
+ background-color: #7289da;
+ color: white;
+ padding: 8px 16px;
+ border-radius: 4px;
+ min-width: 100px;
+ }
+ QPushButton:hover {
+ background-color: #677bc4;
+ }
+ QTextEdit {
+ background-color: #36393f;
+ color: #dcddde;
+ border: none;
+ border-radius: 4px;
+ padding: 10px;
+ font-family: 'Consolas', monospace;
+ }
+ """)
+
+ layout = QVBoxLayout(dialog)
+ layout.setSpacing(15)
+
+ # Header
+ header = QHBoxLayout()
+ title = QLabel(script_info['title'])
+ title.setStyleSheet("font-size: 18px; font-weight: bold;")
+ author = QLabel(f"by {script_info['author']}")
+ author.setStyleSheet("color: #b9bbbe;")
+ header.addWidget(title)
+ header.addWidget(author)
+ header.addStretch()
+
+ # Stats
+ stats = QHBoxLayout()
+ likes = QLabel(f"★ {script_info.get('likes', 0)}")
+ views = QLabel(f"👁 {script_info.get('seen', 0)}")
+ stats.addWidget(likes)
+ stats.addWidget(views)
+ stats.addStretch()
+
+ # Description
+ desc = QLabel(script_info.get('description', ''))
+ desc.setWordWrap(True)
+ desc.setStyleSheet("color: #b9bbbe; padding: 10px;")
+
+ # Code preview
+ code_view = QTextEdit()
+ code_view.setReadOnly(True)
+ code_view.setFont(QFont('Consolas', 11))
+ code_view.setLineWrapMode(QTextEdit.NoWrap)
+ code_view.setText("Loading script...")
+
+ # Usage instructions
+ usage = QLabel(f"Try this code out by running:\n$ frida --codeshare {script_info['author']}/{script_info['id']} -f YOUR_BINARY")
+ usage.setStyleSheet("""
+ background-color: #202225;
+ padding: 10px;
+ border-radius: 4px;
+ font-family: 'Consolas', monospace;
+ """)
+
+ # Action buttons
+ buttons = QHBoxLayout()
+
+ copy_btn = QPushButton(qta.icon('fa5s.copy'), "⎘ Copy Code")
+ copy_btn.clicked.connect(lambda: self.copy_to_clipboard(code_view.toPlainText()))
+
+ inject_btn = QPushButton(qta.icon('fa5s.syringe'), "⚡ Open in Injector")
+ inject_btn.clicked.connect(lambda: self.open_in_injector_page(code_view.toPlainText(), dialog))
+
+ download_btn = QPushButton(qta.icon('fa5s.download'), "⤓ Download")
+ download_btn.clicked.connect(lambda: self.download_script(script_info['title'], code_view.toPlainText()))
+
+ open_btn = QPushButton(qta.icon('fa5s.external-link-alt'), "⧉ Open in Browser")
+ open_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(url)))
+
+ buttons.addWidget(copy_btn)
+ buttons.addWidget(inject_btn)
+ buttons.addWidget(download_btn)
+ buttons.addWidget(open_btn)
+ buttons.addStretch()
+
+ # Add all components
+ layout.addLayout(header)
+ layout.addLayout(stats)
+ layout.addWidget(desc)
+ layout.addWidget(usage)
+ layout.addWidget(code_view)
+ layout.addLayout(buttons)
+
+ dialog.show()
+
+ # Create and start the code fetcher thread
+ self.code_fetcher = CodeFetcher(url)
+ self.code_fetcher.code_fetched.connect(code_view.setText)
+ self.code_fetcher.error_occurred.connect(lambda err: code_view.setText(f"Error loading script: {err}"))
+ self.code_fetcher.start()
+
+ def refresh_scripts(self):
+ """Refresh scripts from API"""
+ # Clear existing grid
+ for i in reversed(range(self.grid_layout.count())):
+ self.grid_layout.itemAt(i).widget().setParent(None)
+
+ # Fetch and sort scripts
+ scripts = self.fetch_scripts()
+
+ # Add all scripts to grid
+ for idx, script_info in enumerate(scripts):
+ row = idx // 3
+ col = idx % 3
+ card = self.create_script_card(script_info)
+ self.grid_layout.addWidget(card, row, col)
+
+ # Refresh favorites tab
+ self.refresh_favorites()
+
+ def add_script(self, script_info):
+ """Add a script card to the grid"""
+ # Calculate grid position
+ count = self.grid_layout.count()
+ row = count // 3
+ col = count % 3
+
+ # Create and add card
+ card = self.create_script_card(script_info)
+ self.grid_layout.addWidget(card, row, col)
+
+ # Cache the script
+ self.scripts_cache[script_info['id']] = script_info
+
+ def filter_scripts(self):
+ """Filter visible scripts based on search and category"""
+ search_text = self.search_input.text().lower()
+ category = self.category_combo.currentText()
+
+ # Show/hide cards based on filters
+ for i in range(self.grid_layout.count()):
+ widget = self.grid_layout.itemAt(i).widget()
+ if widget:
+ title = widget.findChild(QLabel).text().lower()
+ desc = widget.findChildren(QLabel)[-2].text().lower() # Description label
+
+ show = True
+ if search_text and search_text not in title and search_text not in desc:
+ show = False
+ if category != 'All' and category not in desc:
+ show = False
+
+ widget.setVisible(show)
+
+ def copy_to_clipboard(self, text):
+ """Copy text to clipboard"""
+ clipboard = QApplication.clipboard()
+ clipboard.setText(text)
+ QMessageBox.information(self, "✓ Success", "⎘ Copied to clipboard!")
+
+ def download_script(self, title, code):
+ """Download script to file"""
+ filename = f"{title.lower().replace(' ', '_')}.js"
+ file_path, _ = QFileDialog.getSaveFileName(
+ self, "⤓ Save Script", filename, "JavaScript Files (*.js)"
+ )
+
+ if file_path:
+ try:
+ with open(file_path, 'w') as f:
+ f.write(code)
+ QMessageBox.information(self, "✓ Success", "⤓ Script downloaded successfully!")
+ except Exception as e:
+ QMessageBox.critical(self, "✗ Error", f"Failed to save script: {str(e)}")
+
+ def toggle_favorite_ui(self, script_info, button):
+ """Toggle favorite status and update UI"""
+ self.toggle_favorite(script_info)
+ if self.is_favorite(script_info['id']):
+ button.setIcon(QIcon())
+ button.setText("★")
+ else:
+ button.setIcon(QIcon())
+ button.setText("☆")
+ button.setStyleSheet("color: #b9bbbe;")
+
+ # Refresh favorites tab when status changes
+ self.refresh_favorites()
+
+ def open_in_injector_page(self, code, dialog=None):
+ """Open the script in the injector page"""
+ self.open_in_injector.emit(code) # Emit signal to main window
+ if dialog:
+ dialog.close() # Close the preview dialog
\ No newline at end of file
diff --git a/src/gui/widgets/data_visualizer.py b/src/gui/widgets/data_visualizer.py
new file mode 100644
index 0000000..578fd96
--- /dev/null
+++ b/src/gui/widgets/data_visualizer.py
@@ -0,0 +1,44 @@
+from PyQt5.QtWidgets import QWidget, QVBoxLayout
+from PyQt5.QtChart import QChart, QChartView, QLineSeries
+from PyQt5.QtCore import Qt, QTimer
+import json
+
+class DataVisualizer(QWidget):
+ def __init__(self):
+ super().__init__()
+ self.setup_ui()
+ self.api_calls = []
+ self.setup_timer()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Create chart
+ self.chart = QChart()
+ self.chart.setTitle("API Calls Over Time")
+ self.chart.setAnimationOptions(QChart.SeriesAnimations)
+
+ self.series = QLineSeries()
+ self.chart.addSeries(self.series)
+
+ chart_view = QChartView(self.chart)
+ chart_view.setRenderHint(QPainter.Antialiasing)
+
+ layout.addWidget(chart_view)
+
+ def setup_timer(self):
+ self.timer = QTimer()
+ self.timer.timeout.connect(self.update_chart)
+ self.timer.start(1000) # Update every second
+
+ def add_api_call(self, call_data):
+ self.api_calls.append({
+ 'timestamp': time.time(),
+ 'data': call_data
+ })
+
+ def update_chart(self):
+ # Update chart with new data
+ self.series.clear()
+ for i, call in enumerate(self.api_calls[-50:]): # Show last 50 calls
+ self.series.append(i, len(call['data']))
\ No newline at end of file
diff --git a/src/gui/widgets/device_panel.py b/src/gui/widgets/device_panel.py
new file mode 100644
index 0000000..16947c4
--- /dev/null
+++ b/src/gui/widgets/device_panel.py
@@ -0,0 +1,46 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
+ QComboBox, QPushButton, QLabel)
+from PyQt5.QtCore import pyqtSignal
+import frida
+
+class DevicePanel(QWidget):
+ device_selected = pyqtSignal(str)
+
+ def __init__(self):
+ super().__init__()
+ self.setup_ui()
+
+ def setup_ui(self):
+ layout = QHBoxLayout(self)
+
+ # Device selection combo box
+ self.device_combo = QComboBox()
+ self.scan_button = QPushButton("Scan Devices")
+
+ layout.addWidget(QLabel("Select Device:"))
+ layout.addWidget(self.device_combo)
+ layout.addWidget(self.scan_button)
+
+ # Connect signals
+ self.scan_button.clicked.connect(self.scan_devices)
+ self.device_combo.currentIndexChanged.connect(self._on_device_selected)
+
+ # Initial scan
+ self.scan_devices()
+
+ def scan_devices(self):
+ try:
+ self.device_combo.clear()
+ devices = frida.enumerate_devices()
+ for device in devices:
+ if device.type in ['usb', 'remote']:
+ self.device_combo.addItem(f"{device.name} (ADB - {device.type})", device.id)
+ elif device.type == 'local':
+ self.device_combo.addItem(f"{device.name} (Local)", device.id)
+ except Exception as e:
+ print(f"Error scanning devices: {str(e)}")
+
+ def _on_device_selected(self):
+ device_id = self.device_combo.currentData()
+ if device_id:
+ self.device_selected.emit(device_id)
\ No newline at end of file
diff --git a/src/gui/widgets/device_selector.py b/src/gui/widgets/device_selector.py
new file mode 100644
index 0000000..d80fbe0
--- /dev/null
+++ b/src/gui/widgets/device_selector.py
@@ -0,0 +1,313 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QComboBox,
+ QPushButton, QLabel, QFrame, QLineEdit, QMessageBox,
+ QApplication)
+from PyQt5.QtCore import pyqtSignal, QSize
+import frida
+import subprocess
+import qtawesome as qta
+import psutil
+import sys
+from pathlib import Path
+
+# Add project root to Python path
+sys.path.append(str(Path(__file__).parent.parent.parent))
+from core.android_helper import AndroidHelper
+
+class DeviceSelector(QWidget):
+ process_selected = pyqtSignal(str, int) # device_id, pid
+
+ def __init__(self):
+ super().__init__()
+ self.current_device = None
+ self.setup_ui()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+
+ # Create main frame
+ frame = QFrame()
+ frame.setStyleSheet("""
+ QFrame {
+ background-color: #2f3136;
+ border-radius: 8px;
+ padding: 10px;
+ }
+ QComboBox {
+ background-color: #36393f;
+ border: none;
+ border-radius: 4px;
+ padding: 8px;
+ color: white;
+ min-width: 200px;
+ }
+ QComboBox::drop-down {
+ border: none;
+ padding-right: 10px;
+ }
+ QComboBox::down-arrow {
+ image: url(down-arrow.png);
+ }
+ """)
+
+ frame_layout = QVBoxLayout(frame)
+
+ # Device selection
+ device_layout = QHBoxLayout()
+
+ self.device_combo = QComboBox()
+ self.device_combo.setPlaceholderText("Select Device...")
+ self.device_combo.currentIndexChanged.connect(self.on_device_changed)
+
+ refresh_btn = QPushButton(qta.icon('fa5s.sync'), "")
+ refresh_btn.setToolTip("Refresh Devices")
+ refresh_btn.clicked.connect(self.refresh_devices)
+
+ device_layout.addWidget(QLabel("Device:"))
+ device_layout.addWidget(self.device_combo, 1)
+ device_layout.addWidget(refresh_btn)
+
+ # Process selection
+ process_layout = QHBoxLayout()
+
+ # Add filter input
+ self.process_filter = QLineEdit()
+ self.process_filter.setPlaceholderText("Filter processes...")
+ self.process_filter.textChanged.connect(self.filter_processes)
+
+ self.process_combo = QComboBox()
+ self.process_combo.setPlaceholderText("Select Process...")
+ self.process_combo.currentIndexChanged.connect(self.on_process_changed)
+ self.process_combo.setMaxVisibleItems(20) # Show more items in dropdown
+ self.process_combo.setStyleSheet("""
+ QComboBox QListView {
+ min-width: 300px;
+ }
+ """)
+
+ # Add refresh button
+ refresh_btn = QPushButton(qta.icon('fa5s.sync'), "")
+ refresh_btn.setToolTip("Refresh Processes")
+ refresh_btn.clicked.connect(self.refresh_processes)
+
+ process_layout.addWidget(QLabel("Process:"))
+ process_layout.addWidget(self.process_filter)
+ process_layout.addWidget(self.process_combo, 1)
+ process_layout.addWidget(refresh_btn)
+
+ # Add layouts to frame
+ frame_layout.addLayout(device_layout)
+ frame_layout.addLayout(process_layout)
+
+ # Add frame to main layout
+ layout.addWidget(frame)
+
+ # Initial device scan
+ self.refresh_devices()
+
+ def refresh_devices(self):
+ self.device_combo.clear()
+ try:
+ devices = frida.enumerate_devices()
+ for device in devices:
+ if device.type == 'usb':
+ self.device_combo.addItem(f"📱 {device.name} (USB)", device.id)
+ elif device.type == 'remote':
+ self.device_combo.addItem(f"🌐 {device.name} (Remote)", device.id)
+ elif device.type == 'local':
+ self.device_combo.addItem(f"💻 {device.name} (Local)", device.id)
+ except Exception as e:
+ print(f"Error enumerating devices: {e}")
+
+ def on_device_changed(self, index):
+ if index < 0:
+ return
+
+ device_id = self.device_combo.currentData()
+ self.current_device = device_id
+ self.refresh_processes()
+
+ def refresh_processes(self):
+ self.process_combo.clear()
+ if not self.current_device:
+ return
+
+ try:
+ device = frida.get_device(self.current_device)
+
+ if device.type == 'usb':
+ # Show loading message
+ self.process_combo.addItem("Checking device status...")
+ QApplication.processEvents()
+
+ # Check frida-server for Android devices
+ if not AndroidHelper.is_device_connected(self.current_device):
+ raise Exception(f"Device {self.current_device} not connected")
+
+ if not AndroidHelper.is_frida_running(self.current_device):
+ # Show installing message
+ msg = QMessageBox()
+ msg.setIcon(QMessageBox.Information)
+ msg.setText("Installing Frida Server")
+ msg.setInformativeText("Please wait while Frida server is being installed on the device...")
+ msg.setWindowTitle("Installing Frida")
+ msg.show()
+ QApplication.processEvents()
+
+ success = AndroidHelper.start_frida_server(self.current_device)
+ msg.close()
+
+ if not success:
+ error_msg = QMessageBox()
+ error_msg.setIcon(QMessageBox.Critical)
+ error_msg.setText("Frida Installation Failed")
+ error_msg.setInformativeText("Failed to install and start Frida server on the device. Please check your device connection and try again.")
+ error_msg.setWindowTitle("Installation Error")
+ error_msg.exec_()
+ return
+
+ # Show success message
+ success_msg = QMessageBox()
+ success_msg.setIcon(QMessageBox.Information)
+ success_msg.setText("Frida Server Installed")
+ success_msg.setInformativeText("Frida server has been successfully installed and started on the device.")
+ success_msg.setWindowTitle("Installation Complete")
+ success_msg.exec_()
+
+ # Clear loading message and get processes
+ self.process_combo.clear()
+
+ try:
+ # Get Android processes using frida-ps
+ processes = device.enumerate_processes()
+ for process in processes:
+ if process.pid > 0:
+ name = process.name
+ pid = process.pid
+ # Only add user apps (filter out system processes)
+ if '.' in name: # Simple check for app package names
+ self.process_combo.addItem(
+ f"{name} (PID: {pid})",
+ pid
+ )
+ except Exception as e:
+ print(f"Error getting processes: {str(e)}")
+ raise Exception("Failed to get process list from device")
+
+ elif device.type == 'local':
+ # Handle local device processes
+ processes = device.enumerate_processes()
+ for process in processes:
+ if process.pid > 0:
+ self.process_combo.addItem(
+ f"{process.name} (PID: {process.pid})",
+ process.pid
+ )
+
+ except Exception as e:
+ error_msg = QMessageBox()
+ error_msg.setIcon(QMessageBox.Critical)
+ error_msg.setText("Error")
+ error_msg.setInformativeText(f"Failed to refresh processes: {str(e)}")
+ error_msg.setWindowTitle("Process List Error")
+ error_msg.exec_()
+
+ self.process_combo.clear()
+ self.process_combo.addItem("Error loading processes")
+
+ def filter_processes(self, text):
+ """Filter processes in combo box"""
+ text = text.lower()
+ self.process_combo.clear()
+
+ try:
+ device = frida.get_device(self.current_device)
+
+ if device.type == 'local':
+ processes = device.enumerate_processes()
+ for process in processes:
+ try:
+ if process.pid > 0 and process.name and text in process.name.lower():
+ pid = int(process.pid)
+ name = str(process.name)
+ self.process_combo.addItem(
+ f"{name} (PID: {pid})",
+ pid
+ )
+ except (ValueError, AttributeError) as e:
+ continue
+ else:
+ # For Android devices
+ processes = device.enumerate_processes()
+ for process in processes:
+ if process.pid > 0 and text in process.name.lower():
+ if '.' in process.name: # Only show Android apps
+ self.process_combo.addItem(
+ f"{process.name} (PID: {process.pid})",
+ process.pid
+ )
+
+ except Exception as e:
+ print(f"Error filtering processes: {e}")
+
+ def on_process_changed(self, index):
+ if index < 0:
+ return
+
+ try:
+ device_id = self.device_combo.currentData()
+ pid = self.process_combo.currentData()
+
+ # Debug output
+ print(f"Process changed - device_id: {device_id}, pid: {pid} ({type(pid)})")
+
+ # Only emit if we have valid data
+ if device_id and isinstance(pid, int) and pid > 0:
+ self.process_selected.emit(device_id, pid)
+ else:
+ print(f"Skipping invalid process selection - device_id: {device_id}, pid: {pid}")
+
+ except Exception as e:
+ print(f"Error in process selection: {e}")
+
+ def get_selected_process_info(self):
+ """Get info about selected process"""
+ try:
+ index = self.process_combo.currentIndex()
+ if index >= 0:
+ device_id = self.device_combo.currentData()
+ pid = self.process_combo.currentData()
+ name = self.process_combo.currentText().split('(')[0].strip()
+
+ # Debug output
+ print(f"Selected process - PID: {pid} ({type(pid)}), Name: {name}")
+
+ if device_id and pid:
+ return {
+ 'device_id': device_id,
+ 'pid': pid,
+ 'name': name
+ }
+ return None
+ except Exception as e:
+ print(f"Error getting process info: {e}")
+ return None
+
+ def select_device(self, device_id):
+ """Select a device by its ID"""
+ index = self.device_combo.findData(device_id)
+ if index >= 0:
+ self.device_combo.setCurrentIndex(index)
+
+ def select_process(self, pid):
+ """Select a process by its PID"""
+ for i in range(self.process_combo.count()):
+ if str(pid) in self.process_combo.itemText(i):
+ self.process_combo.setCurrentIndex(i)
+ break
+
+ def cleanup(self):
+ """Clean up resources"""
+ self.process_combo.clear()
+ self.device_combo.clear()
+ self.current_device = None
\ No newline at end of file
diff --git a/src/gui/widgets/history_page.py b/src/gui/widgets/history_page.py
new file mode 100644
index 0000000..1e84f44
--- /dev/null
+++ b/src/gui/widgets/history_page.py
@@ -0,0 +1,148 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
+ QLabel, QTableWidget, QTableWidgetItem, QHeaderView,
+ QMenu, QMessageBox)
+from PyQt5.QtCore import Qt, pyqtSignal
+import qtawesome as qta
+from datetime import datetime
+
+class HistoryPage(QWidget):
+ script_selected = pyqtSignal(str) # For opening scripts in injector
+
+ def __init__(self, history_manager):
+ super().__init__()
+ self.history_manager = history_manager
+ self.setup_ui()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Header with title and clear button
+ header_layout = QHBoxLayout()
+ title = QLabel("Action History")
+ title.setStyleSheet("font-size: 18px; font-weight: bold; color: white;")
+
+ clear_btn = QPushButton(qta.icon('fa5s.trash'), "Clear History")
+ clear_btn.clicked.connect(self.clear_history)
+
+ header_layout.addWidget(title)
+ header_layout.addStretch()
+ header_layout.addWidget(clear_btn)
+
+ # History table
+ self.table = QTableWidget()
+ self.table.setColumnCount(4)
+ self.table.setHorizontalHeaderLabels(["Time", "Action", "Details", "Actions"])
+
+ # Style the table
+ self.table.setStyleSheet("""
+ QTableWidget {
+ background-color: #36393f;
+ border: none;
+ border-radius: 8px;
+ }
+ QTableWidget::item {
+ padding: 8px;
+ border-bottom: 1px solid #2f3136;
+ }
+ QHeaderView::section {
+ background-color: #2f3136;
+ padding: 8px;
+ border: none;
+ color: white;
+ font-weight: bold;
+ }
+ """)
+
+ # Set column stretching
+ table_header = self.table.horizontalHeader()
+ table_header.setSectionResizeMode(0, QHeaderView.Fixed) # Time
+ table_header.setSectionResizeMode(1, QHeaderView.Fixed) # Action
+ table_header.setSectionResizeMode(2, QHeaderView.Stretch) # Details
+ table_header.setSectionResizeMode(3, QHeaderView.Fixed) # Actions
+
+ self.table.setColumnWidth(0, 180) # Time
+ self.table.setColumnWidth(1, 120) # Action
+ self.table.setColumnWidth(3, 100) # Actions
+
+ # Context menu
+ self.table.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.table.customContextMenuRequested.connect(self.show_context_menu)
+
+ # Add components to layout
+ layout.addLayout(header_layout)
+ layout.addWidget(self.table)
+
+ self.refresh_history()
+
+ def refresh_history(self):
+ self.table.setRowCount(0)
+
+ for entry in self.history_manager.history:
+ row = self.table.rowCount()
+ self.table.insertRow(row)
+
+ # Time
+ time_item = QTableWidgetItem(
+ datetime.fromisoformat(entry['timestamp']).strftime('%Y-%m-%d %H:%M:%S')
+ )
+
+ # Action
+ action_item = QTableWidgetItem(entry['type'])
+
+ # Details
+ details = entry['details']
+ if isinstance(details, dict):
+ details_text = "\n".join(f"{k}: {v}" for k, v in details.items())
+ else:
+ details_text = str(details)
+ details_item = QTableWidgetItem(details_text)
+
+ # Action buttons
+ action_widget = QWidget()
+ action_layout = QHBoxLayout(action_widget)
+ action_layout.setContentsMargins(4, 4, 4, 4)
+
+ if 'script' in entry['details']:
+ inject_btn = QPushButton(qta.icon('fa5s.syringe'), "")
+ inject_btn.clicked.connect(
+ lambda x, s=entry['details']['script']: self.script_selected.emit(s)
+ )
+ action_layout.addWidget(inject_btn)
+
+ action_layout.addStretch()
+
+ # Add items to row
+ self.table.setItem(row, 0, time_item)
+ self.table.setItem(row, 1, action_item)
+ self.table.setItem(row, 2, details_item)
+ self.table.setCellWidget(row, 3, action_widget)
+
+ def clear_history(self):
+ reply = QMessageBox.question(
+ self,
+ "Clear History",
+ "Are you sure you want to clear all history?",
+ QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.No
+ )
+
+ if reply == QMessageBox.Yes:
+ self.history_manager.clear_history()
+ self.refresh_history()
+
+ def show_context_menu(self, position):
+ menu = QMenu()
+
+ copy_action = menu.addAction("Copy Details")
+ copy_action.triggered.connect(
+ lambda: self.copy_details(self.table.currentRow())
+ )
+
+ menu.exec_(self.table.viewport().mapToGlobal(position))
+
+ def copy_details(self, row):
+ if row >= 0:
+ details_item = self.table.item(row, 2)
+ if details_item:
+ clipboard = QApplication.clipboard()
+ clipboard.setText(details_item.text())
\ No newline at end of file
diff --git a/src/gui/widgets/injection_panel.py b/src/gui/widgets/injection_panel.py
new file mode 100644
index 0000000..187054d
--- /dev/null
+++ b/src/gui/widgets/injection_panel.py
@@ -0,0 +1,220 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
+ QLabel, QProgressBar, QFrame, QMessageBox, QFileDialog)
+from PyQt5.QtCore import Qt, pyqtSignal
+import qtawesome as qta
+import os
+
+class InjectionPanel(QWidget):
+ injection_started = pyqtSignal(str, int) # script, pid
+ injection_completed = pyqtSignal(bool, str) # success, message
+ injection_stopped = pyqtSignal() # Signal to stop injection
+
+ def __init__(self):
+ super().__init__()
+ self.current_pid = None
+ self.setup_ui()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Status panel
+ status_frame = QFrame()
+ status_frame.setStyleSheet("""
+ QFrame {
+ background-color: #2f3136;
+ border-radius: 8px;
+ padding: 10px;
+ }
+ """)
+ status_layout = QHBoxLayout(status_frame)
+
+ self.status_icon = QLabel()
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#99aab5').pixmap(16, 16))
+ self.status_label = QLabel("No process selected")
+ self.status_label.setStyleSheet("color: #99aab5;")
+
+ status_layout.addWidget(self.status_icon)
+ status_layout.addWidget(self.status_label)
+ status_layout.addStretch()
+
+ # Action buttons
+ button_layout = QHBoxLayout()
+
+ self.load_btn = QPushButton(qta.icon('fa5s.folder-open'), "Load Script")
+ self.load_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #7289da;
+ color: white;
+ padding: 8px 16px;
+ border-radius: 4px;
+ }
+ QPushButton:hover {
+ background-color: #677bc4;
+ }
+ """)
+ self.load_btn.clicked.connect(self.load_script_file)
+
+ # Attach button (for running processes)
+ self.attach_btn = QPushButton(qta.icon('fa5s.link'), "Attach")
+ self.attach_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #43b581;
+ color: white;
+ padding: 8px 16px;
+ border-radius: 4px;
+ }
+ QPushButton:hover {
+ background-color: #3ca374;
+ }
+ QPushButton:disabled {
+ background-color: #2f3136;
+ color: #72767d;
+ }
+ """)
+ self.attach_btn.clicked.connect(lambda: self.start_injection(mode="attach"))
+ self.attach_btn.setEnabled(False)
+
+ # Launch button (for spawning new process)
+ self.launch_btn = QPushButton(qta.icon('fa5s.play'), "Launch")
+ self.launch_btn.setStyleSheet(self.attach_btn.styleSheet())
+ self.launch_btn.clicked.connect(lambda: self.start_injection(mode="launch"))
+ self.launch_btn.setEnabled(False)
+
+ self.stop_btn = QPushButton(qta.icon('fa5s.stop'), "Stop")
+ self.stop_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #f04747;
+ color: white;
+ padding: 8px 16px;
+ border-radius: 4px;
+ }
+ QPushButton:hover {
+ background-color: #d84040;
+ }
+ """)
+ self.stop_btn.clicked.connect(self.stop_injection)
+ self.stop_btn.setEnabled(False)
+
+ button_layout.addWidget(self.load_btn)
+ button_layout.addWidget(self.attach_btn)
+ button_layout.addWidget(self.launch_btn)
+ button_layout.addWidget(self.stop_btn)
+ button_layout.addStretch()
+
+ # Progress bar
+ self.progress_bar = QProgressBar()
+ self.progress_bar.setTextVisible(False)
+ self.progress_bar.setStyleSheet("""
+ QProgressBar {
+ border: none;
+ background-color: #2f3136;
+ border-radius: 4px;
+ height: 8px;
+ }
+ QProgressBar::chunk {
+ background-color: #7289da;
+ border-radius: 4px;
+ }
+ """)
+ self.progress_bar.hide()
+
+ # Add all components
+ layout.addWidget(status_frame)
+ layout.addLayout(button_layout)
+ layout.addWidget(self.progress_bar)
+
+ def set_process(self, device_id, pid):
+ """Called when a process is selected"""
+ try:
+ # Ensure pid is an integer
+ if not isinstance(pid, int):
+ print(f"Warning: PID is not an integer: {pid} ({type(pid)})")
+ pid = int(pid)
+
+ if pid <= 0:
+ raise ValueError(f"Invalid PID value: {pid}")
+
+ self.current_pid = pid
+ self.status_label.setText(f"Selected PID: {self.current_pid}")
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(16, 16))
+ self.attach_btn.setEnabled(True)
+ self.launch_btn.setEnabled(True)
+
+ except (ValueError, TypeError) as e:
+ print(f"Error setting process: {e}")
+ self.status_label.setText("Invalid PID")
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#f04747').pixmap(16, 16))
+ self.attach_btn.setEnabled(False)
+ self.launch_btn.setEnabled(False)
+
+ def load_script_file(self):
+ """Load script from file"""
+ file_name, _ = QFileDialog.getOpenFileName(
+ self,
+ "Load Frida Script",
+ "",
+ "JavaScript Files (*.js);;All Files (*.*)"
+ )
+
+ if file_name:
+ try:
+ with open(file_name, 'r') as f:
+ script_content = f.read()
+ self.script_editor.set_script(script_content)
+ self.status_label.setText(f"Loaded script: {os.path.basename(file_name)}")
+ except Exception as e:
+ QMessageBox.critical(self, "Error", f"Failed to load script: {str(e)}")
+
+ def start_injection(self, mode="attach"):
+ """Start the injection process"""
+ if not self.current_pid or not isinstance(self.current_pid, int) or self.current_pid <= 0:
+ QMessageBox.warning(self, "Error", "Invalid PID!")
+ return
+
+ script_content = self.script_editor.get_script()
+ if not script_content:
+ QMessageBox.warning(self, "Error", "No script to inject!")
+ return
+
+ # Debug output
+ print(f"Starting injection - PID: {self.current_pid} ({type(self.current_pid)}), Mode: {mode}")
+
+ # Update UI
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#faa61a').pixmap(16, 16))
+ self.status_label.setText(f"{'Attaching to' if mode == 'attach' else 'Launching'} process...")
+ self.attach_btn.setEnabled(False)
+ self.launch_btn.setEnabled(False)
+ self.stop_btn.setEnabled(True)
+ self.progress_bar.show()
+ self.progress_bar.setRange(0, 0)
+
+ try:
+ self.injection_started.emit(script_content, self.current_pid)
+ except Exception as e:
+ self.injection_failed(str(e))
+
+ def injection_succeeded(self):
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(16, 16))
+ self.status_label.setText("Injection successful!")
+ self.reset_ui()
+ QMessageBox.information(self, "Success", "Script injected successfully!")
+
+ def injection_failed(self, error):
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#f04747').pixmap(16, 16))
+ self.status_label.setText("Injection failed!")
+ self.reset_ui()
+ QMessageBox.critical(self, "Error", f"Injection failed: {error}")
+
+ def reset_ui(self):
+ self.attach_btn.setEnabled(True)
+ self.launch_btn.setEnabled(True)
+ self.stop_btn.setEnabled(False)
+ self.progress_bar.hide()
+
+ def stop_injection(self):
+ """Stop the current injection"""
+ self.injection_stopped.emit()
+ self.reset_ui()
+ self.status_label.setText("Injection stopped")
+ self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#faa61a').pixmap(16, 16))
+
\ No newline at end of file
diff --git a/src/gui/widgets/output_panel.py b/src/gui/widgets/output_panel.py
new file mode 100644
index 0000000..93c065c
--- /dev/null
+++ b/src/gui/widgets/output_panel.py
@@ -0,0 +1,21 @@
+from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit
+
+class OutputPanel(QWidget):
+ def __init__(self):
+ super().__init__()
+ self.setup_ui()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ self.output_area = QTextEdit()
+ self.output_area.setReadOnly(True)
+ self.output_area.setPlaceholderText("Output will appear here...")
+
+ layout.addWidget(self.output_area)
+
+ def append_output(self, text):
+ self.output_area.append(text)
+
+ def clear_output(self):
+ self.output_area.clear()
\ No newline at end of file
diff --git a/src/gui/widgets/process_manager.py b/src/gui/widgets/process_manager.py
new file mode 100644
index 0000000..9bd380e
--- /dev/null
+++ b/src/gui/widgets/process_manager.py
@@ -0,0 +1,307 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
+ QLabel, QLineEdit, QTableWidget, QTableWidgetItem,
+ QMenu, QAction, QComboBox, QCheckBox, QFrame,
+ QTableWidgetSelectionRange, QHeaderView)
+from PyQt5.QtCore import Qt, pyqtSignal, QTimer
+from PyQt5.QtGui import QColor, QFont
+import qtawesome as qta
+import re
+import psutil
+
+class ProcessManager(QWidget):
+ process_selected = pyqtSignal(int) # pid
+
+ def __init__(self):
+ super().__init__()
+ self.processes = {}
+ self.filters = {
+ 'name': '',
+ 'pid': '',
+ 'cpu': 0,
+ 'memory': 0,
+ 'show_system': False
+ }
+ self.setup_ui()
+ self.start_monitoring()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Filter bar
+ filter_frame = QFrame()
+ filter_frame.setStyleSheet("""
+ QFrame {
+ background-color: #2f3136;
+ border-radius: 8px;
+ padding: 10px;
+ }
+ """)
+ filter_layout = QHBoxLayout(filter_frame)
+
+ # Search with regex support
+ search_container = QFrame()
+ search_layout = QHBoxLayout(search_container)
+
+ self.search_input = QLineEdit()
+ self.search_input.setPlaceholderText("Filter processes (supports regex)")
+ self.search_input.textChanged.connect(self.apply_filters)
+
+ self.regex_check = QCheckBox("Regex")
+ self.regex_check.toggled.connect(self.apply_filters)
+
+ search_layout.addWidget(self.search_input)
+ search_layout.addWidget(self.regex_check)
+
+ # Advanced filters
+ self.filter_combo = QComboBox()
+ self.filter_combo.addItems(['All', 'User', 'System', 'Android Apps'])
+ self.filter_combo.currentTextChanged.connect(self.apply_filters)
+
+ # Resource thresholds
+ self.cpu_threshold = QSpinBox()
+ self.cpu_threshold.setSuffix("% CPU")
+ self.cpu_threshold.valueChanged.connect(self.apply_filters)
+
+ self.memory_threshold = QSpinBox()
+ self.memory_threshold.setSuffix("MB")
+ self.memory_threshold.setMaximum(32000)
+ self.memory_threshold.valueChanged.connect(self.apply_filters)
+
+ filter_layout.addWidget(search_container)
+ filter_layout.addWidget(self.filter_combo)
+ filter_layout.addWidget(self.cpu_threshold)
+ filter_layout.addWidget(self.memory_threshold)
+
+ # Process table
+ self.process_table = QTableWidget()
+ self.process_table.setColumnCount(6)
+ self.process_table.setHorizontalHeaderLabels([
+ "PID", "Name", "CPU %", "Memory", "Status", "Path"
+ ])
+
+ # Style the table
+ self.process_table.setStyleSheet("""
+ QTableWidget {
+ background-color: #36393f;
+ border: none;
+ border-radius: 8px;
+ gridline-color: #2f3136;
+ }
+ QTableWidget::item {
+ padding: 5px;
+ border-bottom: 1px solid #2f3136;
+ }
+ QTableWidget::item:selected {
+ background-color: #7289da;
+ }
+ """)
+
+ # Set column widths
+ header = self.process_table.horizontalHeader()
+ header.setSectionResizeMode(0, QHeaderView.Fixed) # PID
+ header.setSectionResizeMode(1, QHeaderView.Stretch) # Name
+ header.setSectionResizeMode(2, QHeaderView.Fixed) # CPU
+ header.setSectionResizeMode(3, QHeaderView.Fixed) # Memory
+ header.setSectionResizeMode(4, QHeaderView.Fixed) # Status
+ header.setSectionResizeMode(5, QHeaderView.Stretch) # Path
+
+ self.process_table.setColumnWidth(0, 70) # PID
+ self.process_table.setColumnWidth(2, 80) # CPU
+ self.process_table.setColumnWidth(3, 100) # Memory
+ self.process_table.setColumnWidth(4, 100) # Status
+
+ # Context menu
+ self.process_table.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.process_table.customContextMenuRequested.connect(self.show_context_menu)
+
+ # Quick action buttons
+ action_layout = QHBoxLayout()
+
+ self.refresh_btn = QPushButton(qta.icon('fa5s.sync'), "Refresh")
+ self.refresh_btn.clicked.connect(self.refresh_processes)
+
+ self.kill_btn = QPushButton(qta.icon('fa5s.stop'), "Kill")
+ self.kill_btn.clicked.connect(self.kill_selected_process)
+
+ self.inject_btn = QPushButton(qta.icon('fa5s.syringe'), "Inject")
+ self.inject_btn.clicked.connect(self.inject_into_selected)
+
+ action_layout.addWidget(self.refresh_btn)
+ action_layout.addWidget(self.kill_btn)
+ action_layout.addWidget(self.inject_btn)
+ action_layout.addStretch()
+
+ # Status bar
+ status_bar = QFrame()
+ status_bar.setStyleSheet("""
+ QFrame {
+ background-color: #2f3136;
+ border-radius: 4px;
+ padding: 5px;
+ }
+ """)
+ status_layout = QHBoxLayout(status_bar)
+
+ self.process_count = QLabel("0 processes")
+ self.cpu_usage = QLabel("CPU: 0%")
+ self.memory_usage = QLabel("Memory: 0 MB")
+
+ status_layout.addWidget(self.process_count)
+ status_layout.addStretch()
+ status_layout.addWidget(self.cpu_usage)
+ status_layout.addWidget(self.memory_usage)
+
+ # Add all components
+ layout.addWidget(filter_frame)
+ layout.addWidget(self.process_table)
+ layout.addLayout(action_layout)
+ layout.addWidget(status_bar)
+
+ def start_monitoring(self):
+ self.update_timer = QTimer()
+ self.update_timer.timeout.connect(self.refresh_processes)
+ self.update_timer.start(2000) # Update every 2 seconds
+
+ def refresh_processes(self):
+ self.processes.clear()
+ total_cpu = 0
+ total_memory = 0
+
+ for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_info', 'status', 'exe']):
+ try:
+ info = proc.info
+ memory_mb = info['memory_info'].rss / 1024 / 1024
+ self.processes[info['pid']] = {
+ 'name': info['name'],
+ 'cpu': info['cpu_percent'],
+ 'memory': memory_mb,
+ 'status': info['status'],
+ 'path': info['exe'] or ''
+ }
+ total_cpu += info['cpu_percent']
+ total_memory += memory_mb
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ continue
+
+ self.update_table()
+ self.update_stats(total_cpu, total_memory)
+
+ def update_table(self):
+ self.process_table.setSortingEnabled(False)
+ self.process_table.setRowCount(0)
+
+ filtered_processes = self.filter_processes()
+
+ for pid, info in filtered_processes.items():
+ row = self.process_table.rowCount()
+ self.process_table.insertRow(row)
+
+ # PID
+ pid_item = QTableWidgetItem(str(pid))
+ pid_item.setTextAlignment(Qt.AlignCenter)
+
+ # Name
+ name_item = QTableWidgetItem(info['name'])
+
+ # CPU
+ cpu_item = QTableWidgetItem(f"{info['cpu']:.1f}%")
+ cpu_item.setTextAlignment(Qt.AlignCenter)
+
+ # Memory
+ memory_item = QTableWidgetItem(f"{info['memory']:.1f} MB")
+ memory_item.setTextAlignment(Qt.AlignCenter)
+
+ # Status
+ status_item = QTableWidgetItem(info['status'])
+ status_item.setTextAlignment(Qt.AlignCenter)
+
+ # Path
+ path_item = QTableWidgetItem(info['path'])
+
+ # Set items
+ self.process_table.setItem(row, 0, pid_item)
+ self.process_table.setItem(row, 1, name_item)
+ self.process_table.setItem(row, 2, cpu_item)
+ self.process_table.setItem(row, 3, memory_item)
+ self.process_table.setItem(row, 4, status_item)
+ self.process_table.setItem(row, 5, path_item)
+
+ # Color coding based on resource usage
+ if info['cpu'] > 50:
+ self.color_row(row, QColor(240, 71, 71, 50)) # Red
+ elif info['memory'] > 1000:
+ self.color_row(row, QColor(250, 166, 26, 50)) # Orange
+
+ self.process_table.setSortingEnabled(True)
+
+ def filter_processes(self):
+ filtered = {}
+ search_text = self.search_input.text().lower()
+
+ for pid, info in self.processes.items():
+ # Apply regex/text filter
+ if self.regex_check.isChecked():
+ try:
+ if not re.search(search_text, info['name'].lower()):
+ continue
+ except re.error:
+ continue
+ elif search_text and search_text not in info['name'].lower():
+ continue
+
+ # Apply type filter
+ if self.filter_combo.currentText() == 'User' and pid < 1000:
+ continue
+ elif self.filter_combo.currentText() == 'System' and pid >= 1000:
+ continue
+ elif self.filter_combo.currentText() == 'Android Apps' and not info['name'].startswith('com.'):
+ continue
+
+ # Apply resource thresholds
+ if info['cpu'] < self.cpu_threshold.value():
+ continue
+ if info['memory'] < self.memory_threshold.value():
+ continue
+
+ filtered[pid] = info
+
+ return filtered
+
+ def color_row(self, row, color):
+ for col in range(self.process_table.columnCount()):
+ item = self.process_table.item(row, col)
+ item.setBackground(color)
+
+ def update_stats(self, total_cpu, total_memory):
+ self.process_count.setText(f"{len(self.processes)} processes")
+ self.cpu_usage.setText(f"CPU: {total_cpu:.1f}%")
+ self.memory_usage.setText(f"Memory: {total_memory:.0f} MB")
+
+ def show_context_menu(self, position):
+ menu = QMenu()
+
+ kill_action = QAction("Kill Process", self)
+ kill_action.triggered.connect(self.kill_selected_process)
+
+ inject_action = QAction("Inject Script", self)
+ inject_action.triggered.connect(self.inject_into_selected)
+
+ menu.addAction(kill_action)
+ menu.addAction(inject_action)
+ menu.exec_(self.process_table.mapToGlobal(position))
+
+ def kill_selected_process(self):
+ selected = self.process_table.selectedItems()
+ if selected:
+ pid = int(self.process_table.item(selected[0].row(), 0).text())
+ try:
+ psutil.Process(pid).terminate()
+ self.refresh_processes()
+ except psutil.NoSuchProcess:
+ pass
+
+ def inject_into_selected(self):
+ selected = self.process_table.selectedItems()
+ if selected:
+ pid = int(self.process_table.item(selected[0].row(), 0).text())
+ self.process_selected.emit(pid)
\ No newline at end of file
diff --git a/src/gui/widgets/process_monitor.py b/src/gui/widgets/process_monitor.py
new file mode 100644
index 0000000..fe5ae7e
--- /dev/null
+++ b/src/gui/widgets/process_monitor.py
@@ -0,0 +1,335 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
+ QLabel, QLineEdit, QTableWidget, QTableWidgetItem,
+ QMenu, QAction, QComboBox, QCheckBox, QFrame,
+ QHeaderView, QStyle, QStyledItemDelegate, QToolButton, QMessageBox)
+from PyQt5.QtCore import Qt, pyqtSignal, QTimer, QSize
+from PyQt5.QtGui import QColor, QFont, QIcon
+import psutil
+import frida
+import re
+import qtawesome as qta
+from datetime import datetime
+import subprocess
+
+class ProcessInfoDelegate(QStyledItemDelegate):
+ def paint(self, painter, option, index):
+ if index.column() in [2, 3]: # CPU and Memory columns
+ value = float(index.data().replace('%', '').replace('MB', ''))
+ if value > 80:
+ option.backgroundBrush = QColor('#f04747')
+ elif value > 50:
+ option.backgroundBrush = QColor('#faa61a')
+ super().paint(painter, option, index)
+
+class ProcessMonitor(QWidget):
+ def __init__(self, main_window=None):
+ QWidget.__init__(self)
+ self.processes = {}
+ self.current_device = None
+ self.main_window = main_window
+ self.setup_ui()
+ self.start_monitoring()
+
+ def start_monitoring(self):
+ self.update_timer = QTimer()
+ self.update_timer.timeout.connect(self.refresh_processes)
+ self.update_timer.start(2000) # Update every 2 seconds
+
+ def stop_monitoring(self):
+ if hasattr(self, 'update_timer'):
+ self.update_timer.stop()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ # Device selection
+ device_frame = QFrame()
+ device_frame.setStyleSheet("""
+ QFrame {
+ background-color: #2f3136;
+ border-radius: 8px;
+ padding: 10px;
+ }
+ """)
+ device_layout = QHBoxLayout(device_frame)
+
+ self.device_combo = QComboBox()
+ self.device_combo.currentIndexChanged.connect(self.on_device_changed)
+
+ refresh_devices_btn = QPushButton(qta.icon('fa5s.sync'), "Refresh Devices")
+ refresh_devices_btn.clicked.connect(self.refresh_devices)
+
+ device_layout.addWidget(QLabel("Device:"))
+ device_layout.addWidget(self.device_combo)
+ device_layout.addWidget(refresh_devices_btn)
+
+ # Search and Filter Bar
+ filter_frame = QFrame()
+ filter_frame.setStyleSheet("""
+ QFrame {
+ background-color: #2f3136;
+ border-radius: 8px;
+ padding: 10px;
+ }
+ """)
+ filter_layout = QHBoxLayout(filter_frame)
+
+ # Process search with regex toggle
+ search_container = QFrame()
+ search_layout = QHBoxLayout(search_container)
+
+ self.search_input = QLineEdit()
+ self.search_input.setPlaceholderText("Filter processes (supports regex)")
+ self.search_input.textChanged.connect(self.apply_filters)
+
+ self.regex_check = QCheckBox("Regex")
+ self.regex_check.toggled.connect(self.apply_filters)
+
+ search_layout.addWidget(self.search_input)
+ search_layout.addWidget(self.regex_check)
+
+ # Advanced filters
+ self.filter_combo = QComboBox()
+ self.filter_combo.addItems(['All', 'User', 'System', 'Android Apps', 'High CPU', 'High Memory'])
+ self.filter_combo.currentTextChanged.connect(self.apply_filters)
+
+ filter_layout.addWidget(search_container)
+ filter_layout.addWidget(self.filter_combo)
+
+ # Process Table
+ self.process_table = QTableWidget()
+ self.process_table.setColumnCount(8)
+ self.process_table.setHorizontalHeaderLabels([
+ "PID", "Name", "CPU %", "Memory", "Status", "User", "Started", "Command Line"
+ ])
+
+ # Context menu
+ self.process_table.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.process_table.customContextMenuRequested.connect(self.show_context_menu)
+
+ # Action buttons
+ action_layout = QHBoxLayout()
+
+ self.refresh_btn = QPushButton(qta.icon('fa5s.sync'), "Refresh")
+ self.refresh_btn.clicked.connect(self.refresh_processes)
+
+ self.kill_btn = QPushButton(qta.icon('fa5s.stop'), "Kill")
+ self.kill_btn.clicked.connect(self.kill_selected_process)
+
+ self.inject_btn = QPushButton(qta.icon('fa5s.syringe'), "Open in Injector")
+ self.inject_btn.clicked.connect(self.open_in_injector_clicked)
+
+ action_layout.addWidget(self.refresh_btn)
+ action_layout.addWidget(self.kill_btn)
+ action_layout.addWidget(self.inject_btn)
+ action_layout.addStretch()
+
+ # Add all components
+ layout.addWidget(device_frame)
+ layout.addWidget(filter_frame)
+ layout.addWidget(self.process_table)
+ layout.addLayout(action_layout)
+
+ # Initial device scan
+ self.refresh_devices()
+
+ def refresh_devices(self):
+ self.device_combo.clear()
+ try:
+ devices = frida.enumerate_devices()
+ for device in devices:
+ if device.type == 'usb':
+ self.device_combo.addItem(f"📱 {device.name} (USB)", device.id)
+ elif device.type == 'remote':
+ self.device_combo.addItem(f"🌐 {device.name} (Remote)", device.id)
+ elif device.type == 'local':
+ self.device_combo.addItem(f"💻 {device.name} (Local)", device.id)
+ except Exception as e:
+ print(f"Error enumerating devices: {e}")
+
+ def on_device_changed(self, index):
+ if index >= 0:
+ self.current_device = self.device_combo.currentData()
+ self.refresh_processes()
+
+ def show_context_menu(self, position):
+ menu = QMenu()
+
+ kill_action = QAction("Kill Process", self)
+ kill_action.triggered.connect(self.kill_selected_process)
+
+ inject_action = QAction("Open in Injector", self)
+ inject_action.triggered.connect(self.open_in_injector_clicked)
+
+ details_action = QAction("Process Details", self)
+ details_action.triggered.connect(self.show_process_details)
+
+ menu.addAction(kill_action)
+ menu.addAction(inject_action)
+ menu.addAction(details_action)
+ menu.exec_(self.process_table.mapToGlobal(position))
+
+ def open_in_injector_clicked(self):
+ """Handle click on 'Open in Injector' button"""
+ if not self.main_window:
+ return
+
+ selected = self.process_table.selectedItems()
+ if selected:
+ row = selected[0].row()
+ pid = int(self.process_table.item(row, 0).text())
+ if self.current_device:
+ self.main_window.open_in_injector(self.current_device, pid)
+ else:
+ QMessageBox.warning(self, "Error", "No device selected!")
+
+ def refresh_processes(self):
+ self.process_table.setRowCount(0)
+ if not self.current_device:
+ return
+
+ try:
+ device = frida.get_device(self.current_device)
+
+ if device.type == 'local':
+ # For local processes, use psutil for more reliable info
+ for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_info', 'status', 'username', 'create_time', 'cmdline']):
+ try:
+ row = self.process_table.rowCount()
+ self.process_table.insertRow(row)
+
+ # Get process info
+ pid = proc.pid
+ name = proc.name()
+ cpu = proc.cpu_percent()
+ memory = proc.memory_info().rss / 1024 / 1024 # Convert to MB
+ status = proc.status()
+ user = proc.username()
+ started = datetime.fromtimestamp(proc.create_time()).strftime('%Y-%m-%d %H:%M:%S')
+ cmdline = ' '.join(proc.cmdline())
+
+ # Create items
+ items = [
+ QTableWidgetItem(str(pid)),
+ QTableWidgetItem(name),
+ QTableWidgetItem(f"{cpu:.1f}%"),
+ QTableWidgetItem(f"{memory:.1f} MB"),
+ QTableWidgetItem(status),
+ QTableWidgetItem(user),
+ QTableWidgetItem(started),
+ QTableWidgetItem(cmdline)
+ ]
+
+ # Set alignment
+ items[0].setTextAlignment(Qt.AlignCenter)
+ items[2].setTextAlignment(Qt.AlignCenter)
+ items[3].setTextAlignment(Qt.AlignCenter)
+ items[4].setTextAlignment(Qt.AlignCenter)
+
+ # Add items to row
+ for col, item in enumerate(items):
+ self.process_table.setItem(row, col, item)
+
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ continue
+
+ else:
+ # For ADB devices
+ try:
+ adb_output = subprocess.check_output(
+ ['adb', '-s', self.current_device, 'shell', 'ps'],
+ text=True
+ ).strip().split('\n')
+
+ for line in adb_output[1:]: # Skip header
+ parts = line.split()
+ if len(parts) >= 9:
+ row = self.process_table.rowCount()
+ self.process_table.insertRow(row)
+
+ pid = parts[1]
+ name = parts[-1]
+
+ items = [
+ QTableWidgetItem(pid),
+ QTableWidgetItem(name),
+ QTableWidgetItem("N/A"),
+ QTableWidgetItem("N/A"),
+ QTableWidgetItem(parts[7]),
+ QTableWidgetItem(parts[0]),
+ QTableWidgetItem("N/A"),
+ QTableWidgetItem("N/A")
+ ]
+
+ for col, item in enumerate(items):
+ self.process_table.setItem(row, col, item)
+
+ except subprocess.CalledProcessError as e:
+ print(f"ADB error: {e}")
+
+ except Exception as e:
+ print(f"Error refreshing processes: {e}")
+
+ def apply_filters(self):
+ search_text = self.search_input.text().lower()
+ filter_type = self.filter_combo.currentText()
+ use_regex = self.regex_check.isChecked()
+
+ for row in range(self.process_table.rowCount()):
+ show_row = True
+ name = self.process_table.item(row, 1).text().lower()
+ pid = int(self.process_table.item(row, 0).text())
+
+ # Apply text filter
+ if search_text:
+ if use_regex:
+ try:
+ if not re.search(search_text, name):
+ show_row = False
+ except re.error:
+ show_row = False
+ elif search_text not in name:
+ show_row = False
+
+ # Apply type filter
+ if filter_type == 'User' and pid < 1000:
+ show_row = False
+ elif filter_type == 'System' and pid >= 1000:
+ show_row = False
+ elif filter_type == 'Android Apps' and not name.startswith('com.'):
+ show_row = False
+ elif filter_type == 'High CPU':
+ cpu = float(self.process_table.item(row, 2).text().replace('%', ''))
+ if cpu < 50:
+ show_row = False
+ elif filter_type == 'High Memory':
+ memory = float(self.process_table.item(row, 3).text().replace('MB', ''))
+ if memory < 500:
+ show_row = False
+
+ self.process_table.setRowHidden(row, not show_row)
+
+ def kill_selected_process(self):
+ selected = self.process_table.selectedItems()
+ if selected:
+ row = selected[0].row()
+ pid = int(self.process_table.item(row, 0).text())
+ try:
+ if self.current_device == 'local':
+ psutil.Process(pid).terminate()
+ else:
+ subprocess.run(['adb', '-s', self.current_device, 'shell', 'kill', str(pid)])
+ self.refresh_processes()
+ except Exception as e:
+ print(f"Error killing process: {e}")
+
+ def show_process_details(self):
+ selected = self.process_table.selectedItems()
+ if selected:
+ row = selected[0].row()
+ details = "\n".join([
+ f"{self.process_table.horizontalHeaderItem(col).text()}: "
+ f"{self.process_table.item(row, col).text()}"
+ for col in range(self.process_table.columnCount())
+ ])
+ QMessageBox.information(self, "Process Details", details)
\ No newline at end of file
diff --git a/src/gui/widgets/process_panel.py b/src/gui/widgets/process_panel.py
new file mode 100644
index 0000000..12aea20
--- /dev/null
+++ b/src/gui/widgets/process_panel.py
@@ -0,0 +1,59 @@
+from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
+ QComboBox, QPushButton, QLabel)
+import frida
+import subprocess
+
+class ProcessPanel(QWidget):
+ def __init__(self):
+ super().__init__()
+ self.setup_ui()
+ self.current_device_id = None
+
+ def setup_ui(self):
+ layout = QHBoxLayout(self)
+
+ self.process_combo = QComboBox()
+ self.refresh_button = QPushButton("Refresh Processes")
+
+ layout.addWidget(QLabel("Select Process:"))
+ layout.addWidget(self.process_combo)
+ layout.addWidget(self.refresh_button)
+
+ self.refresh_button.clicked.connect(self.refresh_processes)
+
+ def update_device(self, device_id):
+ self.current_device_id = device_id
+ self.refresh_processes()
+
+ def refresh_processes(self):
+ if not self.current_device_id:
+ return
+
+ self.process_combo.clear()
+ try:
+ device = frida.get_device(self.current_device_id)
+ if device.type == 'local':
+ processes = device.enumerate_processes()
+ for process in processes:
+ self.process_combo.addItem(
+ f"{process.name} (PID: {process.pid})",
+ process.pid
+ )
+ else:
+ # For ADB devices
+ output = subprocess.check_output(
+ ['adb', '-s', self.current_device_id, 'shell', 'ps'],
+ text=True
+ ).strip().split('\n')
+
+ for line in output[1:]: # Skip header
+ parts = line.split()
+ if len(parts) >= 9:
+ pid = parts[1]
+ process_name = parts[-1]
+ self.process_combo.addItem(
+ f"{process_name} (PID: {pid})",
+ pid
+ )
+ except Exception as e:
+ print(f"Error refreshing processes: {str(e)}")
\ No newline at end of file
diff --git a/src/gui/widgets/script_editor.py b/src/gui/widgets/script_editor.py
new file mode 100644
index 0000000..813ebc6
--- /dev/null
+++ b/src/gui/widgets/script_editor.py
@@ -0,0 +1,25 @@
+from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit
+
+class ScriptEditorPanel(QWidget):
+ def __init__(self):
+ super().__init__()
+ self.setup_ui()
+
+ def setup_ui(self):
+ layout = QVBoxLayout(self)
+
+ self.editor = QTextEdit()
+ self.editor.setPlaceholderText("Enter your Frida script here...")
+
+ # Set default script template
+ self.editor.setPlainText('''Java.perform(function() {
+ console.log("Script loaded!");
+});''')
+
+ layout.addWidget(self.editor)
+
+ def get_script(self):
+ return self.editor.toPlainText()
+
+ def set_script(self, script):
+ self.editor.setPlainText(script)
\ No newline at end of file
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..0fffbf7
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,70 @@
+import sys
+import os
+from pathlib import Path
+
+def setup_qt_environment():
+ """Setup Qt environment variables and paths"""
+ try:
+ # Get the PyQt5 location
+ import PyQt5
+ pyqt_path = Path(PyQt5.__file__).parent
+
+ # Set environment variables
+ os.environ['QT_DEBUG_PLUGINS'] = '1'
+ os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = str(pyqt_path / 'Qt5' / 'plugins')
+
+ # Print debug info
+ print(f"PyQt5 path: {pyqt_path}")
+ print(f"Plugin path: {os.environ['QT_QPA_PLATFORM_PLUGIN_PATH']}")
+
+ # Verify plugin exists
+ cocoa_path = pyqt_path / 'Qt5' / 'plugins' / 'platforms' / 'libqcocoa.dylib'
+ if cocoa_path.exists():
+ print(f"Found cocoa plugin at: {cocoa_path}")
+ else:
+ print(f"Warning: Could not find cocoa plugin at: {cocoa_path}")
+
+ # Try alternate locations
+ alt_paths = [
+ pyqt_path / 'Qt' / 'plugins' / 'platforms' / 'libqcocoa.dylib',
+ Path('/opt/anaconda3/plugins/platforms/libqcocoa.dylib'),
+ Path('/opt/anaconda3/lib/python3.11/site-packages/PyQt5/Qt/plugins/platforms/libqcocoa.dylib')
+ ]
+
+ for path in alt_paths:
+ if path.exists():
+ print(f"Found cocoa plugin in alternate location: {path}")
+ os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = str(path.parent.parent)
+ break
+
+ except Exception as e:
+ print(f"Error setting up Qt environment: {e}")
+
+# Add project root to Python path
+project_root = Path(__file__).parent
+sys.path.append(str(project_root))
+
+# Setup Qt environment before importing PyQt
+setup_qt_environment()
+
+from PyQt5.QtWidgets import QApplication, QMessageBox
+from gui.main_window import FridaInjectorMainWindow
+from utils.themes import set_application_style
+
+def main():
+ try:
+ # Create application
+ app = QApplication(sys.argv)
+ set_application_style(app)
+
+ window = FridaInjectorMainWindow()
+ window.show()
+
+ sys.exit(app.exec_())
+ except Exception as e:
+ print(f"Error starting application: {e}")
+ QMessageBox.critical(None, "Error", f"Application failed to start: {str(e)}")
+ sys.exit(1)
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/src/utils/__pycache__/themes.cpython-311.pyc b/src/utils/__pycache__/themes.cpython-311.pyc
new file mode 100644
index 0000000..74eb79d
Binary files /dev/null and b/src/utils/__pycache__/themes.cpython-311.pyc differ
diff --git a/src/utils/themes.py b/src/utils/themes.py
new file mode 100644
index 0000000..4c808e4
--- /dev/null
+++ b/src/utils/themes.py
@@ -0,0 +1,172 @@
+from PyQt5.QtWidgets import QStyleFactory
+from PyQt5.QtGui import QPalette, QColor
+from PyQt5.QtCore import Qt
+
+# Discord-inspired color scheme
+DISCORD_COLORS = {
+ 'background': '#36393f',
+ 'secondary_bg': '#2f3136',
+ 'tertiary_bg': '#202225',
+ 'text': '#dcddde',
+ 'secondary_text': '#96989d',
+ 'accent': '#ec695c',
+ 'accent_hover': '#4752c4',
+ 'red': '#ed4245',
+ 'green': '#3ba55c'
+}
+
+STYLE_SHEET = """
+QMainWindow, QWidget {
+ background-color: """ + DISCORD_COLORS['background'] + """;
+ color: """ + DISCORD_COLORS['text'] + """;
+ font-family: 'Segoe UI', Arial, sans-serif;
+}
+
+QTabWidget::pane {
+ border: none;
+ background-color: """ + DISCORD_COLORS['background'] + """;
+}
+
+QTabWidget::tab-bar {
+ alignment: left;
+}
+
+QTabBar::tab {
+ background-color: """ + DISCORD_COLORS['tertiary_bg'] + """;
+ color: """ + DISCORD_COLORS['secondary_text'] + """;
+ padding: 8px 16px;
+ border: none;
+ min-width: 100px;
+}
+
+QTabBar::tab:selected {
+ background-color: """ + DISCORD_COLORS['background'] + """;
+ color: """ + DISCORD_COLORS['text'] + """;
+}
+
+QTabBar::tab:hover:!selected {
+ background-color: """ + DISCORD_COLORS['secondary_bg'] + """;
+}
+
+QPushButton {
+ background-color: """ + DISCORD_COLORS['accent'] + """;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-weight: bold;
+}
+
+QPushButton:hover {
+ background-color: """ + DISCORD_COLORS['accent_hover'] + """;
+}
+
+QPushButton:pressed {
+ background-color: """ + DISCORD_COLORS['accent'] + """;
+}
+
+QComboBox {
+ background-color: """ + DISCORD_COLORS['tertiary_bg'] + """;
+ border: none;
+ border-radius: 4px;
+ padding: 6px 12px;
+ color: """ + DISCORD_COLORS['text'] + """;
+ min-width: 150px;
+}
+
+QComboBox::drop-down {
+ border: none;
+ width: 20px;
+}
+
+QComboBox::down-arrow {
+ image: none;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid """ + DISCORD_COLORS['text'] + """;
+ margin-right: 8px;
+}
+
+QTextEdit {
+ background-color: """ + DISCORD_COLORS['tertiary_bg'] + """;
+ border: none;
+ border-radius: 4px;
+ padding: 8px;
+ color: """ + DISCORD_COLORS['text'] + """;
+ font-family: 'Consolas', 'Courier New', monospace;
+}
+
+QLabel {
+ color: """ + DISCORD_COLORS['text'] + """;
+ font-weight: bold;
+}
+
+QScrollBar:vertical {
+ border: none;
+ background-color: """ + DISCORD_COLORS['tertiary_bg'] + """;
+ width: 14px;
+ margin: 0;
+}
+
+QScrollBar::handle:vertical {
+ background-color: """ + DISCORD_COLORS['secondary_bg'] + """;
+ min-height: 30px;
+ border-radius: 7px;
+}
+
+QScrollBar::handle:vertical:hover {
+ background-color: """ + DISCORD_COLORS['accent'] + """;
+}
+
+QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical,
+QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
+ border: none;
+ background: none;
+ color: none;
+}
+
+QListWidget {
+ background-color: """ + DISCORD_COLORS['tertiary_bg'] + """;
+ border: none;
+ border-radius: 4px;
+ padding: 4px;
+}
+
+QListWidget::item {
+ padding: 8px;
+ border-radius: 4px;
+}
+
+QListWidget::item:hover {
+ background-color: """ + DISCORD_COLORS['secondary_bg'] + """;
+}
+
+QListWidget::item:selected {
+ background-color: """ + DISCORD_COLORS['accent'] + """;
+ color: white;
+}
+"""
+
+def set_application_style(app):
+ app.setStyle(QStyleFactory.create("Fusion"))
+
+ # Set the custom style sheet
+ app.setStyleSheet(STYLE_SHEET)
+
+ # Set up dark palette for system dialogs
+ dark_palette = QPalette()
+ dark_palette.setColor(QPalette.Window, QColor(DISCORD_COLORS['background']))
+ dark_palette.setColor(QPalette.WindowText, QColor(DISCORD_COLORS['text']))
+ dark_palette.setColor(QPalette.Base, QColor(DISCORD_COLORS['tertiary_bg']))
+ dark_palette.setColor(QPalette.AlternateBase, QColor(DISCORD_COLORS['secondary_bg']))
+ dark_palette.setColor(QPalette.ToolTipBase, QColor(DISCORD_COLORS['text']))
+ dark_palette.setColor(QPalette.ToolTipText, QColor(DISCORD_COLORS['text']))
+ dark_palette.setColor(QPalette.Text, QColor(DISCORD_COLORS['text']))
+ dark_palette.setColor(QPalette.Button, QColor(DISCORD_COLORS['accent']))
+ dark_palette.setColor(QPalette.ButtonText, Qt.white)
+ dark_palette.setColor(QPalette.BrightText, Qt.red)
+ dark_palette.setColor(QPalette.Link, QColor(DISCORD_COLORS['accent']))
+ dark_palette.setColor(QPalette.Highlight, QColor(DISCORD_COLORS['accent']))
+ dark_palette.setColor(QPalette.HighlightedText, Qt.white)
+
+ app.setPalette(dark_palette)
\ No newline at end of file