From b84b748c03023fd50eb69ed17c9408bf47f25d6b Mon Sep 17 00:00:00 2001 From: RonaldsonBellande Date: Sat, 18 Jan 2025 11:56:17 -0500 Subject: [PATCH] information --- .gitignore | 2 + Cargo.toml | 88 + LICENSE | 674 ++++++++ README.md | 416 +++++ dependencies.bellande | 4 + git_scripts/.gitignore | 3 + make_rust_executable.bellos | 1 + make_rust_executable.sh | 1 + src/audit/audit.rs | 253 +++ src/audit/mod.rs | 2 + src/audit/security_audit.rs | 812 +++++++++ .../authentication.rs | 123 ++ src/authentication_compliance/complication.rs | 1503 +++++++++++++++++ src/authentication_compliance/mod.rs | 2 + src/bell.rs | 213 +++ src/command/command.rs | 693 ++++++++ src/command/mod.rs | 1 + src/config/config.rs | 334 ++++ src/config/mod.rs | 1 + src/hsm/hsm.rs | 820 +++++++++ src/hsm/mod.rs | 1 + src/network/mod.rs | 1 + src/network/network.rs | 522 ++++++ src/user_privilege/mod.rs | 2 + src/user_privilege/privilege.rs | 314 ++++ src/user_privilege/user.rs | 675 ++++++++ 26 files changed, 7461 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dependencies.bellande create mode 100644 git_scripts/.gitignore create mode 100755 make_rust_executable.bellos create mode 100755 make_rust_executable.sh create mode 100644 src/audit/audit.rs create mode 100644 src/audit/mod.rs create mode 100644 src/audit/security_audit.rs create mode 100644 src/authentication_compliance/authentication.rs create mode 100644 src/authentication_compliance/complication.rs create mode 100644 src/authentication_compliance/mod.rs create mode 100644 src/bell.rs create mode 100644 src/command/command.rs create mode 100644 src/command/mod.rs create mode 100644 src/config/config.rs create mode 100644 src/config/mod.rs create mode 100644 src/hsm/hsm.rs create mode 100644 src/hsm/mod.rs create mode 100644 src/network/mod.rs create mode 100644 src/network/network.rs create mode 100644 src/user_privilege/mod.rs create mode 100644 src/user_privilege/privilege.rs create mode 100644 src/user_privilege/user.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..30e1484 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,88 @@ +[package] +name = "bell_system" +version = "0.0.1" +edition = "2021" +authors = ["Ronaldson Bellande "] +description = "Advanced privilege escalation system with multi-level security and compliance features" +license = "GPL-3.0-or-later" +repository = "https://github.com/Architecture-Mechanism/bell" +documentation = "https://bellande-architecture-mechanism-research-innovation-center.org/bell/docs" +readme = "README.md" +keywords = ["privilege-escalation", "security", "authentication", "system", "bellande_bell_system"] +categories = ["command-line-utilities", "authentication"] + +[lib] +name = "bell" +path = "src/bell.rs" + +[dependencies] +# Core async runtime +tokio = { version = "1.28", features = ["full"] } + +# Command line parsing +structopt = "0.3.26" + +# Logging and monitoring +log = "0.4.17" +log4rs = "1.2.0" +sysinfo = "0.29" + +# Error handling +thiserror = "1.0.40" +anyhow = "1.0" + +# System interaction +nix = "0.26.2" +libc = { version = "0.2.144", features = ["extra_traits"] } +libseccomp = "0.3" +syscallz = "0.17" + +# Authentication and security +totp-rs = { version = "5.0.2", features = ["gen_secret"] } +rand = "0.8.5" +rand_core = "0.6.4" +argon2 = { version = "0.5", features = ["std", "password-hash"] } + +# Database +rusqlite = { version = "0.29", features = ["bundled"] } + +# Serialization and configuration +serde = { version = "1.0.163", features = ["derive"] } +serde_json = "1.0" +toml = "0.7.3" + +# Email +lettre = { version = "0.10", features = ["rustls-tls", "smtp-transport"] } + +# Time +chrono = { version = "0.4", features = ["serde"] } + +# Cryptography +aes-gcm = { version = "0.10.1", features = ["std"] } +sha2 = "0.10.6" + +# Networking +ipnetwork = "0.20.0" +reqwest = { version = "0.11", features = ["json", "rustls-tls"] } +walkdir = "2.4" +regex = "1.5" +local-ip-address = "0.5" +base64 = "0.21" + +[target.'cfg(unix)'.dependencies] +users = "0.11.0" + +[features] +default = ["standard"] +standard = [] +enterprise = ["hsm", "advanced-audit", "network-isolation"] +hsm = [] +advanced-audit = [] +network-isolation = [] + +[package.metadata.bellande] +organization = "Architecture-Mechanism" +project-type = "system-security" +supported-os = ["bellandeos", "linux", "macos"] +minimum-rust-version = "1.70.0" +security-contact = "security@bellande-architecture-mechanism-research-innovation-center.org" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c1e268a --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + bell Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + bell Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7866f0 --- /dev/null +++ b/README.md @@ -0,0 +1,416 @@ +# bell +--- +title: Bell Privilege Escalation System +author: Bellande Architecture Mechanism Research Innovation Center +version: 0.0.1 +date: 2024 +--- + +## Website Crates +- https://crates.io/crates/bell_system + +### Installation +- `cargo add bell_system` + +``` +Name: bell_system +Summary: Bell is a comprehensive privilege escalation system designed for secure command execution with granular access controls, robust auditing, and compliance features +Home-page: github.com/Architecture-Mechanism/bell +Author: Ronaldson Bellande +Author-email: ronaldsonbellande@gmail.com +License: GNU General Public License v3.0 +``` + +# Bell Privilege Escalation System + +Bell is a comprehensive privilege escalation system designed for secure command execution with granular access controls, robust auditing, and compliance features + +## Table of Contents + +1. [Overview](#overview) +2. [Installation](#installation) +3. [Usage](#usage) +4. [Configuration](#configuration) +5. [Security Features](#security-features) +6. [OS-Specific Features](#os-specific-features) +7. [Best Practices](#best-practices) +8. [Troubleshooting](#troubleshooting) +9. [API Reference](#api-reference) +10. [License](#license) + +## Overview + +Bell is an advanced privilege escalation system designed for secure enterprise environments. It integrates hardware security modules, multi-factor authentication, and comprehensive audit logging. + +### Key Features + +* Multi-level privilege management +* Hardware Security Module (HSM) integration +* Two-factor authentication (TOTP) +* Network isolation capabilities +* Fine-grained access control +* Comprehensive audit logging +* Cross-platform support (Linux, MacOS, BellandeOS) + +### Architecture +``` ++------------------+ +------------------+ +------------------+ +| Bell Client | --> | Bell Core | --> | Security Layer | ++------------------+ +------------------+ +------------------+ + | | | + v v v ++------------------+ +------------------+ +------------------+ +| Auth Module | | HSM Module | | Audit Module | ++------------------+ +------------------+ +------------------+ +``` + +## Installation +Prerequisites + +- Rust 1.70 or higher +- OpenSSL development libraries +- Hardware Security Module (optional) +- TOTP device/app for 2FA + +git clone https://github.com/Architecture-Mechanism/bell.git +cd bell-system + +# Build in release mode +cargo build --release + +# Run tests +cargo test --all-features + +# Install system-wide +sudo make install + + +## System Requirements + +### Hardware Requirements + +| Component | Minimum Specification | +|-----------|---------------------| +| CPU | x86_64 or ARM64 | +| RAM | 512MB | +| Disk Space | 1GB free | + +### Operating System Support + +| OS | Minimum Version | +|----|----------------| +| Linux | 4.19+ | +| MacOS | 10.15+ | +| BellandeOS | 0.1+ | + +# Running Commands +``` +bell run --privilege-level --command --args +bell run --privilege-level admin --command "/usr/bin/systemctl" --args "restart" "nginx" +bell run --privilege-level root --command "/usr/bin/apt" --args "update" +``` +# User Management +## Adding Users + +``` +bell user add --privilege + +# Examples +bell user add johndoe --privilege admin +bell user add service-account --privilege user + +``` +## Modifying Users +``` +# Change password +bell user change-password + +# Change privilege +bell user change-privilege + +# Remove user +bell user remove +``` + +## Group Management +``` +# Add to group +bell user add-to-group + +# Remove from group +bell user remove-from-group + +# List group members +bell group list-members +``` +## File Locations +``` +/etc/bell/ +├── config.bellande # Main configuration +├── users/ # User configurations +│ ├── admin.bellande +│ └── service.bellande +├── groups/ # Group configurations +│ ├── admins.bellande +│ └── services.bellande +└── security/ # Security policies + ├── policy.bellande + └── rules.bellande +``` + +## Section Management +``` +# View active sessions +bell session list + +# Terminate session +bell session terminate + +# Refresh session +bell session refresh +``` + +## Log Management +``` +# View logs +bell logs view --level error --since "1 hour ago" + +# Export logs +bell logs export --format json --start "2024-01-01" --end "2024-01-31" + +# Analyze logs +bell logs analyze --pattern "failed_auth" --report detailed +``` + +## MacOS Intergration +``` +# FileVault management +bell run --privilege-level admin --command "fdesetup" --args "status" + +# SIP verification +bell run --privilege-level bell --command "csrutil" --args "status" + +# Keychain access +bell run --privilege-level admin --command "security" --args "list-keychains" +``` + +## Linux Security +``` +# SELinux management +bell run --privilege-level admin --command "semanage" --args "login" "-l" + +# AppArmor profiles +bell run --privilege-level root --command "aa-status" + +# Kernel parameters +bell run --privilege-level bell --command "sysctl" --args "-a" +``` + +## BellandeOS Features +``` +# Security status +bell run --privilege-level bell --command "bellctl" --args "security" "status" + +# Kernel protection +bell run --privilege-level admin --command "bellctl" --args "kernel" "protect" + +# System integrity +bell run --privilege-level root --command "bellctl" --args "verify" "system" +``` + +## Bellande Operating System Access +``` +EXTENDED 5-LEVEL PERMISSION SYSTEM (77777) +======================================== + +BASIC PERMISSION VALUES +---------------------- +Read (r) = 4 +Write (w) = 2 +Execute (x) = 1 + +PERMISSION NUMBER MEANINGS +------------------------ +0 = --- = no access +1 = --x = execute only +2 = -w- = write only +3 = -wx = write and execute +4 = r-- = read only +5 = r-x = read and execute +6 = rw- = read and write +7 = rwx = read, write, and execute (full access) + +POSITION MEANINGS (LEFT TO RIGHT) +------------------------------- +Position 1 (leftmost) = Owner/Bell +Position 2 = Root +Position 3 = Administration +Position 4 = Group +Position 5 (rightmost)= User + +STANDARD PERMISSION: 77531 +------------------------- +Owner (7) = rwx = 4+2+1 = 7 +Root (7) = rwx = 4+2+1 = 7 +Administration (5) = r-x = 4+0+1 = 5 +Group (3) = -wx = 0+2+1 = 3 +User (1) = --x = 0+0+1 = 1 + +DETAILED ACCESS LEVELS +-------------------- +OWNER/BELL (Position 1) +- Value: 7 (rwx) +- Calculation: 4(read) + 2(write) + 1(execute) = 7 +- Access: + * All system files and directories + * Core components + * Kernel level access + * Hardware level access + * Can override all permissions + * Complete system control + +ROOT (Position 2) +- Value: 7 (rwx) +- Calculation: 4(read) + 2(write) + 1(execute) = 7 +- Access: + * System files + * Configuration files + * Installation files + * Startup sequences + * Cannot access core components + * Cannot modify kernel + +ADMINISTRATION (Position 3) +- Value: 5 (r-x) +- Calculation: 4(read) + 0(write) + 1(execute) = 5 +- Access: + * Read system configurations + * Execute administrative tasks + * Manage users + * Cannot modify system files + * No core component access + * No kernel modifications + +GROUP (Position 4) +- Value: 3 (-wx) +- Calculation: 0(read) + 2(write) + 1(execute) = 3 +- Access: + * Modify group files + * Execute group programs + * Share within group + * No read outside group + * No system modifications + * Limited to group scope + +USER (Position 5) +- Value: 1 (--x) +- Calculation: 0(read) + 0(write) + 1(execute) = 1 +- Access: + * Execute allowed programs + * Access own directory + * Use basic utilities + * No system modifications + * No file modifications + * No read access outside home + +COMMON PERMISSION COMBINATIONS +---------------------------- +77000 - System Critical Files +Owner: 7 (rwx) = 4+2+1 : Full control +Root: 7 (rwx) = 4+2+1 : Full control +Admin: 0 (---) = 0+0+0 : No access +Group: 0 (---) = 0+0+0 : No access +User: 0 (---) = 0+0+0 : No access +Use: Core system files, kernel components + +77530 - Administrative Tools +Owner: 7 (rwx) = 4+2+1 : Full control +Root: 7 (rwx) = 4+2+1 : Full control +Admin: 5 (r-x) = 4+0+1 : Read + Execute +Group: 3 (-wx) = 0+2+1 : Write + Execute +User: 0 (---) = 0+0+0 : No access +Use: System management tools, configuration files + +75531 - Standard Applications +Owner: 7 (rwx) = 4+2+1 : Full control +Root: 5 (r-x) = 4+0+1 : Read + Execute +Admin: 5 (r-x) = 4+0+1 : Read + Execute +Group: 3 (-wx) = 0+2+1 : Write + Execute +User: 1 (--x) = 0+0+1 : Execute only +Use: Standard applications, user programs + +PERMISSION GUIDELINES +------------------- +1. New Files/Directories + - Start restrictive (77000 for system) + - Add permissions as needed + - Document changes + +2. Directory Requirements + - Need execute (x) to access + - Need read (r) to list contents + - Need write (w) to create/delete + +3. Security Practices + - Use minimum needed permissions + - Regular permission checks + - Document all changes + - Monitor access patterns + +4. Important Rules + - Higher positions override lower + - Cannot exceed upper level permissions + - Execute needed for directories + - Write permission alone is rarely used + +EXAMPLES AND USE CASES +--------------------- +77777 - NOT RECOMMENDED +- Gives full access to all levels +- Security risk +- Never use in production + +77531 - STANDARD SECURE +- Owner: Full control +- Root: Full control +- Admin: Limited control +- Group: Write in scope +- User: Execute only + +77000 - SYSTEM FILES +- Only Owner and Root access +- Maximum security +- Use for critical files + +75531 - USER APPLICATIONS +- Limited Root access +- Admin can manage +- Group collaboration +- User can execute +``` + +# Command Line +``` +bell [OPTIONS] COMMAND [ARGS] + +Commands: + run Execute privileged command + user User management + group Group management + session Session management + logs Log management + debug Debug tools + help Show help information + +Options: + -d, --debug Enable debug mode + -c, --config Config file location + -q, --quiet Suppress output + -v, --version Show version + -h, --help Show help +``` +## License +Bell is distributed under the [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html), see [LICENSE](https://github.com/Architecture-Mechanism/bell/blob/main/LICENSE) and [NOTICE](https://github.com/Architecture-Mechanism/bell/blob/main/LICENSE) for more information. + +## Code of Conduct +Bell is distributed under the [CODE_OF_CONDUCT](https://github.com/Architecture-Mechanism/bell/blob/main/CODE_OF_CONDUCT.md) and [NOTICE](https://github.com/Architecture-Mechanism/bell/blob/main/CODE_OF_CONDUCT.md) for more information. diff --git a/dependencies.bellande b/dependencies.bellande new file mode 100644 index 0000000..1906968 --- /dev/null +++ b/dependencies.bellande @@ -0,0 +1,4 @@ +glob: "0.3.0" +tempfile: "3.2" +shellexpand: "3.1.0" +meval: "0.2" diff --git a/git_scripts/.gitignore b/git_scripts/.gitignore new file mode 100644 index 0000000..e5a7a9c --- /dev/null +++ b/git_scripts/.gitignore @@ -0,0 +1,3 @@ +fix_errors.sh +push.sh +repository_recal.sh diff --git a/make_rust_executable.bellos b/make_rust_executable.bellos new file mode 100755 index 0000000..b4f210b --- /dev/null +++ b/make_rust_executable.bellos @@ -0,0 +1 @@ +bellande_rust_executable -d dependencies.bellande -s src -m bellos.rs -o executable/bellos diff --git a/make_rust_executable.sh b/make_rust_executable.sh new file mode 100755 index 0000000..b4f210b --- /dev/null +++ b/make_rust_executable.sh @@ -0,0 +1 @@ +bellande_rust_executable -d dependencies.bellande -s src -m bellos.rs -o executable/bellos diff --git a/src/audit/audit.rs b/src/audit/audit.rs new file mode 100644 index 0000000..a69d764 --- /dev/null +++ b/src/audit/audit.rs @@ -0,0 +1,253 @@ +// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use chrono::{DateTime, Local, Utc}; +use lettre::message::header::ContentType; +use lettre::transport::smtp::authentication::Credentials; +use lettre::{Message, SmtpTransport, Transport}; +use rusqlite::{params, Connection}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct AuditConfig { + log_file: PathBuf, + database_file: PathBuf, + max_log_size: u64, + rotation_count: u32, + alert_email: String, + smtp_server: String, + smtp_port: u16, + smtp_username: String, + smtp_password: String, + critical_events: Vec, +} + +impl Default for AuditConfig { + fn default() -> Self { + let os_specific_path = match std::env::consts::OS { + "macos" => PathBuf::from("/var/log/bell"), + "linux" => PathBuf::from("/var/log/bell"), + "bellandeos" => PathBuf::from("/bell/log"), + _ => PathBuf::from("./log"), + }; + + Self { + log_file: os_specific_path.join("audit.log"), + database_file: os_specific_path.join("audit.db"), + max_log_size: 10 * 1024 * 1024, // 10MB + rotation_count: 5, + alert_email: "admin@bellande-architecture-mechanism-research-innovation-center.org" + .to_string(), + smtp_server: "smtp.bellande-architecture-mechanism-research-innovation-center.org" + .to_string(), + smtp_port: 587, + smtp_username: "alerts@bellande-architecture-mechanism.org".to_string(), + smtp_password: "your_secure_password".to_string(), + critical_events: vec![ + "AUTHENTICATION_FAILURE".to_string(), + "PERMISSION_DENIED".to_string(), + "SUSPICIOUS_ACTIVITY".to_string(), + "SECURITY_BREACH".to_string(), + "SYSTEM_MODIFICATION".to_string(), + ], + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AuditEvent { + timestamp: DateTime, + user: String, + event: String, + details: String, + system: String, + process_id: u32, + severity: EventSeverity, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub enum EventSeverity { + Info, + Warning, + Critical, + Emergency, +} + +pub async fn log_audit_event(event: &str, user: &str, details: &str) -> Result<()> { + let config = AuditConfig::default(); + let audit_event = create_audit_event(event, user, details); + + // Ensure log directory exists + if let Some(parent) = config.log_file.parent() { + fs::create_dir_all(parent).context("Failed to create log directory")?; + } + + // Check log rotation + check_and_rotate_logs(&config).await?; + + // Write to log file + write_to_log_file(&config, &audit_event).await?; + + // Write to database + log_to_database(&config, &audit_event).await?; + + // Send alert if critical + if is_critical_event(&config, event) { + send_alert(&config, &audit_event).await?; + } + + Ok(()) +} + +fn create_audit_event(event: &str, user: &str, details: &str) -> AuditEvent { + AuditEvent { + timestamp: Utc::now(), + user: user.to_string(), + event: event.to_string(), + details: details.to_string(), + system: std::env::consts::OS.to_string(), + process_id: std::process::id(), + severity: determine_severity(event), + } +} + +async fn write_to_log_file(config: &AuditConfig, event: &AuditEvent) -> Result<()> { + let mut file = OpenOptions::new() + .append(true) + .create(true) + .open(&config.log_file) + .context("Failed to open audit log file")?; + + let log_entry = format!( + "{} - User: {} - Event: {} - Details: {} - System: {} - PID: {} - Severity: {:?}\n", + event.timestamp.with_timezone(&Local), + event.user, + event.event, + event.details, + event.system, + event.process_id, + event.severity + ); + + file.write_all(log_entry.as_bytes()) + .context("Failed to write to audit log")?; + + Ok(()) +} + +async fn log_to_database(config: &AuditConfig, event: &AuditEvent) -> Result<()> { + let conn = Connection::open(&config.database_file).context("Failed to open database")?; + + conn.execute( + "CREATE TABLE IF NOT EXISTS audit_log ( + id INTEGER PRIMARY KEY, + timestamp TEXT NOT NULL, + user TEXT NOT NULL, + event TEXT NOT NULL, + details TEXT NOT NULL, + system TEXT NOT NULL, + process_id INTEGER NOT NULL, + severity TEXT NOT NULL + )", + [], + ) + .context("Failed to create audit_log table")?; + + conn.execute( + "INSERT INTO audit_log (timestamp, user, event, details, system, process_id, severity) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![ + event.timestamp.to_rfc3339(), + event.user, + event.event, + event.details, + event.system, + event.process_id, + format!("{:?}", event.severity) + ], + ) + .context("Failed to insert log entry into database")?; + + Ok(()) +} + +fn is_critical_event(config: &AuditConfig, event: &str) -> bool { + config.critical_events.contains(&event.to_string()) +} + +fn determine_severity(event: &str) -> EventSeverity { + match event { + "AUTHENTICATION_FAILURE" | "PERMISSION_DENIED" => EventSeverity::Warning, + "SUSPICIOUS_ACTIVITY" | "SECURITY_BREACH" => EventSeverity::Critical, + "SYSTEM_MODIFICATION" => EventSeverity::Emergency, + _ => EventSeverity::Info, + } +} + +async fn send_alert(config: &AuditConfig, event: &AuditEvent) -> Result<()> { + let email = Message::builder() + .from(config.smtp_username.parse().context("Invalid from address")?) + .to(config.alert_email.parse().context("Invalid to address")?) + .subject(format!("Critical Security Alert: {}", event.event)) + .header(ContentType::TEXT_PLAIN) + .body(format!( + "Critical security event detected:\n\nTimestamp: {}\nUser: {}\nEvent: {}\nDetails: {}\nSystem: {}\nProcess ID: {}\nSeverity: {:?}", + event.timestamp.with_timezone(&Local), + event.user, + event.event, + event.details, + event.system, + event.process_id, + event.severity + )) + .context("Failed to build email")?; + + let creds = Credentials::new(config.smtp_username.clone(), config.smtp_password.clone()); + + let mailer = SmtpTransport::relay(&config.smtp_server) + .context("Failed to create SMTP transport")? + .credentials(creds) + .port(config.smtp_port) + .build(); + + mailer.send(&email).context("Failed to send email")?; + + Ok(()) +} + +async fn check_and_rotate_logs(config: &AuditConfig) -> Result<()> { + let metadata = fs::metadata(&config.log_file)?; + + if metadata.len() >= config.max_log_size { + for i in (1..config.rotation_count).rev() { + let current = config.log_file.with_extension(format!("log.{}", i)); + let next = config.log_file.with_extension(format!("log.{}", i + 1)); + if current.exists() { + fs::rename(current, next)?; + } + } + + let backup = config.log_file.with_extension("log.1"); + fs::rename(&config.log_file, backup)?; + fs::File::create(&config.log_file)?; + } + + Ok(()) +} diff --git a/src/audit/mod.rs b/src/audit/mod.rs new file mode 100644 index 0000000..0396e69 --- /dev/null +++ b/src/audit/mod.rs @@ -0,0 +1,2 @@ +pub mod audit; +pub mod security_audit; diff --git a/src/audit/security_audit.rs b/src/audit/security_audit.rs new file mode 100644 index 0000000..1f9e966 --- /dev/null +++ b/src/audit/security_audit.rs @@ -0,0 +1,812 @@ +// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::collections::{HashMap, HashSet}; +use std::fs::{self, File}; +use std::io::BufReader; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::Duration; + +use anyhow::{Context, Result}; +use log::info; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::os::unix::fs::PermissionsExt; +use sysinfo::{ProcessExt, System, SystemExt}; +use walkdir::WalkDir; + +use crate::audit::audit::log_audit_event; +use crate::config::config::Config; + +#[derive(Debug, Serialize, Deserialize)] +pub struct SecurityAuditConfig { + pub critical_paths: Vec, + pub suspicious_process_patterns: Vec, + pub allowed_ports: HashSet, + pub file_hash_database: PathBuf, + pub scan_interval: Duration, +} + +impl Default for SecurityAuditConfig { + fn default() -> Self { + SecurityAuditConfig { + critical_paths: get_os_critical_paths(), + suspicious_process_patterns: vec![ + "crypto".to_string(), + "miner".to_string(), + "suspicious".to_string(), + ], + allowed_ports: [80, 443, 22, 53].iter().cloned().collect(), + file_hash_database: PathBuf::from("file_hashes.db"), + scan_interval: Duration::from_secs(3600), + } + } +} + +fn get_os_critical_paths() -> Vec { + match std::env::consts::OS { + "macos" => vec![ + PathBuf::from("/etc"), + PathBuf::from("/System"), + PathBuf::from("/usr/local/bin"), + ], + "linux" => vec![ + PathBuf::from("/etc"), + PathBuf::from("/bin"), + PathBuf::from("/sbin"), + ], + "bellandeos" => vec![ + PathBuf::from("/bell/etc"), + PathBuf::from("/bell/bin"), + PathBuf::from("/bell/security"), + ], + _ => vec![], + } +} + +/// Performs a comprehensive security audit of the system +pub async fn perform_security_audit(config: &Config) -> Result<()> { + let audit_config = SecurityAuditConfig::default(); + info!("Starting security audit for {}", std::env::consts::OS); + + // Check for system updates + check_system_updates().await?; + + // Scan for vulnerabilities + scan_for_vulnerabilities(&audit_config).await?; + + // Check for suspicious processes + check_suspicious_processes(&audit_config).await?; + + // Check for unauthorized users + check_unauthorized_users(config).await?; + + // Check for open ports + check_open_ports(&audit_config).await?; + + // Check file integrity + check_file_integrity(&audit_config).await?; + + // OS-specific security checks + perform_os_specific_checks().await?; + + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!("Completed security audit on {}", std::env::consts::OS), + ) + .await?; + + Ok(()) +} + +async fn check_system_updates() -> Result<()> { + match std::env::consts::OS { + "macos" => { + let output = Command::new("softwareupdate") + .arg("--list") + .output() + .context("Failed to check for macOS updates")?; + + if !output.stdout.is_empty() { + log_audit_event("SECURITY_AUDIT", "SYSTEM", "macOS updates available").await?; + } + } + "linux" => { + let output = Command::new("apt") + .args(&["list", "--upgradable"]) + .output() + .context("Failed to check for Linux updates")?; + + if !output.stdout.is_empty() { + log_audit_event("SECURITY_AUDIT", "SYSTEM", "Linux updates available").await?; + } + } + "bellandeos" => { + let output = Command::new("bellctl") + .args(&["update", "check"]) + .output() + .context("Failed to check for BellandeOS updates")?; + + if !output.stdout.is_empty() { + log_audit_event("SECURITY_AUDIT", "SYSTEM", "BellandeOS updates available").await?; + } + } + _ => anyhow::bail!("Unsupported operating system"), + } + + Ok(()) +} + +async fn scan_for_vulnerabilities(config: &SecurityAuditConfig) -> Result<()> { + log_audit_event("SECURITY_AUDIT", "SYSTEM", "Starting vulnerability scan").await?; + + // Check for known vulnerable software versions + check_software_versions().await?; + + // Check for common misconfigurations + check_common_misconfigurations(config).await?; + + // Check for weak permissions + check_permissions(config).await?; + + log_audit_event("SECURITY_AUDIT", "SYSTEM", "Vulnerability scan completed").await?; + Ok(()) +} + +async fn check_software_versions() -> Result<()> { + // Check OpenSSL version + let openssl_version = Command::new("openssl") + .arg("version") + .output() + .context("Failed to check OpenSSL version")?; + + if !openssl_version.status.success() { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + "Warning: Unable to verify OpenSSL version", + ) + .await?; + } + + Ok(()) +} + +async fn check_common_misconfigurations(config: &SecurityAuditConfig) -> Result<()> { + for path in &config.critical_paths { + check_path_permissions(path).await?; + } + + // Check world-writable files + check_world_writable_files().await?; + + // Check for dangerous SUID/SGID binaries + check_suid_binaries().await?; + + Ok(()) +} + +async fn check_permissions(config: &SecurityAuditConfig) -> Result<()> { + for path in &config.critical_paths { + let metadata = fs::metadata(path).context("Failed to get path metadata")?; + let mode = metadata.permissions().mode(); + + if mode & 0o777 > 0o755 { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!("Excessive permissions found on: {:?}", path), + ) + .await?; + } + } + Ok(()) +} + +async fn check_path_permissions(path: &Path) -> Result<()> { + let metadata = fs::metadata(path).context("Failed to get path metadata")?; + let mode = metadata.permissions().mode(); + + // Check for excessive permissions + if mode & 0o777 > 0o755 { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!( + "Warning: Excessive permissions ({:o}) on path: {:?}", + mode & 0o777, + path + ), + ) + .await?; + } + + // Check owner/group + if mode & 0o7000 != 0 { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!( + "Warning: Special bits ({:o}) set on path: {:?}", + mode & 0o7000, + path + ), + ) + .await?; + } + + Ok(()) +} + +async fn check_world_writable_files() -> Result<()> { + let critical_directories = match std::env::consts::OS { + "macos" => vec!["/etc", "/usr", "/bin", "/sbin", "/System"], + "linux" => vec!["/etc", "/usr", "/bin", "/sbin", "/lib", "/boot"], + "bellandeos" => vec!["/bell/etc", "/bell/bin", "/bell/lib", "/bell/security"], + _ => vec![], + }; + + for dir in critical_directories { + for entry in WalkDir::new(dir) + .follow_links(false) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if let Ok(metadata) = fs::metadata(path) { + let mode = metadata.permissions().mode(); + + // Check for world-writable permissions (others write permission) + if mode & 0o002 != 0 { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!("Warning: World-writable file found: {:?}", path), + ) + .await?; + } + } + } + } + + Ok(()) +} + +async fn check_suid_binaries() -> Result<()> { + let critical_directories = match std::env::consts::OS { + "macos" => vec!["/usr/bin", "/usr/sbin", "/usr/local/bin"], + "linux" => vec!["/usr/bin", "/usr/sbin", "/usr/local/bin", "/usr/local/sbin"], + "bellandeos" => vec!["/bell/bin", "/bell/sbin", "/bell/local/bin"], + _ => vec![], + }; + + // Known safe SUID binaries + let safe_suid_binaries = HashSet::from([ + "ping", + "su", + "sudo", + "passwd", + "mount", + "umount", + "fusermount", + "newgrp", + "chsh", + "gpasswd", + ]); + + for dir in critical_directories { + for entry in WalkDir::new(dir) + .follow_links(false) + .into_iter() + .filter_map(|e| e.ok()) + { + let path = entry.path(); + if let Ok(metadata) = fs::metadata(path) { + let mode = metadata.permissions().mode(); + + // Check for SUID/SGID bits + if mode & 0o6000 != 0 { + // Get binary name + let binary_name = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("unknown"); + + // If it's not in our safe list, log it + if !safe_suid_binaries.contains(binary_name) { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!( + "Warning: SUID/SGID binary found: {:?} (mode: {:o})", + path, + mode & 0o7777 + ), + ) + .await?; + } + } + } + } + } + + Ok(()) +} + +async fn check_suspicious_processes(config: &SecurityAuditConfig) -> Result<()> { + let system = System::new_all(); + + for (pid, process) in system.processes() { + let process_name = process.name().to_lowercase(); + + for pattern in &config.suspicious_process_patterns { + if process_name.contains(pattern) { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!("Suspicious process found: {} (PID: {})", process_name, pid), + ) + .await?; + + // Additional process information + if let Some(cmd) = process.cmd().first() { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!("Process command: {}", cmd), + ) + .await?; + } + } + } + } + + Ok(()) +} + +async fn check_unauthorized_users(config: &Config) -> Result<()> { + match std::env::consts::OS { + "macos" => check_macos_users(config).await?, + "linux" => check_linux_users(config).await?, + "bellandeos" => check_bellande_users(config).await?, + _ => anyhow::bail!("Unsupported operating system"), + } + + Ok(()) +} + +async fn check_macos_users(config: &Config) -> Result<()> { + let output = Command::new("dscl") + .args(&[".", "list", "/Users"]) + .output() + .context("Failed to list macOS users")?; + + let users = String::from_utf8_lossy(&output.stdout); + for user in users.lines() { + if !config.users.iter().any(|u| u.username == user) && !is_macos_system_user(user) { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!("Unauthorized macOS user found: {}", user), + ) + .await?; + } + } + + Ok(()) +} + +async fn check_linux_users(config: &Config) -> Result<()> { + let passwd = fs::read_to_string("/etc/passwd").context("Failed to read /etc/passwd")?; + + for line in passwd.lines() { + let username = line.split(':').next().unwrap_or(""); + if !config.users.iter().any(|u| u.username == username) && !is_linux_system_user(username) { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!("Unauthorized Linux user found: {}", username), + ) + .await?; + } + } + + Ok(()) +} + +async fn check_bellande_users(config: &Config) -> Result<()> { + let output = Command::new("bellctl") + .args(&["user", "list"]) + .output() + .context("Failed to list BellandeOS users")?; + + let users = String::from_utf8_lossy(&output.stdout); + for user in users.lines() { + if !config.users.iter().any(|u| u.username == user) && !is_bellande_system_user(user) { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!("Unauthorized BellandeOS user found: {}", user), + ) + .await?; + } + } + + Ok(()) +} + +fn is_macos_system_user(username: &str) -> bool { + matches!( + username, + "_spotlight" | "_locationd" | "_mdnsresponder" | "root" | "daemon" + ) +} + +fn is_linux_system_user(username: &str) -> bool { + matches!( + username, + "root" + | "daemon" + | "bin" + | "sys" + | "sync" + | "games" + | "man" + | "lp" + | "mail" + | "news" + | "uucp" + | "proxy" + | "www-data" + | "backup" + | "list" + | "irc" + | "gnats" + | "nobody" + | "systemd-network" + | "systemd-resolve" + | "systemd-timesync" + | "messagebus" + | "syslog" + | "avahi" + | "_apt" + | "sshd" + ) +} + +fn is_bellande_system_user(username: &str) -> bool { + matches!( + username, + "bellroot" | "bellsys" | "bellservice" | "bellnetwork" | "bellsecurity" + ) +} + +async fn check_open_ports(config: &SecurityAuditConfig) -> Result<()> { + match std::env::consts::OS { + "macos" => { + let output = Command::new("lsof") + .args(&["-i", "-P", "-n"]) + .output() + .context("Failed to check macOS open ports")?; + + check_port_output( + &String::from_utf8_lossy(&output.stdout), + &config.allowed_ports, + ) + .await?; + } + "linux" => { + let output = Command::new("netstat") + .args(&["-tuln"]) + .output() + .context("Failed to check Linux open ports")?; + + check_port_output( + &String::from_utf8_lossy(&output.stdout), + &config.allowed_ports, + ) + .await?; + } + "bellandeos" => { + let output = Command::new("bellctl") + .args(&["network", "ports"]) + .output() + .context("Failed to check BellandeOS open ports")?; + + check_port_output( + &String::from_utf8_lossy(&output.stdout), + &config.allowed_ports, + ) + .await?; + } + _ => anyhow::bail!("Unsupported operating system"), + } + + Ok(()) +} + +async fn check_port_output(output: &str, allowed_ports: &HashSet) -> Result<()> { + for line in output.lines() { + if line.contains("LISTEN") { + let port = extract_port_from_line(line); + if let Some(port) = port { + if !allowed_ports.contains(&port) { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!("Unauthorized open port found: {}", port), + ) + .await?; + } + } + } + } + Ok(()) +} + +fn extract_port_from_line(line: &str) -> Option { + line.split(':') + .last()? + .split_whitespace() + .next()? + .parse() + .ok() +} + +async fn check_file_integrity(config: &SecurityAuditConfig) -> Result<()> { + // Initialize or load hash database + let mut hash_database = load_hash_database(&config.file_hash_database)?; + + for path in &config.critical_paths { + check_directory_integrity(path, &mut hash_database).await?; + } + + // Save updated hashes + save_hash_database(&config.file_hash_database, &hash_database)?; + + Ok(()) +} + +async fn check_directory_integrity( + path: &Path, + hash_database: &mut HashMap, +) -> Result<()> { + if path.is_dir() { + for entry in fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() { + let current_hash = calculate_file_hash(&path)?; + + if let Some(stored_hash) = hash_database.get(&path) { + if stored_hash != ¤t_hash { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!("File integrity mismatch: {:?}", path), + ) + .await?; + } + } + // Update hash in database + hash_database.insert(path, current_hash); + } else if path.is_dir() { + Box::pin(check_directory_integrity(&path, hash_database)).await?; + } + } + } + Ok(()) +} + +async fn perform_os_specific_checks() -> Result<()> { + match std::env::consts::OS { + "macos" => perform_macos_specific_checks().await?, + "linux" => perform_linux_specific_checks().await?, + "bellandeos" => perform_bellande_specific_checks().await?, + _ => anyhow::bail!("Unsupported operating system"), + } + Ok(()) +} + +async fn perform_macos_specific_checks() -> Result<()> { + // Check System Integrity Protection (SIP) + let sip_status = Command::new("csrutil") + .arg("status") + .output() + .context("Failed to check SIP status")?; + + if !String::from_utf8_lossy(&sip_status.stdout).contains("enabled") { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + "Warning: System Integrity Protection is disabled", + ) + .await?; + } + + // Check FileVault status + let filevault_status = Command::new("fdesetup") + .arg("status") + .output() + .context("Failed to check FileVault status")?; + + if !String::from_utf8_lossy(&filevault_status.stdout).contains("On") { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + "Warning: FileVault is not enabled", + ) + .await?; + } + + // Check Gatekeeper status + let gatekeeper_status = Command::new("spctl") + .args(&["--status"]) + .output() + .context("Failed to check Gatekeeper status")?; + + if !String::from_utf8_lossy(&gatekeeper_status.stdout).contains("enabled") { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + "Warning: Gatekeeper is disabled", + ) + .await?; + } + + Ok(()) +} + +async fn perform_linux_specific_checks() -> Result<()> { + // Check SELinux status + if Path::new("/etc/selinux/config").exists() { + let selinux_status = Command::new("getenforce") + .output() + .context("Failed to check SELinux status")?; + + if !String::from_utf8_lossy(&selinux_status.stdout).contains("Enforcing") { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + "Warning: SELinux is not in enforcing mode", + ) + .await?; + } + } + + // Check AppArmor status + if Path::new("/etc/apparmor").exists() { + let apparmor_status = Command::new("aa-status") + .output() + .context("Failed to check AppArmor status")?; + + if !apparmor_status.status.success() { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + "Warning: AppArmor is not properly configured", + ) + .await?; + } + } + + // Check kernel parameters + check_kernel_parameters().await?; + + Ok(()) +} + +async fn perform_bellande_specific_checks() -> Result<()> { + // Check BellandeOS security module status + let security_status = Command::new("bellctl") + .args(&["security", "status"]) + .output() + .context("Failed to check BellandeOS security status")?; + + if !String::from_utf8_lossy(&security_status.stdout).contains("enabled") { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + "Warning: BellandeOS security module is not enabled", + ) + .await?; + } + + // Check BellandeOS integrity + let integrity_check = Command::new("bellctl") + .args(&["verify", "system"]) + .output() + .context("Failed to verify BellandeOS integrity")?; + + if !integrity_check.status.success() { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + "Warning: BellandeOS system integrity check failed", + ) + .await?; + } + + // Check BellandeOS update status + let update_status = Command::new("bellctl") + .args(&["update", "status"]) + .output() + .context("Failed to check BellandeOS update status")?; + + if !update_status.status.success() { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + "Warning: BellandeOS update check failed", + ) + .await?; + } + + Ok(()) +} + +async fn check_kernel_parameters() -> Result<()> { + let critical_params = [ + "kernel.randomize_va_space", + "kernel.kptr_restrict", + "kernel.dmesg_restrict", + "kernel.perf_event_paranoid", + "net.ipv4.tcp_syncookies", + ]; + + for param in &critical_params { + let output = Command::new("sysctl") + .arg(param) + .output() + .context(format!("Failed to check kernel parameter: {}", param))?; + + if !output.status.success() { + log_audit_event( + "SECURITY_AUDIT", + "SYSTEM", + &format!("Warning: Failed to verify kernel parameter: {}", param), + ) + .await?; + } + } + + Ok(()) +} + +fn calculate_file_hash(path: &Path) -> Result { + let mut file = File::open(path)?; + let mut hasher = Sha256::new(); + std::io::copy(&mut file, &mut hasher)?; + Ok(format!("{:x}", hasher.finalize())) +} + +fn load_hash_database(path: &Path) -> Result> { + if path.exists() { + let file = File::open(path)?; + let reader = BufReader::new(file); + Ok(serde_json::from_reader(reader)?) + } else { + Ok(HashMap::new()) + } +} + +fn save_hash_database(path: &Path, database: &HashMap) -> Result<()> { + let file = File::create(path)?; + serde_json::to_writer_pretty(file, database)?; + Ok(()) +} diff --git a/src/authentication_compliance/authentication.rs b/src/authentication_compliance/authentication.rs new file mode 100644 index 0000000..5f3e278 --- /dev/null +++ b/src/authentication_compliance/authentication.rs @@ -0,0 +1,123 @@ +// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::audit::audit::log_audit_event; +use crate::config::config::Config; +use crate::user_privilege::user::User; +use anyhow::{Context, Result}; +use argon2; +use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; +use argon2::Argon2; +use rand_core::OsRng; +use std::collections::HashMap; +use std::time::{Duration, Instant, SystemTime}; +use totp_rs::TOTP; + +pub struct Session { + pub user: User, + pub expiry: SystemTime, +} + +pub struct RateLimiter { + attempts: HashMap>, + max_attempts: usize, + window: Duration, +} + +impl RateLimiter { + pub fn new(max_attempts: usize, window: Duration) -> Self { + RateLimiter { + attempts: HashMap::new(), + max_attempts, + window, + } + } + + pub fn check(&mut self, key: &str) -> bool { + let now = Instant::now(); + let attempts = self + .attempts + .entry(key.to_string()) + .or_insert_with(Vec::new); + + attempts.retain(|&t| now.duration_since(t) < self.window); + + if attempts.len() >= self.max_attempts { + false + } else { + attempts.push(now); + true + } + } +} + +pub async fn authenticate_user( + config: &Config, + username: &str, + password: &str, + totp_code: &str, + rate_limiter: &mut RateLimiter, +) -> Result> { + if !rate_limiter.check(username) { + log_audit_event("AUTHENTICATION_RATE_LIMIT", username, "Rate limit exceeded").await?; + return Ok(None); + } + + if let Some(user) = config.users.iter().find(|u| u.username == username) { + if verify_password(&user.password_hash, password)? { + let totp = TOTP::new( + totp_rs::Algorithm::SHA1, + 6, + 1, + 30, + user.totp_secret.as_bytes().to_vec(), + ) + .context("Failed to create TOTP")?; + + if totp.check_current(totp_code)? { + log_audit_event( + "AUTHENTICATION_SUCCESS", + &user.username, + "User authenticated successfully", + ) + .await?; + return Ok(Some(user.clone())); + } + } + } + + log_audit_event("AUTHENTICATION_FAILURE", username, "Authentication failed").await?; + Ok(None) +} + +fn verify_password(hash: &str, password: &str) -> Result { + let parsed_hash = PasswordHash::new(hash).context("Failed to parse password hash")?; + + Ok(Argon2::default() + .verify_password(password.as_bytes(), &parsed_hash) + .is_ok()) +} + +fn hash_password(password: &str) -> Result { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + + let password_hash = argon2 + .hash_password(password.as_bytes(), &salt) + .context("Failed to hash password")? + .to_string(); + + Ok(password_hash) +} diff --git a/src/authentication_compliance/complication.rs b/src/authentication_compliance/complication.rs new file mode 100644 index 0000000..d9e2136 --- /dev/null +++ b/src/authentication_compliance/complication.rs @@ -0,0 +1,1503 @@ +// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use chrono::Utc; +use regex::Regex; +use std::collections::HashMap; +use std::fs::{self, File}; +use std::io::{BufRead, BufReader}; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{Context, Result}; +use log::{info, warn}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use std::os::unix::fs::MetadataExt; + +use crate::audit::audit::log_audit_event; +use crate::config::config::Config; + +#[derive(Debug)] +pub struct NetworkRequirements { + pub required_protocols: Vec, + pub minimum_networks: usize, + pub required_firewall: bool, + pub required_encryption: bool, +} + +#[derive(Debug)] +pub struct ComplianceConfig { + // Original fields + pub min_password_length: usize, + pub min_password_entropy: f64, + pub password_complexity_regex: String, + pub critical_files: Vec, + pub required_services: Vec, + pub required_kernel_params: Vec, + pub audit_file_hashes: PathBuf, + pub network_requirements: NetworkRequirements, + + // New password policy fields + pub password_max_days: u32, + pub password_min_days: u32, + pub password_warn_days: u32, + pub max_repeated_chars: usize, +} + +#[derive(Debug)] +struct PasswordViolation { + description: String, + severity: ViolationSeverity, +} + +#[derive(Debug)] +enum ViolationSeverity { + Low, + Medium, + High, + Critical, +} + +impl Default for ComplianceConfig { + fn default() -> Self { + let security_paths = get_security_paths(); + let critical_files = security_paths.get("critical").cloned().unwrap_or_default(); + let services = get_security_services(); + let required_services = services.get("required").cloned().unwrap_or_default(); + + ComplianceConfig { + min_password_length: 12, + min_password_entropy: 50.0, + max_repeated_chars: 3, + password_max_days: 90, + password_min_days: 1, + password_warn_days: 7, + password_complexity_regex: String::from( + r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,}$", + ), + critical_files, + required_services, + // Fix the type mismatch by converting HashMap to Vec + required_kernel_params: get_required_kernel_params() + .into_iter() + .map(|(key, value)| format!("{}={}", key, value)) + .collect(), + audit_file_hashes: PathBuf::from("audit_hashes.db"), + network_requirements: NetworkRequirements { + required_protocols: vec![ + "TLSv1.3".to_string(), + "SSHv2".to_string(), + "TLS_AES_256_GCM_SHA384".to_string(), + "TLS_CHACHA20_POLY1305_SHA256".to_string(), + ], + minimum_networks: 1, + required_firewall: true, + required_encryption: true, + }, + } + } +} + +fn get_security_paths() -> HashMap> { + let mut paths = HashMap::new(); + match std::env::consts::OS { + "linux" => { + paths.insert( + "critical".to_string(), + vec![ + PathBuf::from("/etc/security/limits.conf"), + PathBuf::from("/etc/security/pwquality.conf"), + PathBuf::from("/etc/passwd"), + PathBuf::from("/etc/shadow"), + PathBuf::from("/etc/group"), + PathBuf::from("/etc/sudoers"), + PathBuf::from("/etc/ssh/sshd_config"), + ], + ); + } + "bellandeos" => { + paths.insert( + "critical".to_string(), + vec![ + PathBuf::from("/bell/security/audit.conf"), + PathBuf::from("/bell/security/password.conf"), + PathBuf::from("/bell/security/users"), + PathBuf::from("/bell/security/access"), + PathBuf::from("/bell/security/keys"), + PathBuf::from("/bell/config/system"), + ], + ); + } + "macos" => { + paths.insert( + "critical".to_string(), + vec![ + PathBuf::from("/etc/security/audit_control"), + PathBuf::from("/etc/security/pwpolicy"), + PathBuf::from("/etc/pam.d"), + PathBuf::from("/Library/Security"), + PathBuf::from("/etc/ssh/sshd_config"), + ], + ); + } + _ => {} + } + paths +} + +fn get_required_services() -> Vec { + match std::env::consts::OS { + "macos" => vec![ + "com.apple.auditd".to_string(), + "com.apple.security".to_string(), + ], + "linux" => vec!["auditd".to_string(), "sshd".to_string(), "ufw".to_string()], + "bellandeos" => vec![ + "bell.audit".to_string(), + "bell.security".to_string(), + "bell.firewall".to_string(), + ], + _ => vec![], + } +} + +fn get_required_kernel_params() -> HashMap { + let mut params = HashMap::new(); + match std::env::consts::OS { + "linux" => { + // Memory protection + params.insert("kernel.randomize_va_space".to_string(), "2".to_string()); + params.insert("kernel.kptr_restrict".to_string(), "1".to_string()); + params.insert("kernel.yama.ptrace_scope".to_string(), "1".to_string()); + params.insert("vm.mmap_min_addr".to_string(), "65536".to_string()); + + // Network security + params.insert("net.ipv4.tcp_syncookies".to_string(), "1".to_string()); + params.insert("net.ipv4.conf.all.rp_filter".to_string(), "1".to_string()); + params.insert( + "net.ipv4.conf.default.rp_filter".to_string(), + "1".to_string(), + ); + params.insert( + "net.ipv4.conf.all.accept_redirects".to_string(), + "0".to_string(), + ); + params.insert( + "net.ipv6.conf.all.accept_redirects".to_string(), + "0".to_string(), + ); + params.insert( + "net.ipv4.conf.all.send_redirects".to_string(), + "0".to_string(), + ); + params.insert( + "net.ipv4.conf.all.accept_source_route".to_string(), + "0".to_string(), + ); + params.insert( + "net.ipv6.conf.all.accept_source_route".to_string(), + "0".to_string(), + ); + + // Core dumps + params.insert("kernel.core_pattern".to_string(), "|/bin/false".to_string()); + params.insert("fs.suid_dumpable".to_string(), "0".to_string()); + + // System security + params.insert("kernel.sysrq".to_string(), "0".to_string()); + params.insert("kernel.dmesg_restrict".to_string(), "1".to_string()); + params.insert( + "kernel.unprivileged_bpf_disabled".to_string(), + "1".to_string(), + ); + + // Module loading + params.insert("kernel.modules_disabled".to_string(), "1".to_string()); + + // IPv6 security + params.insert( + "net.ipv6.conf.all.disable_ipv6".to_string(), + "1".to_string(), + ); + params.insert( + "net.ipv6.conf.default.disable_ipv6".to_string(), + "1".to_string(), + ); + } + "bellandeos" => { + // General security + params.insert("bell.security.level".to_string(), "high".to_string()); + params.insert("bell.memory.protection".to_string(), "strict".to_string()); + params.insert("bell.process.isolation".to_string(), "enforced".to_string()); + + // System hardening + params.insert("bell.kernel.hardening".to_string(), "maximum".to_string()); + params.insert("bell.syscall.filtering".to_string(), "strict".to_string()); + params.insert( + "bell.exploit.prevention".to_string(), + "aggressive".to_string(), + ); + + // Memory security + params.insert("bell.memory.aslr".to_string(), "full".to_string()); + params.insert("bell.stack.protection".to_string(), "strong".to_string()); + params.insert("bell.heap.protection".to_string(), "strict".to_string()); + + // Network security + params.insert("bell.network.filtering".to_string(), "strict".to_string()); + params.insert("bell.network.isolation".to_string(), "enforced".to_string()); + params.insert( + "bell.network.encryption".to_string(), + "required".to_string(), + ); + + // Access control + params.insert("bell.access.control".to_string(), "mandatory".to_string()); + params.insert( + "bell.privilege.escalation".to_string(), + "restricted".to_string(), + ); + params.insert("bell.capability.control".to_string(), "strict".to_string()); + + // Monitoring and auditing + params.insert("bell.audit.level".to_string(), "comprehensive".to_string()); + params.insert("bell.monitoring.mode".to_string(), "active".to_string()); + params.insert("bell.incident.detection".to_string(), "enabled".to_string()); + } + "macos" => { + // While macOS doesn't use sysctl for all security settings, + params.insert("kern.sugid_coredump".to_string(), "0".to_string()); + params.insert( + "kern.bootargs".to_string(), + "cs_enforcement_disable=0".to_string(), + ); + params.insert("kern.secure_kernel".to_string(), "1".to_string()); + params.insert("net.inet.tcp.blackhole".to_string(), "2".to_string()); + params.insert("net.inet.udp.blackhole".to_string(), "1".to_string()); + params.insert("net.inet.icmp.icmplim".to_string(), "50".to_string()); + params.insert("net.inet.ip.forwarding".to_string(), "0".to_string()); + params.insert("net.inet.ip.redirect".to_string(), "0".to_string()); + params.insert("net.inet.tcp.always_keepalive".to_string(), "0".to_string()); + params.insert("net.inet.tcp.drop_synfin".to_string(), "1".to_string()); + } + _ => {} + } + params +} + +fn get_security_services() -> HashMap> { + let mut services = HashMap::new(); + match std::env::consts::OS { + "linux" => { + services.insert( + "required".to_string(), + vec![ + "auditd".to_string(), + "fail2ban".to_string(), + "ufw".to_string(), + "apparmor".to_string(), + "systemd-journald".to_string(), + ], + ); + services.insert( + "prohibited".to_string(), + vec![ + "telnet".to_string(), + "rsh".to_string(), + "rlogin".to_string(), + "rexec".to_string(), + ], + ); + } + "bellandeos" => { + services.insert( + "required".to_string(), + vec![ + "bell.audit".to_string(), + "bell.security".to_string(), + "bell.firewall".to_string(), + "bell.intrusion_detection".to_string(), + "bell.integrity_monitor".to_string(), + "bell.endpoint_protection".to_string(), + ], + ); + services.insert( + "prohibited".to_string(), + vec![ + "bell.legacy_protocols".to_string(), + "bell.unsecured_services".to_string(), + ], + ); + } + "macos" => { + services.insert( + "required".to_string(), + vec![ + "com.apple.auditd".to_string(), + "com.apple.security.firewall".to_string(), + "com.apple.security.fdesetup".to_string(), + "com.apple.security.SecureIO".to_string(), + ], + ); + services.insert( + "prohibited".to_string(), + vec!["com.apple.tftp".to_string(), "com.apple.ftp".to_string()], + ); + } + _ => {} + } + services +} + +pub async fn check_compliance(config: &Config) -> Result<()> { + let compliance_config = ComplianceConfig::default(); + info!("Starting compliance check for {}", std::env::consts::OS); + + // Password compliance + check_password_complexity(config, &compliance_config).await?; + + // File permissions + check_file_permissions(&compliance_config).await?; + + // System configurations + check_system_configurations(&compliance_config).await?; + + // Audit log integrity + check_audit_log_integrity(&compliance_config).await?; + + // Network configurations + check_network_configurations(config, &compliance_config).await?; + + // OS-specific checks + perform_os_specific_checks(&compliance_config).await?; + + log_audit_event( + "COMPLIANCE_CHECK", + "SYSTEM", + &format!("Completed compliance check on {}", std::env::consts::OS), + ) + .await?; + + Ok(()) +} + +async fn check_password_complexity( + config: &Config, + compliance_config: &ComplianceConfig, +) -> Result<()> { + let regex = Regex::new(&compliance_config.password_complexity_regex) + .context("Failed to compile password complexity regex")?; + + for user in &config.users { + let mut violations: Vec = Vec::new(); + + // Check hash length (Argon2) + if user.password_hash.len() < 60 { + violations.push(PasswordViolation { + description: "Password hash does not meet length requirements".to_string(), + severity: ViolationSeverity::High, + }); + } + + // Check password expiry + let days_since_change = (Utc::now() - user.password_changed_at).num_days(); + if days_since_change > compliance_config.password_max_days as i64 { + violations.push(PasswordViolation { + description: format!( + "Password expired {} days ago (max: {} days)", + days_since_change, compliance_config.password_max_days + ), + severity: ViolationSeverity::Medium, + }); + } + + // Check if password will expire soon + let days_until_expiry = compliance_config.password_max_days as i64 - days_since_change; + if days_until_expiry <= compliance_config.password_warn_days as i64 && days_until_expiry > 0 + { + violations.push(PasswordViolation { + description: format!("Password will expire in {} days", days_until_expiry), + severity: ViolationSeverity::Low, + }); + } + + // Check additional password requirements + if let Some(raw_password) = get_last_password_change(&user.username).await? { + if !regex.is_match(&raw_password) { + violations.push(PasswordViolation { + description: "Password does not meet complexity requirements".to_string(), + severity: ViolationSeverity::Critical, + }); + } + + // Check minimum length + if raw_password.len() < compliance_config.min_password_length { + violations.push(PasswordViolation { + description: format!( + "Password length ({}) below minimum required ({})", + raw_password.len(), + compliance_config.min_password_length + ), + severity: ViolationSeverity::High, + }); + } + + // Add detailed password validation + let strength_violations = validate_password_strength(&raw_password, compliance_config); + violations.extend(strength_violations); + + // Check for common passwords + if is_common_password(&raw_password).await? { + violations.push(PasswordViolation { + description: "Password found in common password list".to_string(), + severity: ViolationSeverity::Critical, + }); + } + + // Check password entropy + let entropy = calculate_password_entropy(&raw_password); + if entropy < compliance_config.min_password_entropy { + violations.push(PasswordViolation { + description: format!( + "Password entropy too low: {:.2} bits (minimum: {} bits)", + entropy, compliance_config.min_password_entropy + ), + severity: ViolationSeverity::High, + }); + } + } + + // Log all violations + for violation in violations { + log_audit_event( + "COMPLIANCE_VIOLATION", + &user.username, + &format!( + "{} (Severity: {:?})", + violation.description, violation.severity + ), + ) + .await?; + } + } + + Ok(()) +} +async fn is_common_password(password: &str) -> Result { + // This could be implemented by checking against a BellandeSQL of common passwords + // For now, we'll just check some basic patterns + let common_patterns = [ + "password", "123456", "qwerty", "admin", "letmein", "welcome", + ]; + + Ok(common_patterns + .iter() + .any(|&pattern| password.contains(pattern))) +} + +fn validate_password_strength(password: &str, config: &ComplianceConfig) -> Vec { + let mut violations: Vec = Vec::new(); + + // Check length + if password.len() < config.min_password_length { + violations.push(PasswordViolation { + description: format!( + "Password too short: {} chars (minimum {})", + password.len(), + config.min_password_length + ), + severity: ViolationSeverity::High, + }); + } + + // Check for uppercase letters + if !password.chars().any(|c| c.is_uppercase()) { + violations.push(PasswordViolation { + description: "Password must contain at least one uppercase letter".to_string(), + severity: ViolationSeverity::Medium, + }); + } + + // Check for lowercase letters + if !password.chars().any(|c| c.is_lowercase()) { + violations.push(PasswordViolation { + description: "Password must contain at least one lowercase letter".to_string(), + severity: ViolationSeverity::Medium, + }); + } + + // Check for numbers + if !password.chars().any(|c| c.is_numeric()) { + violations.push(PasswordViolation { + description: "Password must contain at least one number".to_string(), + severity: ViolationSeverity::Medium, + }); + } + + // Check for special characters + if !password.chars().any(|c| !c.is_alphanumeric()) { + violations.push(PasswordViolation { + description: "Password must contain at least one special character".to_string(), + severity: ViolationSeverity::Medium, + }); + } + + // Check for repeating characters + if has_repeating_chars(password, 3) { + violations.push(PasswordViolation { + description: "Password contains repeating characters".to_string(), + severity: ViolationSeverity::Low, + }); + } + + violations +} + +// Helper function to check for repeating characters +fn has_repeating_chars(password: &str, max_repeats: usize) -> bool { + let chars: Vec = password.chars().collect(); + let mut repeat_count = 1; + + for i in 1..chars.len() { + if chars[i] == chars[i - 1] { + repeat_count += 1; + if repeat_count > max_repeats { + return true; + } + } else { + repeat_count = 1; + } + } + + false +} + +// Entropy calculation for password strength +fn calculate_password_entropy(password: &str) -> f64 { + let mut charset_size = 0; + + // Check what types of characters are used + let has_lowercase = password.chars().any(|c| c.is_ascii_lowercase()); + let has_uppercase = password.chars().any(|c| c.is_ascii_uppercase()); + let has_numbers = password.chars().any(|c| c.is_ascii_digit()); + let has_symbols = password.chars().any(|c| !c.is_alphanumeric()); + + // Calculate charset size + if has_lowercase { + charset_size += 26; + } + if has_uppercase { + charset_size += 26; + } + if has_numbers { + charset_size += 10; + } + if has_symbols { + charset_size += 32; + } + + // Calculate entropy + (password.len() as f64) * (charset_size as f64).log2() +} + +#[cfg(target_family = "unix")] +async fn check_file_permissions(compliance_config: &ComplianceConfig) -> Result<()> { + for file_path in &compliance_config.critical_files { + let metadata = fs::metadata(file_path) + .context(format!("Failed to get metadata for {:?}", file_path))?; + + // Use direct metadata mode() for Unix systems + let mode = metadata.mode() & 0o777; + + // Check for secure permissions + if mode != 0o600 { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!("Incorrect permissions on {:?}: {:o}", file_path, mode), + ) + .await?; + } + + // Check ownership + let uid = metadata.uid(); + if uid != 0 { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!("Incorrect ownership on {:?}", file_path), + ) + .await?; + } + + // Check group ownership + let gid = metadata.gid(); + if gid != 0 { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!("Incorrect group ownership on {:?}: GID {}", file_path, gid), + ) + .await?; + } + } + Ok(()) +} + +#[cfg(target_family = "unix")] +async fn check_single_file_permissions(file_path: &Path) -> Result<()> { + let metadata = + fs::metadata(file_path).context(format!("Failed to get metadata for {:?}", file_path))?; + + let mode = metadata.mode() & 0o777; + let uid = metadata.uid(); + let gid = metadata.gid(); + + let mut violations = Vec::new(); + + // Check basic permissions + if mode != 0o600 { + violations.push(format!("incorrect permissions: {:o}", mode)); + } + + // Check ownership + if uid != 0 { + violations.push(format!("incorrect owner UID: {}", uid)); + } + + // Check group + if gid != 0 { + violations.push(format!("incorrect group GID: {}", gid)); + } + + // Special bits check + if mode & 0o7000 != 0 { + violations.push(format!("special bits set: {:o}", mode & 0o7000)); + } + + // Log all violations if any found + if !violations.is_empty() { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!( + "Security violations for {:?}: {}", + file_path, + violations.join(", ") + ), + ) + .await?; + } + + Ok(()) +} + +async fn check_system_configurations(compliance_config: &ComplianceConfig) -> Result<()> { + match std::env::consts::OS { + "macos" => check_macos_configurations().await?, + "linux" => check_linux_configurations(compliance_config).await?, + "bellandeos" => check_bellande_configurations().await?, + _ => warn!("System configuration checking not implemented for this OS"), + } + + // Check required services + for service in &compliance_config.required_services { + if !is_service_running(service).await? { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!("Required service not running: {}", service), + ) + .await?; + } + } + + // Check kernel parameters + for param in &compliance_config.required_kernel_params { + let expected_value = get_expected_value(param)?; + check_kernel_parameter(param, &expected_value).await?; + } + + Ok(()) +} + +async fn check_kernel_parameter(param: &str, expected_value: &str) -> Result<()> { + match std::env::consts::OS { + "linux" => { + let output = Command::new("sysctl") + .arg(param) + .output() + .context(format!("Failed to check kernel parameter: {}", param))?; + let value = String::from_utf8_lossy(&output.stdout); + if !value.contains(expected_value) { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!("Kernel parameter {} has incorrect value", param), + ) + .await?; + } + } + "bellandeos" => { + let output = Command::new("bellctl") + .args(&["kernel", "param", param]) + .output() + .context(format!("Failed to check BellandeOS parameter: {}", param))?; + let value = String::from_utf8_lossy(&output.stdout); + if !value.contains(expected_value) { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!("BellandeOS parameter {} has incorrect value", param), + ) + .await?; + } + } + _ => warn!("Kernel parameter checking not implemented for this OS"), + } + Ok(()) +} + +fn get_expected_value(param: &str) -> Result { + match param { + "kernel.randomize_va_space" => Ok("2".to_string()), + "net.ipv4.ip_forward" => Ok("0".to_string()), + "kernel.yama.ptrace_scope" => Ok("1".to_string()), + "kernel.kptr_restrict" => Ok("2".to_string()), + "net.ipv4.conf.all.accept_redirects" => Ok("0".to_string()), + "net.ipv4.conf.all.send_redirects" => Ok("0".to_string()), + _ => Ok("0".to_string()), + } +} + +async fn get_kernel_parameter(param: &str) -> Result { + match std::env::consts::OS { + "linux" => { + let path = format!("/proc/sys/{}", param.replace(".", "/")); + Ok(fs::read_to_string(path)?.trim().to_string()) + } + _ => Ok("0".to_string()), + } +} + +async fn check_audit_log_integrity(compliance_config: &ComplianceConfig) -> Result<()> { + // Load stored hashes + let stored_hashes = load_file_hashes(&compliance_config.audit_file_hashes)?; + + // Check audit log file + let audit_log_path = Path::new("audit_log.txt"); + if !audit_log_path.exists() { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "Audit log file is missing", + ) + .await?; + return Ok(()); + } + + // Calculate current hash + let current_hash = calculate_file_hash(audit_log_path)?; + + // Compare with stored hash + if let Some(stored_hash) = stored_hashes.get(audit_log_path) { + if current_hash != *stored_hash { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "Audit log integrity check failed", + ) + .await?; + } + } else { + // Store initial hash + save_file_hash( + &compliance_config.audit_file_hashes, + audit_log_path, + ¤t_hash, + )?; + } + + Ok(()) +} + +async fn check_network_configurations( + config: &Config, + compliance_config: &ComplianceConfig, +) -> Result<()> { + // Check network restrictions + if config.allowed_networks.len() < compliance_config.network_requirements.minimum_networks { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "Insufficient network restrictions configured", + ) + .await?; + } + + // Check firewall + if compliance_config.network_requirements.required_firewall { + check_firewall_status().await?; + } + + // Check encryption requirements + if compliance_config.network_requirements.required_encryption { + check_network_encryption(&compliance_config.network_requirements).await?; + } + + Ok(()) +} + +async fn perform_os_specific_checks(compliance_config: &ComplianceConfig) -> Result<()> { + match std::env::consts::OS { + "macos" => { + // Check SIP status + let sip_output = Command::new("csrutil") + .arg("status") + .output() + .context("Failed to check SIP status")?; + + if !String::from_utf8_lossy(&sip_output.stdout).contains("enabled") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "System Integrity Protection is disabled", + ) + .await?; + } + + // Check FileVault + let filevault_output = Command::new("fdesetup") + .arg("status") + .output() + .context("Failed to check FileVault status")?; + + if !String::from_utf8_lossy(&filevault_output.stdout).contains("On") { + log_audit_event("COMPLIANCE_VIOLATION", "SYSTEM", "FileVault is not enabled") + .await?; + } + } + "linux" => { + // Check SELinux + if !Path::new("/sys/fs/selinux/enforce").exists() { + log_audit_event("COMPLIANCE_VIOLATION", "SYSTEM", "SELinux is not enabled").await?; + } + + // Check AppArmor + let apparmor_output = Command::new("aa-status") + .output() + .context("Failed to check AppArmor status")?; + + if !apparmor_output.status.success() { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "AppArmor is not properly configured", + ) + .await?; + } + } + "bellandeos" => { + // Check BellandeOS security module + let security_output = Command::new("bellctl") + .args(&["security", "status"]) + .output() + .context("Failed to check BellandeOS security status")?; + + if !String::from_utf8_lossy(&security_output.stdout).contains("enabled") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "BellandeOS security module is not enabled", + ) + .await?; + } + + // Check BellandeOS integrity + let integrity_output = Command::new("bellctl") + .args(&["verify", "system"]) + .output() + .context("Failed to verify BellandeOS integrity")?; + + if !integrity_output.status.success() { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "BellandeOS system integrity check failed", + ) + .await?; + } + } + _ => warn!("OS-specific checks not implemented for this operating system"), + } + + Ok(()) +} + +async fn check_macos_configurations() -> Result<()> { + // Check System Integrity Protection (SIP) + let sip_output = Command::new("csrutil") + .arg("status") + .output() + .context("Failed to check SIP status")?; + + if !String::from_utf8_lossy(&sip_output.stdout).contains("enabled") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "System Integrity Protection (SIP) is disabled", + ) + .await?; + } + + // Check FileVault encryption + let filevault_output = Command::new("fdesetup") + .arg("status") + .output() + .context("Failed to check FileVault status")?; + + if !String::from_utf8_lossy(&filevault_output.stdout).contains("FileVault is On") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "FileVault encryption is not enabled", + ) + .await?; + } + + // Check Gatekeeper status + let gatekeeper_output = Command::new("spctl") + .args(&["--status"]) + .output() + .context("Failed to check Gatekeeper status")?; + + if !String::from_utf8_lossy(&gatekeeper_output.stdout).contains("assessments enabled") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "Gatekeeper is not enabled", + ) + .await?; + } + + // Check software update settings + let update_output = Command::new("softwareupdate") + .args(&["--schedule"]) + .output() + .context("Failed to check software update schedule")?; + + if !String::from_utf8_lossy(&update_output.stdout).contains("enabled") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "Automatic software updates are not enabled", + ) + .await?; + } + + // Check firewall status + let firewall_output = Command::new("defaults") + .args(&["read", "/Library/Preferences/com.apple.alf", "globalstate"]) + .output() + .context("Failed to check firewall status")?; + + if !String::from_utf8_lossy(&firewall_output.stdout) + .trim() + .eq("1") + { + log_audit_event("COMPLIANCE_VIOLATION", "SYSTEM", "Firewall is not enabled").await?; + } + + Ok(()) +} + +async fn check_linux_configurations(compliance_config: &ComplianceConfig) -> Result<()> { + // Check SELinux status + if Path::new("/etc/selinux/config").exists() { + let selinux_output = Command::new("getenforce") + .output() + .context("Failed to check SELinux status")?; + + if !String::from_utf8_lossy(&selinux_output.stdout).contains("Enforcing") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "SELinux is not in enforcing mode", + ) + .await?; + } + } + + // Check firewall status (UFW) + let ufw_output = Command::new("ufw") + .arg("status") + .output() + .context("Failed to check UFW status")?; + + if !String::from_utf8_lossy(&ufw_output.stdout).contains("active") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "UFW firewall is not active", + ) + .await?; + } + + // Check system security settings + check_sysctl_settings().await?; + + // Check important security files + let security_files = ["/etc/shadow", "/etc/passwd", "/etc/group", "/etc/sudoers"]; + + for file in &security_files { + let metadata = + fs::metadata(file).context(format!("Failed to check permissions for {}", file))?; + + let mode = metadata.permissions().mode(); + if mode & 0o777 != 0o600 { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!("Incorrect permissions on {}: {:o}", file, mode & 0o777), + ) + .await?; + } + } + + // Check for password policies + check_password_policies().await?; + + // Check for core dumps + check_core_dumps().await?; + + Ok(()) +} + +async fn check_bellande_configurations() -> Result<()> { + // Check BellandeOS security module status + let security_status = Command::new("bellctl") + .args(&["security", "status"]) + .output() + .context("Failed to check BellandeOS security status")?; + + if !String::from_utf8_lossy(&security_status.stdout).contains("enabled") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "BellandeOS security module is not enabled", + ) + .await?; + } + + // Check BellandeOS integrity + let integrity_check = Command::new("bellctl") + .args(&["verify", "system"]) + .output() + .context("Failed to verify BellandeOS integrity")?; + + if !integrity_check.status.success() { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "BellandeOS system integrity check failed", + ) + .await?; + } + + // Check BellandeOS encryption status + let encryption_status = Command::new("bellctl") + .args(&["encryption", "status"]) + .output() + .context("Failed to check encryption status")?; + + if !String::from_utf8_lossy(&encryption_status.stdout).contains("enabled") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "BellandeOS encryption is not enabled", + ) + .await?; + } + + // Check BellandeOS firewall configuration + let firewall_status = Command::new("bellctl") + .args(&["firewall", "status"]) + .output() + .context("Failed to check firewall status")?; + + if !firewall_status.status.success() { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "BellandeOS firewall is not properly configured", + ) + .await?; + } + + // Check security policies + check_bellande_security_policies().await?; + + Ok(()) +} + +// Helper functions + +async fn check_sysctl_settings() -> Result<()> { + let sysctl_checks = [ + ("kernel.randomize_va_space", "2"), + ("kernel.kptr_restrict", "2"), + ("kernel.dmesg_restrict", "1"), + ("kernel.yama.ptrace_scope", "1"), + ("net.ipv4.conf.all.rp_filter", "1"), + ("net.ipv4.conf.default.rp_filter", "1"), + ]; + + for (setting, expected_value) in &sysctl_checks { + let output = Command::new("sysctl") + .arg("-n") + .arg(setting) + .output() + .context(format!("Failed to check sysctl setting: {}", setting))?; + + let value = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if value != *expected_value { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!( + "Incorrect sysctl setting {}: expected {}, got {}", + setting, expected_value, value + ), + ) + .await?; + } + } + + Ok(()) +} + +async fn check_password_policies() -> Result<()> { + let login_defs_path = "/etc/login.defs"; + if Path::new(login_defs_path).exists() { + let content = fs::read_to_string(login_defs_path).context("Failed to read login.defs")?; + + let checks = [ + ("PASS_MAX_DAYS", "90"), + ("PASS_MIN_DAYS", "1"), + ("PASS_MIN_LEN", "12"), + ("PASS_WARN_AGE", "7"), + ]; + + for (setting, expected) in &checks { + if !content.lines().any(|line| { + line.starts_with(setting) && line.split_whitespace().nth(1) == Some(expected) + }) { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!("Incorrect password policy setting: {}", setting), + ) + .await?; + } + } + } + + Ok(()) +} + +async fn check_core_dumps() -> Result<()> { + let limits_conf = "/etc/security/limits.conf"; + if Path::new(limits_conf).exists() { + let content = fs::read_to_string(limits_conf).context("Failed to read limits.conf")?; + + if !content.lines().any(|line| line.contains("* hard core 0")) { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "Core dumps are not disabled", + ) + .await?; + } + } + + Ok(()) +} + +async fn check_bellande_security_policies() -> Result<()> { + let policies = [ + ("password-complexity", "high"), + ("session-timeout", "enabled"), + ("audit-level", "full"), + ("network-isolation", "enforced"), + ]; + + for (policy, expected_value) in &policies { + let output = Command::new("bellctl") + .args(&["policy", "get", policy]) + .output() + .context(format!("Failed to check policy: {}", policy))?; + + let value = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if value != *expected_value { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!( + "Incorrect security policy {}: expected {}, got {}", + policy, expected_value, value + ), + ) + .await?; + } + } + + Ok(()) +} +async fn is_service_running(service: &str) -> Result { + match std::env::consts::OS { + "macos" => { + let output = Command::new("launchctl") + .args(&["list", service]) + .output()?; + Ok(output.status.success()) + } + "linux" => { + let output = Command::new("systemctl") + .args(&["is-active", service]) + .output()?; + Ok(output.status.success()) + } + "bellandeos" => { + let output = Command::new("bellctl") + .args(&["service", "status", service]) + .output()?; + Ok(output.status.success()) + } + _ => Ok(false), + } +} + +async fn check_firewall_status() -> Result<()> { + match std::env::consts::OS { + "macos" => { + let output = Command::new("defaults") + .args(&["read", "/Library/Preferences/com.apple.alf", "globalstate"]) + .output() + .context("Failed to check macOS firewall status")?; + + if !String::from_utf8_lossy(&output.stdout).contains("1") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "macOS firewall is not enabled", + ) + .await?; + } + } + "linux" => { + let output = Command::new("ufw") + .arg("status") + .output() + .context("Failed to check UFW status")?; + + if !String::from_utf8_lossy(&output.stdout).contains("Status: active") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "UFW firewall is not active", + ) + .await?; + } + } + "bellandeos" => { + let output = Command::new("bellctl") + .args(&["firewall", "status"]) + .output() + .context("Failed to check BellandeOS firewall status")?; + + if !String::from_utf8_lossy(&output.stdout).contains("enabled") { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "BellandeOS firewall is not enabled", + ) + .await?; + } + } + _ => warn!("Firewall checking not implemented for this OS"), + } + Ok(()) +} + +async fn check_network_encryption(network_requirements: &NetworkRequirements) -> Result<()> { + // Check SSL/TLS versions + check_ssl_versions(&network_requirements.required_protocols).await?; + + // Check SSH configuration + check_ssh_configuration().await?; + + // Check encrypted protocols + check_encrypted_protocols().await?; + + Ok(()) +} + +async fn check_ssl_versions(required_protocols: &[String]) -> Result<()> { + match std::env::consts::OS { + "macos" | "linux" | "bellandeos" => { + let output = Command::new("openssl") + .args(&["version"]) + .output() + .context("Failed to check OpenSSL version")?; + + let version = String::from_utf8_lossy(&output.stdout); + + for protocol in required_protocols { + if !version.contains(protocol) { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!("Required protocol {} not available", protocol), + ) + .await?; + } + } + } + _ => warn!("SSL version checking not implemented for this OS"), + } + Ok(()) +} + +async fn check_ssh_configuration() -> Result<()> { + let ssh_config_path = match std::env::consts::OS { + "macos" | "linux" => Path::new("/etc/ssh/sshd_config"), + "bellandeos" => Path::new("/bell/security/ssh/sshd_config"), + _ => return Ok(()), + }; + + if !ssh_config_path.exists() { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + "SSH configuration file not found", + ) + .await?; + return Ok(()); + } + + let file = File::open(ssh_config_path)?; + let reader = BufReader::new(file); + + let required_settings = [ + ("PermitRootLogin", "no"), + ("PasswordAuthentication", "no"), + ("X11Forwarding", "no"), + ("Protocol", "2"), + ]; + + for line in reader.lines() { + let line = line?; + for (setting, expected_value) in &required_settings { + if line.starts_with(setting) && !line.contains(expected_value) { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!("SSH setting {} has incorrect value", setting), + ) + .await?; + } + } + } + + Ok(()) +} + +async fn check_encrypted_protocols() -> Result<()> { + // Check for unencrypted protocols + let unsafe_protocols = ["telnet", "ftp", "http"]; + + let output = Command::new("netstat") + .args(&["-tulpn"]) + .output() + .context("Failed to check network protocols")?; + + let output_str = String::from_utf8_lossy(&output.stdout); + + for protocol in unsafe_protocols { + if output_str.contains(protocol) { + log_audit_event( + "COMPLIANCE_VIOLATION", + "SYSTEM", + &format!("Unsafe protocol in use: {}", protocol), + ) + .await?; + } + } + + Ok(()) +} + +async fn get_last_password_change(username: &str) -> Result> { + match std::env::consts::OS { + "macos" => { + let output = Command::new("dscl") + .args(&[ + ".", + "-read", + &format!("/Users/{}", username), + "passwordLastSetTime", + ]) + .output()?; + Ok(Some(String::from_utf8_lossy(&output.stdout).to_string())) + } + "linux" => { + let output = Command::new("chage").args(&["-l", username]).output()?; + Ok(Some(String::from_utf8_lossy(&output.stdout).to_string())) + } + "bellandeos" => { + let output = Command::new("bellctl") + .args(&["user", "password-info", username]) + .output()?; + Ok(Some(String::from_utf8_lossy(&output.stdout).to_string())) + } + _ => Ok(None), + } +} + +fn calculate_file_hash(path: &Path) -> Result { + let mut file = File::open(path)?; + let mut hasher = Sha256::new(); + std::io::copy(&mut file, &mut hasher)?; + Ok(format!("{:x}", hasher.finalize())) +} + +fn load_file_hashes(path: &Path) -> Result> { + if path.exists() { + let file = File::open(path)?; + Ok(serde_json::from_reader(file)?) + } else { + Ok(HashMap::new()) + } +} + +fn save_file_hash(path: &Path, file_path: &Path, hash: &str) -> Result<()> { + let mut hashes = load_file_hashes(path)?; + hashes.insert(file_path.to_path_buf(), hash.to_string()); + let file = File::create(path)?; + serde_json::to_writer_pretty(file, &hashes)?; + Ok(()) +} diff --git a/src/authentication_compliance/mod.rs b/src/authentication_compliance/mod.rs new file mode 100644 index 0000000..4721b79 --- /dev/null +++ b/src/authentication_compliance/mod.rs @@ -0,0 +1,2 @@ +pub mod authentication; +pub mod complication; diff --git a/src/bell.rs b/src/bell.rs new file mode 100644 index 0000000..f96694a --- /dev/null +++ b/src/bell.rs @@ -0,0 +1,213 @@ +// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +mod audit; +mod authentication_compliance; +mod command; +mod config; +mod hsm; +mod network; +mod user_privilege; + +use std::time::{Duration, SystemTime}; +use structopt::StructOpt; +use tokio; + +use crate::authentication_compliance::authentication::{authenticate_user, RateLimiter, Session}; +use crate::command::command::run_command_with_privilege; +use crate::config::config::Config; +use crate::user_privilege::privilege::{PrivilegeConfig, PrivilegeLevel, PrivilegeManager}; +use crate::user_privilege::user::{ + add_user, add_user_to_group, change_password, change_privilege, remove_user, + remove_user_from_group, +}; + +#[derive(StructOpt, Debug)] +#[structopt(name = "bell", about = "Privilege escalation system")] +enum Opt { + #[structopt(name = "run")] + Run { + #[structopt(short, long)] + privilege_level: String, + #[structopt(short, long)] + command: String, + #[structopt(short, long)] + args: Vec, + }, + #[structopt(name = "user")] + User { + #[structopt(subcommand)] + cmd: UserCommand, + }, +} + +#[derive(StructOpt, Debug)] +enum UserCommand { + Add { + username: String, + #[structopt(short, long)] + privilege: String, + }, + Remove { + username: String, + }, + ChangePassword { + username: String, + }, + ChangePrivilege { + username: String, + privilege: String, + }, + AddToGroup { + username: String, + group: String, + }, + RemoveFromGroup { + username: String, + group: String, + }, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize logging + log4rs::init_file("log4rs.yaml", Default::default())?; + + let opt = Opt::from_args(); + + let mut config = Config::load()?; + let privilege_config = PrivilegeConfig::default(); + let mut rate_limiter = RateLimiter::new(5, Duration::from_secs(60)); + let privilege_manager = PrivilegeManager::new(privilege_config); + + match opt { + Opt::Run { + privilege_level, + command, + args, + } => { + println!("Enter username:"); + let mut username = String::new(); + std::io::stdin().read_line(&mut username)?; + let username = username.trim(); + + println!("Enter password:"); + let mut password = String::new(); + std::io::stdin().read_line(&mut password)?; + let password = password.trim(); + + println!("Enter TOTP code:"); + let mut totp_code = String::new(); + std::io::stdin().read_line(&mut totp_code)?; + let totp_code = totp_code.trim(); + + if let Some(user) = + authenticate_user(&config, username, password, totp_code, &mut rate_limiter).await? + { + let session = Session { + user: user.clone(), + expiry: SystemTime::now() + Duration::from_secs(config.session_duration), + }; + + let privilege_level = match privilege_level.as_str() { + "bell" => PrivilegeLevel::Bell, + "root" => PrivilegeLevel::Root, + "admin" => PrivilegeLevel::Administrator, + "user" => PrivilegeLevel::User, + _ => { + println!( + "Invalid privilege level. Use 'bell', 'root', 'admin', or 'user'." + ); + return Ok(()); + } + }; + + run_command_with_privilege( + &session, + &command, + &args, + privilege_level, + &config, + &privilege_manager, + ) + .await?; + } else { + println!("Authentication failed."); + } + } + Opt::User { cmd } => match cmd { + UserCommand::Add { + username, + privilege, + } => { + println!("Enter new password:"); + let mut password = String::new(); + std::io::stdin().read_line(&mut password)?; + let password = password.trim(); + + let privilege_level = match privilege.as_str() { + "bell" => PrivilegeLevel::Bell, + "root" => PrivilegeLevel::Root, + "admin" | "administrator" => PrivilegeLevel::Administrator, + "user" => PrivilegeLevel::User, + _ => { + println!( + "Invalid privilege level. Use 'bell', 'root', 'admin', or 'user'." + ); + return Ok(()); + } + }; + add_user(&mut config, &username, password, privilege_level).await?; + } + UserCommand::Remove { username } => { + remove_user(&mut config, &username).await?; + } + UserCommand::ChangePassword { username } => { + println!("Enter new password:"); + let mut password = String::new(); + std::io::stdin().read_line(&mut password)?; + let password = password.trim(); + + change_password(&mut config, &username, password).await?; + } + UserCommand::ChangePrivilege { + username, + privilege, + } => { + let privilege_level = match privilege.as_str() { + "bell" => PrivilegeLevel::Bell, + "root" => PrivilegeLevel::Root, + "admin" | "administrator" => PrivilegeLevel::Administrator, + "user" => PrivilegeLevel::User, + _ => { + println!( + "Invalid privilege level. Use 'bell', 'root', 'admin', or 'user'." + ); + return Ok(()); + } + }; + change_privilege(&mut config, &username, privilege_level).await?; + } + UserCommand::AddToGroup { username, group } => { + add_user_to_group(&mut config, &username, &group).await?; + } + UserCommand::RemoveFromGroup { username, group } => { + remove_user_from_group(&mut config, &username, &group).await?; + } + }, + } + + Ok(()) +} diff --git a/src/command/command.rs b/src/command/command.rs new file mode 100644 index 0000000..f605896 --- /dev/null +++ b/src/command/command.rs @@ -0,0 +1,693 @@ +// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::collections::HashSet; +use std::path::PathBuf; +use std::time::{Duration, SystemTime}; +use tokio::process::Command as TokioCommand; + +use anyhow::{Context, Result}; +use nix::unistd::{Gid, Uid}; +use serde::{Deserialize, Serialize}; +use syscallz::{Context as SyscallContext, Syscall}; +use tokio::time::timeout; + +use crate::audit::audit::log_audit_event; +use crate::authentication_compliance::authentication::Session; +use crate::config::config::Config; +use crate::network::network::{is_network_allowed, isolate_network, restore_network}; +use crate::user_privilege::privilege::{PrivilegeLevel, PrivilegeManager}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CommandConfig { + dangerous_patterns: HashSet, + allowed_paths: Vec, + max_execution_time: Duration, + sandbox_enabled: bool, + network_isolation_required: bool, + max_output_size: usize, + log_output: bool, +} + +impl Default for CommandConfig { + fn default() -> Self { + let allowed_paths = match std::env::consts::OS { + "macos" => vec![ + PathBuf::from("/usr/bin"), + PathBuf::from("/usr/local/bin"), + PathBuf::from("/opt/homebrew/bin"), + ], + "linux" => vec![ + PathBuf::from("/usr/bin"), + PathBuf::from("/usr/local/bin"), + PathBuf::from("/bin"), + ], + "bellandeos" => vec![ + PathBuf::from("/bell/bin"), + PathBuf::from("/bell/usr/bin"), + PathBuf::from("/bell/local/bin"), + ], + _ => vec![], + }; + + let mut dangerous_patterns = HashSet::new(); + dangerous_patterns.insert("rm -rf /*".to_string()); + dangerous_patterns.insert("chmod 777".to_string()); + dangerous_patterns.insert("dd if=/dev/zero".to_string()); + dangerous_patterns.insert("mkfs".to_string()); + dangerous_patterns.insert("> /dev/sda".to_string()); + dangerous_patterns.insert(":(){ :|:& };:".to_string()); // Fork bomb + dangerous_patterns.insert("sudo rm".to_string()); + dangerous_patterns.insert("> /dev/null".to_string()); + + CommandConfig { + dangerous_patterns, + allowed_paths, + max_execution_time: Duration::from_secs(300), + sandbox_enabled: true, + network_isolation_required: true, + max_output_size: 1024 * 1024, + log_output: true, + } + } +} + +// Create a wrapper that implements Debug +struct SandboxContext { + inner: SyscallContext, +} + +impl std::fmt::Debug for SandboxContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SandboxContext") + .field("inner", &"SyscallContext") + .finish() + } +} + +impl SandboxContext { + fn new(context: SyscallContext) -> Self { + Self { inner: context } + } + + fn load(&self) -> Result<()> { + self.inner + .load() + .map_err(|e| anyhow::anyhow!("Failed to load sandbox: {}", e)) + } + + fn allow_syscall(&mut self, syscall: Syscall) -> Result<()> { + self.inner + .allow_syscall(syscall) + .map_err(|e| anyhow::anyhow!("Failed to allow syscall: {}", e)) + } +} + +#[derive(Debug)] +struct CommandContext { + command: String, + args: Vec, + privilege_level: PrivilegeLevel, + username: String, + start_time: SystemTime, + sandbox: Option, + config: CommandConfig, +} + +#[derive(Debug, Clone)] +struct DangerousPattern { + pattern: String, + description: String, +} + +impl From<(&str, &str)> for DangerousPattern { + fn from((pattern, description): (&str, &str)) -> Self { + DangerousPattern { + pattern: pattern.to_string(), + description: description.to_string(), + } + } +} + +// Implementation for command validation and execution +pub async fn run_command_with_privilege( + session: &Session, + command: &str, + args: &[String], + required_privilege: PrivilegeLevel, + config: &Config, + privilege_manager: &PrivilegeManager, +) -> Result<()> { + let cmd_config = CommandConfig::default(); + + // Validate input + validate_command_input(command, args)?; + + // Check privileges and session + check_session_and_permissions( + session, + command, + args, + required_privilege, + config, + privilege_manager, + &cmd_config, // Add the command config parameter + ) + .await?; + + // Create and execute command context + let ctx = create_command_context(command, args, required_privilege, session, &cmd_config)?; + + execute_command_safely(ctx).await +} + +fn validate_command_input(command: &str, args: &[String]) -> Result<()> { + if command.is_empty() { + return Err(anyhow::anyhow!("Command cannot be empty")); + } + + // Check for null bytes and other dangerous characters + if command.contains('\0') || args.iter().any(|arg| arg.contains('\0')) { + return Err(anyhow::anyhow!("Command contains invalid characters")); + } + + // Validate command path + let command_path = PathBuf::from(command); + if !command_path.is_absolute() { + return Err(anyhow::anyhow!("Command must use absolute path")); + } + + Ok(()) +} + +async fn check_session_and_permissions( + session: &Session, + command: &str, + args: &[String], + required_privilege: PrivilegeLevel, + config: &Config, + privilege_manager: &PrivilegeManager, + cmd_config: &CommandConfig, +) -> Result<()> { + // Check session state + check_session_state(session, command, args).await?; + + // Check permissions + check_command_permissions( + session, + command, + args, + required_privilege, + config, + privilege_manager, + ) + .await?; + + // Check network access if required by command config + if cmd_config.network_isolation_required { + check_network_access(config, session, command, args).await?; + } + + Ok(()) +} + +async fn check_session_state(session: &Session, command: &str, args: &[String]) -> Result<()> { + // Check session expiry + if SystemTime::now() > session.expiry { + log_audit_event( + "SESSION_EXPIRED", + &session.user.username, + &format!( + "Attempted to run command with expired session: {} {:?}", + command, args + ), + ) + .await?; + return Err(anyhow::anyhow!( + "Session expired. Please authenticate again." + )); + } + + // Check that user exists and is valid + if session.user.username.is_empty() { + log_audit_event( + "INVALID_SESSION", + "unknown", + &format!( + "Attempted to run command without valid user: {} {:?}", + command, args + ), + ) + .await?; + return Err(anyhow::anyhow!("Invalid session: no user associated")); + } + + Ok(()) +} + +async fn check_command_permissions( + session: &Session, + command: &str, + args: &[String], + required_privilege: PrivilegeLevel, + config: &Config, + privilege_manager: &PrivilegeManager, +) -> Result { + // Check base user privileges + if !privilege_manager + .check_permission(&session.user, required_privilege, config) + .await? + { + log_audit_event( + "PERMISSION_DENIED", + &session.user.username, + &format!( + "Insufficient privileges for command: {} {:?}, required: {:?}", + command, args, required_privilege + ), + ) + .await?; + return Ok(false); // Return Ok(false) instead of Err + } + + // Check if user belongs to required groups + let has_required_group = session.user.groups.iter().any(|group| { + config + .groups + .iter() + .any(|g| &g.name == group && g.permissions.contains(&required_privilege.to_string())) + }); + + if !has_required_group && required_privilege > session.user.privilege { + log_audit_event( + "GROUP_PERMISSION_DENIED", + &session.user.username, + &format!( + "User lacks required group membership for command: {} {:?}", + command, args + ), + ) + .await?; + return Ok(false); // Return Ok(false) instead of Err + } + + Ok(true) // Return Ok(true) if all checks pass +} + +async fn check_network_access( + config: &Config, + session: &Session, + command: &str, + args: &[String], +) -> Result { + // Use is_network_allowed directly with the config + if !is_network_allowed(config).await? { + log_audit_event( + "NETWORK_DENIED", + &session.user.username, + &format!("Network access denied for: {} {:?}", command, args), + ) + .await?; + return Ok(false); + } + + Ok(true) +} + +async fn execute_command_safely(ctx: CommandContext) -> Result<()> { + // Log command execution start + log_audit_event( + "COMMAND_START", + &ctx.username, + &format!("Executing: {} {:?}", ctx.command, ctx.args), + ) + .await?; + + // Check for dangerous patterns + check_dangerous_patterns(&ctx).await?; + + // Apply sandbox if enabled + if let Some(ref sandbox) = ctx.sandbox { + sandbox.load().context("Failed to load sandbox")?; + } + + // Drop privileges if necessary + if ctx.privilege_level != PrivilegeLevel::Bell { + drop_privileges().context("Failed to drop privileges")?; + } + + // Isolate network if required + let network_isolated = if ctx.config.network_isolation_required { + isolate_network().await?; + true + } else { + false + }; + + // Execute command with timeout + let result = execute_command_with_timeout(&ctx).await; + + // Restore network if it was isolated + if network_isolated { + restore_network().await?; + } + + // Handle command result + match result { + Ok(output) => process_command_output(&ctx, &output).await?, + Err(e) => { + log_audit_event( + "COMMAND_FAILED", + &ctx.username, + &format!("Command failed: {} - Error: {}", ctx.command, e), + ) + .await?; + return Err(e); + } + } + + Ok(()) +} + +fn create_command_context( + command: &str, + args: &[String], + privilege_level: PrivilegeLevel, + session: &Session, + cmd_config: &CommandConfig, +) -> Result { + Ok(CommandContext { + command: command.to_string(), + args: args.to_vec(), + privilege_level, + username: session.user.username.clone(), + start_time: SystemTime::now(), + sandbox: if cmd_config.sandbox_enabled { + Some(create_sandbox()?) + } else { + None + }, + config: cmd_config.clone(), + }) +} + +async fn execute_command_with_timeout(ctx: &CommandContext) -> Result { + // Create tokio command + let mut command = TokioCommand::new(&ctx.command); + command.args(&ctx.args); + + // Run with timeout + let output = timeout(ctx.config.max_execution_time, command.output()) + .await + .context("Command execution timed out")? + .context("Command execution failed")?; + + Ok(output) +} + +async fn process_command_output(ctx: &CommandContext, output: &std::process::Output) -> Result<()> { + // Check output size limits + if output.stdout.len() > ctx.config.max_output_size + || output.stderr.len() > ctx.config.max_output_size + { + log_audit_event( + "COMMAND_OUTPUT_TOO_LARGE", + &ctx.username, + &format!( + "Output size exceeds limit of {} bytes", + ctx.config.max_output_size + ), + ) + .await?; + return Err(anyhow::anyhow!("Command output exceeds size limit")); + } + + // Process stderr if present + if !output.stderr.is_empty() { + let stderr = String::from_utf8_lossy(&output.stderr); + log_audit_event( + "COMMAND_ERROR", + &ctx.username, + &format!("Command produced error output: {}", stderr), + ) + .await?; + } + + // Process stdout if logging is enabled + if ctx.config.log_output && !output.stdout.is_empty() { + let stdout = String::from_utf8_lossy(&output.stdout); + log_audit_event( + "COMMAND_OUTPUT", + &ctx.username, + &format!("Command output: {}", stdout), + ) + .await?; + } + + // Check exit status + if !output.status.success() { + return Err(anyhow::anyhow!( + "Command failed with exit code: {}", + output.status.code().unwrap_or(-1) + )); + } + + Ok(()) +} + +fn create_sandbox() -> Result { + let mut ctx = SyscallContext::init()?; + + use syscallz::Syscall; + + // Essential system calls + let essential_syscalls = [ + Syscall::read, + Syscall::write, + Syscall::exit, + Syscall::exit_group, + Syscall::brk, + Syscall::arch_prctl, + ]; + + // File operations + let file_syscalls = [ + Syscall::open, + Syscall::openat, + Syscall::close, + Syscall::access, + Syscall::getcwd, + Syscall::lseek, + Syscall::stat, + Syscall::fstat, + Syscall::lstat, + Syscall::readlink, + ]; + + // Memory management + let memory_syscalls = [ + Syscall::mmap, + Syscall::munmap, + Syscall::mprotect, + Syscall::mremap, + ]; + + // Process management + let process_syscalls = [ + Syscall::clone, + Syscall::fork, + Syscall::execve, + Syscall::kill, + Syscall::wait4, + Syscall::getpid, + Syscall::getppid, + Syscall::getuid, + Syscall::geteuid, + ]; + + // Allow the syscalls + for syscall in essential_syscalls + .iter() + .chain(file_syscalls.iter()) + .chain(memory_syscalls.iter()) + .chain(process_syscalls.iter()) + { + ctx.allow_syscall(*syscall) + .with_context(|| format!("Failed to add syscall rule: {:?}", syscall))?; + } + + Ok(SandboxContext::new(ctx)) +} +fn drop_privileges() -> Result<()> { + let nobody_uid = Uid::from_raw(65534); // nobody user + let nobody_gid = Gid::from_raw(65534); // nobody group + + // Clear supplementary groups first + nix::unistd::setgroups(&[]).context("Failed to clear supplementary groups")?; + + // Drop group privileges + nix::unistd::setresgid(nobody_gid, nobody_gid, nobody_gid) + .context("Failed to drop group privileges")?; + + // Drop user privileges + nix::unistd::setresuid(nobody_uid, nobody_uid, nobody_uid) + .context("Failed to drop user privileges")?; + + Ok(()) +} + +async fn check_dangerous_patterns(ctx: &CommandContext) -> Result<()> { + let full_command = format!("{} {}", ctx.command, ctx.args.join(" ")); + + // Check against dangerous patterns + for pattern in &ctx.config.dangerous_patterns { + if full_command.contains(pattern) { + log_audit_event( + "DANGEROUS_COMMAND", + &ctx.username, + &format!("Dangerous pattern detected: {}", pattern), + ) + .await?; + return Err(anyhow::anyhow!("Dangerous command pattern detected")); + } + } + + // OS-specific pattern checks + match std::env::consts::OS { + "macos" => check_macos_specific_patterns(ctx, &full_command).await?, + "linux" => check_linux_specific_patterns(ctx, &full_command).await?, + "bellandeos" => check_bellande_specific_patterns(ctx, &full_command).await?, + _ => {} + } + + Ok(()) +} + +fn convert_patterns(patterns: [(&str, &str); N]) -> Vec { + patterns.into_iter().map(DangerousPattern::from).collect() +} + +async fn check_macos_specific_patterns(ctx: &CommandContext, command: &str) -> Result<()> { + let dangerous_patterns = convert_patterns([ + ("diskutil eraseDisk", "Disk erasure attempt"), + ("csrutil disable", "SIP disable attempt"), + ("nvram", "NVRAM modification attempt"), + ("kextload", "Kernel extension loading attempt"), + ("spctl --master-disable", "Gatekeeper disable attempt"), + ]); + check_patterns(ctx, command, &dangerous_patterns).await +} + +async fn check_linux_specific_patterns(ctx: &CommandContext, command: &str) -> Result<()> { + let dangerous_patterns = convert_patterns([ + ("modprobe", "Kernel module loading attempt"), + ("insmod", "Kernel module insertion attempt"), + ("mount", "File system mounting attempt"), + ("sysctl -w", "Sysctl modification attempt"), + ("echo 1 > /proc/sys", "Sysctl modification attempt"), + ("iptables -F", "Firewall flush attempt"), + ]); + check_patterns(ctx, command, &dangerous_patterns).await +} + +async fn check_bellande_specific_patterns(ctx: &CommandContext, command: &str) -> Result<()> { + let dangerous_patterns = convert_patterns([ + ("bellctl system reset", "System reset attempt"), + ("bellctl security disable", "Security disable attempt"), + ("bellctl kernel modify", "Kernel modification attempt"), + ("bellctl firewall disable", "Firewall disable attempt"), + ("bellctl audit stop", "Audit stop attempt"), + ]); + check_patterns(ctx, command, &dangerous_patterns).await +} + +async fn check_patterns( + ctx: &CommandContext, + command: &str, + patterns: &[DangerousPattern], +) -> Result<()> { + for pattern in patterns { + if command.contains(&pattern.pattern) { + log_audit_event("DANGEROUS_COMMAND", &ctx.username, &pattern.description).await?; + return Err(anyhow::anyhow!("Dangerous command pattern detected")); + } + } + Ok(()) +} + +async fn check_macos_patterns(ctx: &CommandContext, command: &str) -> Result<()> { + let patterns = vec![ + DangerousPattern { + pattern: "diskutil eraseDisk".to_string(), + description: "Disk erasure attempt".to_string(), + }, + DangerousPattern { + pattern: "csrutil disable".to_string(), + description: "SIP disable attempt".to_string(), + }, + DangerousPattern { + pattern: "nvram".to_string(), + description: "NVRAM modification attempt".to_string(), + }, + DangerousPattern { + pattern: "kextload".to_string(), + description: "Kernel extension loading attempt".to_string(), + }, + ]; + + check_patterns(ctx, command, &patterns).await +} + +async fn check_linux_patterns(ctx: &CommandContext, command: &str) -> Result<()> { + let patterns = vec![ + DangerousPattern { + pattern: "modprobe".to_string(), + description: "Kernel module loading attempt".to_string(), + }, + DangerousPattern { + pattern: "insmod".to_string(), + description: "Kernel module insertion attempt".to_string(), + }, + DangerousPattern { + pattern: "mount".to_string(), + description: "File system mounting attempt".to_string(), + }, + DangerousPattern { + pattern: "sysctl -w".to_string(), + description: "Sysctl modification attempt".to_string(), + }, + ]; + + check_patterns(ctx, command, &patterns).await +} + +async fn check_bellande_patterns(ctx: &CommandContext, command: &str) -> Result<()> { + let patterns = vec![ + DangerousPattern { + pattern: "bellctl system reset".to_string(), + description: "System reset attempt".to_string(), + }, + DangerousPattern { + pattern: "bellctl security disable".to_string(), + description: "Security disable attempt".to_string(), + }, + DangerousPattern { + pattern: "bellctl kernel modify".to_string(), + description: "Kernel modification attempt".to_string(), + }, + ]; + + check_patterns(ctx, command, &patterns).await +} diff --git a/src/command/mod.rs b/src/command/mod.rs new file mode 100644 index 0000000..9fe7961 --- /dev/null +++ b/src/command/mod.rs @@ -0,0 +1 @@ +pub mod command; diff --git a/src/config/config.rs b/src/config/config.rs new file mode 100644 index 0000000..861c141 --- /dev/null +++ b/src/config/config.rs @@ -0,0 +1,334 @@ +// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::collections::HashSet; +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::os::unix::fs::OpenOptionsExt; +use std::path::PathBuf; +use std::time::Duration; + +use anyhow::{Context, Result}; +use log::warn; +use serde::{Deserialize, Serialize}; +use tokio::runtime::Runtime; + +use crate::hsm::hsm::{decrypt_data, encrypt_data}; +use crate::user_privilege::user::User; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Config { + pub users: Vec, + pub groups: Vec, + pub session_duration: u64, + pub allowed_commands: Vec, + pub denied_commands: Vec, + pub allowed_networks: Vec, + pub hsm_slot: u64, + pub hsm_pin: String, + pub security_settings: SecuritySettings, + pub os_specific: OsSpecificConfig, + pub paths: ConfigPaths, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Group { + pub name: String, + pub permissions: Vec, + pub members: Vec, + pub description: Option, + pub created_at: chrono::DateTime, + pub modified_at: chrono::DateTime, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SecuritySettings { + pub password_min_length: usize, + pub password_require_special: bool, + pub password_require_numbers: bool, + pub password_require_uppercase: bool, + pub max_login_attempts: usize, + pub lockout_duration: Duration, + pub session_timeout: Duration, + pub mfa_required: bool, + pub allowed_ip_ranges: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OsSpecificConfig { + pub macos: MacOSConfig, + pub linux: LinuxConfig, + pub bellandeos: BellandeOSConfig, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct MacOSConfig { + pub require_filevault: bool, + pub require_sip: bool, + pub allowed_applications: Vec, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct LinuxConfig { + pub selinux_mode: String, + pub require_apparmor: bool, + pub kernel_hardening: bool, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct BellandeOSConfig { + pub security_level: String, + pub require_secure_boot: bool, + pub enable_kernel_protection: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ConfigPaths { + pub config_dir: PathBuf, + pub log_dir: PathBuf, + pub backup_dir: PathBuf, +} + +impl Default for Config { + fn default() -> Self { + let os_paths = match std::env::consts::OS { + "macos" => ConfigPaths { + config_dir: PathBuf::from("/Library/Application Support/bell"), + log_dir: PathBuf::from("/var/log/bell"), + backup_dir: PathBuf::from("/var/backup/bell"), + }, + "linux" => ConfigPaths { + config_dir: PathBuf::from("/etc/bell"), + log_dir: PathBuf::from("/var/log/bell"), + backup_dir: PathBuf::from("/var/backup/bell"), + }, + "bellandeos" => ConfigPaths { + config_dir: PathBuf::from("/bell/etc/bell"), + log_dir: PathBuf::from("/bell/log/bell"), + backup_dir: PathBuf::from("/bell/backup/bell"), + }, + _ => ConfigPaths { + config_dir: PathBuf::from("./config"), + log_dir: PathBuf::from("./log"), + backup_dir: PathBuf::from("./backup"), + }, + }; + + Config { + users: Vec::new(), + groups: Vec::new(), + session_duration: 3600, + allowed_commands: get_default_allowed_commands(), + denied_commands: get_default_denied_commands(), + allowed_networks: vec!["127.0.0.1/8".to_string()], + hsm_slot: 0, + hsm_pin: String::new(), + security_settings: SecuritySettings { + password_min_length: 12, + password_require_special: true, + password_require_numbers: true, + password_require_uppercase: true, + max_login_attempts: 3, + lockout_duration: Duration::from_secs(300), + session_timeout: Duration::from_secs(3600), + mfa_required: true, + allowed_ip_ranges: vec!["192.168.0.0/16".to_string()], + }, + os_specific: OsSpecificConfig { + macos: MacOSConfig { + require_filevault: true, + require_sip: true, + allowed_applications: vec![], + }, + linux: LinuxConfig { + selinux_mode: "enforcing".to_string(), + require_apparmor: true, + kernel_hardening: true, + }, + bellandeos: BellandeOSConfig { + security_level: "high".to_string(), + require_secure_boot: true, + enable_kernel_protection: true, + }, + }, + paths: os_paths, + } + } +} + +impl Config { + pub fn load() -> Result { + let rt = Runtime::new()?; + rt.block_on(async { + let config_path = Self::get_config_path()?; + Self::ensure_directories_exist()?; + + let encrypted_config = + fs::read_to_string(&config_path).context("Failed to read config file")?; + + let decrypted_config = decrypt_data(&encrypted_config) + .await + .context("Failed to decrypt config file")?; + + let mut config: Config = + toml::from_str(&decrypted_config).context("Failed to parse config file")?; + + config.verify_integrity()?; + config.update_os_settings()?; + + Ok(config) + }) + } + + pub fn save(&self) -> Result<()> { + let rt = Runtime::new()?; + rt.block_on(async { + self.verify_integrity()?; + self.create_backup().await?; + + let config_str = toml::to_string(self).context("Failed to serialize config")?; + let encrypted_config = encrypt_data(&config_str) + .await + .context("Failed to encrypt config")?; + + let config_path = Self::get_config_path()?; + let mut file = OpenOptions::new() + .write(true) + .create(true) + .mode(0o600) + .open(&config_path) + .context("Failed to open config file for writing")?; + + file.write_all(encrypted_config.as_bytes()) + .context("Failed to write config file")?; + + Ok(()) + }) + } + + fn get_config_path() -> Result { + let config = Config::default(); + let config_file = config.paths.config_dir.join("config.toml"); + Ok(config_file) + } + + fn ensure_directories_exist() -> Result<()> { + let config = Config::default(); + fs::create_dir_all(&config.paths.config_dir)?; + fs::create_dir_all(&config.paths.log_dir)?; + fs::create_dir_all(&config.paths.backup_dir)?; + Ok(()) + } + + fn verify_integrity(&self) -> Result<()> { + if self.users.is_empty() { + warn!("No users defined in configuration"); + } + + for group in &self.groups { + for permission in &group.permissions { + if !is_valid_permission(permission) { + return Err(anyhow::anyhow!("Invalid permission: {}", permission)); + } + } + } + + let mut seen_users = HashSet::new(); + for user in &self.users { + if !seen_users.insert(&user.username) { + return Err(anyhow::anyhow!("Duplicate user: {}", user.username)); + } + } + + Ok(()) + } + + async fn create_backup(&self) -> Result<()> { + let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); + let backup_path = self + .paths + .backup_dir + .join(format!("config_backup_{}.toml", timestamp)); + + let config_str = toml::to_string(self)?; + let encrypted_backup = encrypt_data(&config_str).await?; + fs::write(backup_path, encrypted_backup)?; + + Ok(()) + } + + fn update_os_settings(&mut self) -> Result<()> { + match std::env::consts::OS { + "macos" => { + self.os_specific.macos = MacOSConfig { + require_filevault: true, + require_sip: true, + allowed_applications: get_default_macos_applications(), + }; + } + "linux" => { + self.os_specific.linux = LinuxConfig { + selinux_mode: "enforcing".to_string(), + require_apparmor: true, + kernel_hardening: true, + }; + } + "bellandeos" => { + self.os_specific.bellandeos = BellandeOSConfig { + security_level: "high".to_string(), + require_secure_boot: true, + enable_kernel_protection: true, + }; + } + _ => warn!("Unsupported operating system"), + } + Ok(()) + } +} + +fn get_default_allowed_commands() -> Vec { + match std::env::consts::OS { + "macos" => vec!["ls".to_string(), "cd".to_string(), "pwd".to_string()], + "linux" => vec!["ls".to_string(), "cd".to_string(), "pwd".to_string()], + "bellandeos" => vec!["bellctl".to_string(), "ls".to_string(), "cd".to_string()], + _ => vec![], + } +} + +fn get_default_denied_commands() -> Vec { + match std::env::consts::OS { + "macos" => vec!["rm -rf /*".to_string(), "sudo su -".to_string()], + "linux" => vec!["rm -rf /*".to_string(), "dd".to_string()], + "bellandeos" => vec![ + "bellctl system reset".to_string(), + "bellctl security disable".to_string(), + ], + _ => vec![], + } +} + +fn get_default_macos_applications() -> Vec { + vec![ + "/Applications/Terminal.app".to_string(), + "/Applications/Utilities/Terminal.app".to_string(), + ] +} + +fn is_valid_permission(permission: &str) -> bool { + matches!( + permission, + "read" | "write" | "execute" | "admin" | "system" + ) +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..ef68c36 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1 @@ +pub mod config; diff --git a/src/hsm/hsm.rs b/src/hsm/hsm.rs new file mode 100644 index 0000000..ef0b515 --- /dev/null +++ b/src/hsm/hsm.rs @@ -0,0 +1,820 @@ +// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::collections::HashMap; +use std::fs::{self, File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::SystemTime; + +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Key, Nonce, +}; +use anyhow::{Context, Result}; +use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; +use log::{info, warn}; +use rand::{rngs::OsRng, RngCore}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use tokio::sync::RwLock; + +const KEY_SIZE: usize = 32; +const NONCE_SIZE: usize = 12; +const KEY_ROTATION_DAYS: u64 = 30; + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeyStore { + label: String, + key_id: [u8; 8], + encrypted_key: Vec, + created_at: SystemTime, + rotated_at: Option, + fingerprint: String, + metadata: KeyMetadata, +} + +#[derive(Debug, Serialize, Deserialize)] +struct KeyMetadata { + algorithm: String, + key_type: KeyType, + usage: KeyUsage, + platform: String, +} + +#[derive(Debug, Serialize, Deserialize)] +enum KeyType { + Master, + Data, + Signing, + Authentication, +} + +#[derive(Debug, Serialize, Deserialize)] +enum KeyUsage { + Encryption, + Decryption, + Both, +} + +pub struct SecureStorage { + key_store: Arc>>, + storage_path: PathBuf, +} + +// Platform-specific implementations +#[cfg(unix)] +fn set_secure_permissions(options: &mut OpenOptions) -> &mut OpenOptions { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600) +} + +fn encode_sensitive_data(data: &[u8]) -> String { + base64.encode(data) +} + +fn decode_sensitive_data(data: &str) -> Result> { + base64 + .decode(data.trim()) + .context("Failed to decode base64 data") +} + +impl SecureStorage { + pub async fn new() -> Result { + let storage_path = get_platform_storage_path()?; + ensure_secure_directory(&storage_path)?; + + let key_store = Arc::new(RwLock::new(HashMap::new())); + let storage = SecureStorage { + key_store, + storage_path, + }; + + storage.initialize().await?; + Ok(storage) + } + + async fn initialize(&self) -> Result<()> { + // Load existing keys + self.load_keys().await?; + + // Check for key rotation + self.check_key_rotation().await?; + + // Initialize platform-specific secure storage + match std::env::consts::OS { + "macos" => self.initialize_keychain().await?, + "linux" => self.initialize_keyring().await?, + "bellandeos" => self.initialize_bellande_secure_store().await?, + _ => warn!("No platform-specific secure storage available"), + } + + Ok(()) + } + + async fn load_key_from_keyring(&self, line: &str) -> Result> { + let label = match line.split("bell_key_").nth(1) { + Some(l) => l, + None => return Ok(None), + }; + + let output = std::process::Command::new("keyctl") + .args(&["read", "user", &format!("bell_key_{}", label)]) + .output() + .context("Failed to read key from keyring")?; + + if output.status.success() { + let encoded_data = String::from_utf8_lossy(&output.stdout); + let key_data = decode_sensitive_data(&encoded_data)?; + let key_store: KeyStore = + serde_json::from_slice(&key_data).context("Failed to deserialize key store")?; + Ok(Some(key_store)) + } else { + Ok(None) + } + } + + async fn load_key_from_keychain(&self, line: &str) -> Result> { + let label = match line.split("bell_key_").nth(1) { + Some(l) => l, + None => return Ok(None), + }; + + let output = std::process::Command::new("security") + .args(&[ + "find-generic-password", + "-s", + &format!("bell_key_{}", label), + "-w", + ]) + .output() + .context("Failed to read key from keychain")?; + + if output.status.success() { + let encoded_data = String::from_utf8_lossy(&output.stdout); + let key_data = decode_sensitive_data(&encoded_data)?; + let key_store: KeyStore = + serde_json::from_slice(&key_data).context("Failed to deserialize key store")?; + Ok(Some(key_store)) + } else { + Ok(None) + } + } + + async fn load_key_from_bellande(&self, line: &str) -> Result> { + let label = match line.split("bell_key_").nth(1) { + Some(l) => l, + None => return Ok(None), + }; + + let output = std::process::Command::new("bellctl") + .args(&["secure-store", "get", &format!("bell_key_{}", label)]) + .output() + .context("Failed to read key from BellandeOS secure store")?; + + if output.status.success() { + let encoded_data = String::from_utf8_lossy(&output.stdout); + let key_data = decode_sensitive_data(&encoded_data)?; + let key_store: KeyStore = + serde_json::from_slice(&key_data).context("Failed to deserialize key store")?; + Ok(Some(key_store)) + } else { + Ok(None) + } + } + + async fn load_master_key_from_keychain(&self) -> Result> { + let output = std::process::Command::new("security") + .args(&["find-generic-password", "-s", "bell_master_key", "-w"]) + .output() + .context("Failed to read from keychain")?; + + if output.status.success() { + let encoded = String::from_utf8_lossy(&output.stdout); + let key_data = decode_sensitive_data(&encoded)?; + if key_data.len() != KEY_SIZE { + return Err(anyhow::anyhow!("Invalid key length")); + } + let key = Key::::from_slice(&key_data); + Ok(key.clone()) + } else { + self.generate_and_store_master_key().await + } + } + + async fn load_master_key_from_bellande(&self) -> Result> { + let output = std::process::Command::new("bellctl") + .args(&["secure-store", "get", "bell_master_key"]) + .output() + .context("Failed to read from BellandeOS secure store")?; + + if output.status.success() { + let encoded = String::from_utf8_lossy(&output.stdout); + let key_data = decode_sensitive_data(&encoded)?; + if key_data.len() != KEY_SIZE { + return Err(anyhow::anyhow!("Invalid key length")); + } + let key = Key::::from_slice(&key_data); + Ok(key.clone()) + } else { + self.generate_and_store_master_key().await + } + } + + async fn save_keys(&self) -> Result<()> { + let store = self.key_store.read().await; + + for key_store in store.values() { + let key_data = serde_json::to_string(key_store)?; + let encoded_data = encode_sensitive_data(key_data.as_bytes()); + + match std::env::consts::OS { + "macos" => { + std::process::Command::new("security") + .args(&[ + "add-generic-password", + "-s", + &format!("bell_key_{}", key_store.label), + "-w", + &encoded_data, + ]) + .output() + .context("Failed to store in keychain")?; + } + "linux" => { + std::process::Command::new("keyctl") + .args(&[ + "add", + "user", + &format!("bell_key_{}", key_store.label), + &encoded_data, + "@u", + ]) + .output() + .context("Failed to store in keyring")?; + } + "bellandeos" => { + std::process::Command::new("bellctl") + .args(&[ + "secure-store", + "set", + &format!("bell_key_{}", key_store.label), + &encoded_data, + ]) + .output() + .context("Failed to store in BellandeOS secure store")?; + } + _ => { + let key_file = self.storage_path.join(format!("{}.key", key_store.label)); + fs::write(key_file, &encoded_data)?; + } + } + } + + Ok(()) + } + + pub async fn generate_key( + &self, + label: &str, + key_type: KeyType, + usage: KeyUsage, + ) -> Result> { + let mut key = vec![0u8; KEY_SIZE]; + OsRng.fill_bytes(&mut key); + + let key_id = rand::random::<[u8; 8]>(); + let master_key = self.load_master_key().await?; + + let encrypted_key = self.encrypt_with_master_key(&master_key, &key).await?; + let fingerprint = calculate_key_fingerprint(&key); + + let key_store = KeyStore { + label: label.to_string(), + key_id, + encrypted_key, + created_at: SystemTime::now(), + rotated_at: None, + fingerprint, + metadata: KeyMetadata { + algorithm: "AES-256-GCM".to_string(), + key_type, + usage, + platform: std::env::consts::OS.to_string(), + }, + }; + + // Store in platform-specific secure storage + self.store_key_in_platform_storage(&key_store).await?; + + // Update in-memory store + let mut store = self.key_store.write().await; + store.insert(label.to_string(), key_store); + + // Save to disk + self.save_keys().await?; + + Ok(key_id.to_vec()) + } + + pub async fn encrypt_data(&self, data: &str) -> Result> { + let master_key = self.load_master_key().await?; + let cipher = Aes256Gcm::new(&master_key); + + let mut nonce = [0u8; NONCE_SIZE]; + OsRng.fill_bytes(&mut nonce); + let nonce = Nonce::from_slice(&nonce); + + let ciphertext = cipher + .encrypt(nonce, data.as_bytes()) + .context("Failed to encrypt data")?; + + let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len()); + result.extend_from_slice(nonce); + result.extend_from_slice(&ciphertext); + + log_crypto_operation("ENCRYPT", &result).await?; + Ok(result) + } + + pub async fn decrypt_data(&self, data: &[u8]) -> Result { + if data.len() < NONCE_SIZE { + anyhow::bail!("Invalid encrypted data"); + } + + let master_key = self.load_master_key().await?; + let cipher = Aes256Gcm::new(&master_key); + + let nonce = Nonce::from_slice(&data[..NONCE_SIZE]); + let ciphertext = &data[NONCE_SIZE..]; + + let plaintext = cipher + .decrypt(nonce, ciphertext) + .context("Failed to decrypt data")?; + + log_crypto_operation("DECRYPT", data).await?; + + String::from_utf8(plaintext).context("Failed to convert decrypted data to string") + } + + async fn load_master_key_from_keyring(&self) -> Result> { + let output = std::process::Command::new("keyctl") + .args(&["read", "user", "bell_master_key"]) + .output() + .context("Failed to read from keyring")?; + + if output.status.success() { + let encoded = String::from_utf8_lossy(&output.stdout); + let key_data = decode_sensitive_data(&encoded)?; + if key_data.len() != KEY_SIZE { + return Err(anyhow::anyhow!("Invalid key length")); + } + let key = Key::::from_slice(&key_data); + Ok(key.clone()) + } else { + self.generate_and_store_master_key().await + } + } + + async fn load_master_key(&self) -> Result> { + match std::env::consts::OS { + "macos" => self.load_master_key_from_keychain().await, + "linux" => self.load_master_key_from_keyring().await, + "bellandeos" => self.load_master_key_from_bellande().await, + _ => self.load_master_key_from_file().await, + } + } + + async fn load_master_key_from_file(&self) -> Result> { + let master_key_path = self.storage_path.join("master.key"); + + if master_key_path.exists() { + let mut file = + File::open(&master_key_path).context("Failed to open master key file")?; + + let mut key_bytes = [0u8; KEY_SIZE]; + file.read_exact(&mut key_bytes) + .context("Failed to read master key")?; + + let key = Key::::from_slice(&key_bytes); + Ok(key.clone()) + } else { + self.generate_and_store_master_key().await + } + } + + async fn encrypt_with_master_key( + &self, + master_key: &Key, + data: &[u8], + ) -> Result> { + let cipher = Aes256Gcm::new(master_key); + let nonce = Nonce::from_slice(&[0u8; NONCE_SIZE]); + cipher + .encrypt(nonce, data) + .context("Failed to encrypt with master key") + } + + async fn check_key_rotation(&self) -> Result<()> { + let mut store = self.key_store.write().await; + let now = SystemTime::now(); + + for key_store in store.values_mut() { + let last_rotation = key_store.rotated_at.unwrap_or(key_store.created_at); + if now.duration_since(last_rotation)?.as_secs() > KEY_ROTATION_DAYS * 24 * 60 * 60 { + let mut new_key = vec![0u8; KEY_SIZE]; + OsRng.fill_bytes(&mut new_key); + + let master_key = self.load_master_key().await?; + key_store.encrypted_key = + self.encrypt_with_master_key(&master_key, &new_key).await?; + key_store.rotated_at = Some(now); + key_store.fingerprint = calculate_key_fingerprint(&new_key); + + self.store_key_in_platform_storage(key_store).await?; + } + } + + Ok(()) + } + + // Platform-specific implementations + async fn initialize_keychain(&self) -> Result<()> { + let output = std::process::Command::new("security") + .args(&["create-keychain", "bell.keychain"]) + .output() + .context("Failed to create keychain")?; + + if !output.status.success() { + warn!("Keychain already exists or creation failed"); + } + + Ok(()) + } + + async fn initialize_keyring(&self) -> Result<()> { + let output = std::process::Command::new("keyctl") + .args(&["new_session"]) + .output() + .context("Failed to create keyring session")?; + + if !output.status.success() { + warn!("Keyring session creation failed"); + } + + Ok(()) + } + + async fn initialize_bellande_secure_store(&self) -> Result<()> { + let output = std::process::Command::new("bellctl") + .args(&["secure-store", "init"]) + .output() + .context("Failed to initialize BellandeOS secure store")?; + + if !output.status.success() { + warn!("BellandeOS secure store initialization failed"); + } + + Ok(()) + } + + async fn generate_and_store_master_key(&self) -> Result> { + let mut key_bytes = [0u8; KEY_SIZE]; + OsRng.fill_bytes(&mut key_bytes); + let key = Key::::from_slice(&key_bytes).clone(); + let encoded_key = encode_sensitive_data(&key_bytes); + + match std::env::consts::OS { + "macos" => { + std::process::Command::new("security") + .args(&[ + "add-generic-password", + "-s", + "bell_master_key", + "-w", + &encoded_key, + ]) + .output() + .context("Failed to store in keychain")?; + } + "linux" => { + std::process::Command::new("keyctl") + .args(&["add", "user", "bell_master_key", &encoded_key, "@u"]) + .output() + .context("Failed to store in keyring")?; + } + "bellandeos" => { + std::process::Command::new("bellctl") + .args(&["secure-store", "set", "bell_master_key", &encoded_key]) + .output() + .context("Failed to store in BellandeOS secure store")?; + } + _ => { + self.store_master_key_to_file(&key_bytes).await?; + } + } + + Ok(key) + } + + async fn store_master_key_to_file(&self, key: &[u8]) -> Result<()> { + let master_key_path = self.storage_path.join("master.key"); + + let mut options = OpenOptions::new(); + options.write(true).create(true).truncate(true); + let mut file = set_secure_permissions(&mut options) + .open(&master_key_path) + .context("Failed to create master key file")?; + + file.write_all(key).context("Failed to write master key")?; + file.sync_all().context("Failed to sync master key file")?; + + Ok(()) + } + + async fn store_key_in_platform_storage(&self, key_store: &KeyStore) -> Result<()> { + let key_data = serde_json::to_string(key_store)?; + let encoded_data = encode_sensitive_data(key_data.as_bytes()); + + match std::env::consts::OS { + "macos" => self.store_key_in_keychain(key_store, &encoded_data).await?, + "linux" => self.store_key_in_keyring(key_store, &encoded_data).await?, + "bellandeos" => self.store_key_in_bellande(key_store, &encoded_data).await?, + _ => self.store_key_in_file(key_store, &encoded_data).await?, + } + + Ok(()) + } + + async fn store_key_in_keychain(&self, key_store: &KeyStore, encoded_data: &str) -> Result<()> { + let output = std::process::Command::new("security") + .args(&[ + "add-generic-password", + "-s", + &format!("bell_key_{}", key_store.label), + "-w", + encoded_data, + ]) + .output() + .context("Failed to store key in keychain")?; + + if !output.status.success() { + // Try to delete existing entry first and retry + let _ = std::process::Command::new("security") + .args(&[ + "delete-generic-password", + "-s", + &format!("bell_key_{}", key_store.label), + ]) + .output(); + + std::process::Command::new("security") + .args(&[ + "add-generic-password", + "-s", + &format!("bell_key_{}", key_store.label), + "-w", + encoded_data, + ]) + .output() + .context("Failed to store key in keychain after deletion")?; + } + + Ok(()) + } + + async fn store_key_in_keyring(&self, key_store: &KeyStore, encoded_data: &str) -> Result<()> { + // First, try to remove any existing key + let _ = std::process::Command::new("keyctl") + .args(&["unlink", &format!("bell_key_{}", key_store.label), "@u"]) + .output(); + + let output = std::process::Command::new("keyctl") + .args(&[ + "add", + "user", + &format!("bell_key_{}", key_store.label), + encoded_data, + "@u", + ]) + .output() + .context("Failed to store key in keyring")?; + + if !output.status.success() { + anyhow::bail!("Failed to store key in keyring: {:?}", output); + } + + Ok(()) + } + + async fn store_key_in_bellande(&self, key_store: &KeyStore, encoded_data: &str) -> Result<()> { + let output = std::process::Command::new("bellctl") + .args(&[ + "secure-store", + "set", + &format!("bell_key_{}", key_store.label), + encoded_data, + ]) + .output() + .context("Failed to store key in BellandeOS secure store")?; + + if !output.status.success() { + // Try to delete and retry + let _ = std::process::Command::new("bellctl") + .args(&[ + "secure-store", + "delete", + &format!("bell_key_{}", key_store.label), + ]) + .output(); + + std::process::Command::new("bellctl") + .args(&[ + "secure-store", + "set", + &format!("bell_key_{}", key_store.label), + encoded_data, + ]) + .output() + .context("Failed to store key in BellandeOS secure store after deletion")?; + } + + Ok(()) + } + + async fn store_key_in_file(&self, key_store: &KeyStore, encoded_data: &str) -> Result<()> { + let key_file = self.storage_path.join(format!("{}.key", key_store.label)); + + // Create a temporary file first + let temp_file = key_file.with_extension("tmp"); + + // Write to temporary file + let mut options = OpenOptions::new(); + options.write(true).create(true).truncate(true); + let mut file = set_secure_permissions(&mut options) + .open(&temp_file) + .context("Failed to create temporary key file")?; + + file.write_all(encoded_data.as_bytes()) + .context("Failed to write key data")?; + + file.sync_all().context("Failed to sync key file")?; + + // Atomically rename temporary file to final location + fs::rename(&temp_file, &key_file).context("Failed to save key file")?; + + Ok(()) + } + + async fn load_keys(&self) -> Result<()> { + let mut store = self.key_store.write().await; + + match std::env::consts::OS { + "macos" => { + let output = std::process::Command::new("security") + .args(&["dump-keychain"]) + .output() + .context("Failed to dump keychain")?; + + if output.status.success() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if line.contains("bell_key_") { + if let Some(key_store) = self.load_key_from_keychain(line).await? { + store.insert(key_store.label.clone(), key_store); + } + } + } + } + } + "linux" => { + let output = std::process::Command::new("keyctl") + .args(&["list", "@u"]) + .output() + .context("Failed to list keyring")?; + + if output.status.success() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if line.contains("bell_key_") { + if let Some(key_store) = self.load_key_from_keyring(line).await? { + store.insert(key_store.label.clone(), key_store); + } + } + } + } + } + "bellandeos" => { + let output = std::process::Command::new("bellctl") + .args(&["secure-store", "list"]) + .output() + .context("Failed to list BellandeOS secure store")?; + + if output.status.success() { + for line in String::from_utf8_lossy(&output.stdout).lines() { + if line.contains("bell_key_") { + if let Some(key_store) = self.load_key_from_bellande(line).await? { + store.insert(key_store.label.clone(), key_store); + } + } + } + } + } + _ => { + // Fallback to file-based storage + if let Ok(entries) = fs::read_dir(&self.storage_path) { + for entry in entries { + if let Ok(entry) = entry { + if let Some(filename) = entry.file_name().to_str() { + if filename.ends_with(".key") { + if let Ok(key_data) = fs::read_to_string(entry.path()) { + match serde_json::from_str::(&key_data) { + Ok(key_store) => { + store.insert(key_store.label.clone(), key_store); + } + Err(err) => { + warn!("Failed to deserialize key store: {}", err); + continue; + } + } + } + } + } + } + } + } + } + } + + Ok(()) + } +} + +fn get_platform_storage_path() -> Result { + let path = match std::env::consts::OS { + "macos" => PathBuf::from("/Library/Application Support/bell/secure"), + "linux" => PathBuf::from("/var/lib/bell/secure"), + "bellandeos" => PathBuf::from("/bell/secure/storage"), + _ => { + let mut path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + path.push("secure"); + path + } + }; + + Ok(path) +} + +fn ensure_secure_directory(path: &Path) -> Result<()> { + if !path.exists() { + fs::create_dir_all(path)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(path, fs::Permissions::from_mode(0o700))?; + } + } + + Ok(()) +} + +fn calculate_key_fingerprint(key: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(key); + format!("{:x}", hasher.finalize()) +} + +async fn log_crypto_operation(operation: &str, data: &[u8]) -> Result<()> { + let fingerprint = calculate_key_fingerprint(data); + info!( + "Crypto operation: {} - Size: {} bytes - Fingerprint: {}", + operation, + data.len(), + fingerprint + ); + Ok(()) +} + +pub async fn encrypt_data(data: &str) -> anyhow::Result { + let storage = SecureStorage::new().await?; + let encrypted = storage.encrypt_data(data).await?; + Ok(base64::engine::general_purpose::STANDARD.encode(&encrypted)) +} + +pub async fn decrypt_data(encrypted_data: &str) -> anyhow::Result { + let storage = SecureStorage::new().await?; + let data = base64::engine::general_purpose::STANDARD.decode(encrypted_data)?; + storage.decrypt_data(&data).await +} diff --git a/src/hsm/mod.rs b/src/hsm/mod.rs new file mode 100644 index 0000000..52585b8 --- /dev/null +++ b/src/hsm/mod.rs @@ -0,0 +1 @@ +pub mod hsm; diff --git a/src/network/mod.rs b/src/network/mod.rs new file mode 100644 index 0000000..a61610b --- /dev/null +++ b/src/network/mod.rs @@ -0,0 +1 @@ +pub mod network; diff --git a/src/network/network.rs b/src/network/network.rs new file mode 100644 index 0000000..624f029 --- /dev/null +++ b/src/network/network.rs @@ -0,0 +1,522 @@ +// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::fs::{File, OpenOptions}; +use std::io::Write; +use std::net::IpAddr; +use std::path::PathBuf; +use std::process::Command; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use anyhow::{Context, Result}; +use chrono::{DateTime, Utc}; +use ipnetwork::Ipv4Network; +use log::{error, info, warn}; +use serde::{Deserialize, Serialize}; +use tokio::time::sleep; + +use crate::config::config::Config; + +#[derive(Debug, Serialize, Deserialize)] +pub struct NetworkConfig { + pub interface: String, + pub namespace: String, + pub allowed_ports: Vec, + pub dns_servers: Vec, + pub retry_attempts: u32, + pub retry_delay: u64, +} + +impl Default for NetworkConfig { + fn default() -> Self { + NetworkConfig { + interface: get_default_interface(), + namespace: "bell_isolated".to_string(), + allowed_ports: vec![53, 80, 443], // DNS, HTTP, HTTPS + dns_servers: vec!["8.8.8.8".to_string(), "8.8.4.4".to_string()], + retry_attempts: 3, + retry_delay: 1, + } + } +} + +#[derive(Debug)] +struct NetworkCommands { + down_cmd: Vec, + up_cmd: Vec, + flush_cmd: Vec, + firewall_cmd: Vec, +} + +impl NetworkCommands { + fn new() -> Self { + match std::env::consts::OS { + "macos" => Self { + down_cmd: vec!["ifconfig".into(), "{interface}".into(), "down".into()], + up_cmd: vec!["ifconfig".into(), "{interface}".into(), "up".into()], + flush_cmd: vec![ + "ifconfig".into(), + "{interface}".into(), + "inet".into(), + "0".into(), + ], + firewall_cmd: vec!["pfctl".into(), "-f".into(), "/etc/pf.conf".into()], + }, + "linux" => Self { + down_cmd: vec![ + "ip".into(), + "link".into(), + "set".into(), + "{interface}".into(), + "down".into(), + ], + up_cmd: vec![ + "ip".into(), + "link".into(), + "set".into(), + "{interface}".into(), + "up".into(), + ], + flush_cmd: vec![ + "ip".into(), + "addr".into(), + "flush".into(), + "dev".into(), + "{interface}".into(), + ], + firewall_cmd: vec!["iptables".into(), "-F".into()], + }, + "bellandeos" => Self { + down_cmd: vec![ + "bellctl".into(), + "net".into(), + "down".into(), + "{interface}".into(), + ], + up_cmd: vec![ + "bellctl".into(), + "net".into(), + "up".into(), + "{interface}".into(), + ], + flush_cmd: vec![ + "bellctl".into(), + "net".into(), + "flush".into(), + "{interface}".into(), + ], + firewall_cmd: vec!["bellctl".into(), "firewall".into(), "reset".into()], + }, + _ => Self { + down_cmd: vec![], + up_cmd: vec![], + flush_cmd: vec![], + firewall_cmd: vec![], + }, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct AuditEvent { + timestamp: DateTime, + event_type: String, + user: String, + message: String, + source_ip: Option, + severity: AuditSeverity, +} + +#[derive(Debug, Serialize, Deserialize)] +enum AuditSeverity { + Info, + Warning, + Error, + Critical, +} + +impl Default for AuditSeverity { + fn default() -> Self { + AuditSeverity::Info + } +} + +pub async fn isolate_network() -> Result<()> { + let config = NetworkConfig::default(); + let commands = NetworkCommands::new(); + + info!( + "Starting network isolation process for {}", + std::env::consts::OS + ); + + let down_cmd = replace_interface_placeholder(&commands.down_cmd, &config.interface); + if let Some((cmd, args)) = down_cmd.split_first() { + run_command(cmd, args).await?; + } + + let flush_cmd = replace_interface_placeholder(&commands.flush_cmd, &config.interface); + if let Some((cmd, args)) = flush_cmd.split_first() { + run_command(cmd, args).await?; + } + + setup_firewall_rules(&config).await?; + + log_audit_event( + "NETWORK_ISOLATION", + "SYSTEM", + &format!( + "Network isolated on {}: {}", + std::env::consts::OS, + config.interface + ), + ) + .await?; + + Ok(()) +} + +pub async fn restore_network() -> Result<()> { + let config = NetworkConfig::default(); + let commands = NetworkCommands::new(); + + info!( + "Starting network restoration process for {}", + std::env::consts::OS + ); + + let up_cmd = replace_interface_placeholder(&commands.up_cmd, &config.interface); + if let Some((cmd, args)) = up_cmd.split_first() { + run_command(cmd, args).await?; + } + + let mut attempts = 0; + while attempts < config.retry_attempts { + match request_dhcp_lease(&config.interface).await { + Ok(_) => break, + Err(e) => { + warn!("DHCP request failed, attempt {}: {}", attempts + 1, e); + if attempts + 1 == config.retry_attempts { + return Err(e); + } + sleep(Duration::from_secs(config.retry_delay)).await; + attempts += 1; + } + } + } + + log_audit_event( + "NETWORK_RESTORATION", + "SYSTEM", + &format!( + "Network restored on {}: {}", + std::env::consts::OS, + config.interface + ), + ) + .await?; + + Ok(()) +} + +async fn run_command(cmd: &str, args: &[String]) -> Result<()> { + let status = Command::new(cmd) + .args(args) + .status() + .context(format!("Failed to run command: {} {:?}", cmd, args))?; + + if !status.success() { + error!("Command failed: {} {:?}", cmd, args); + anyhow::bail!("Command failed with status: {}", status); + } + + Ok(()) +} + +fn replace_interface_placeholder(cmd: &[String], interface: &str) -> Vec { + cmd.iter() + .map(|s| s.replace("{interface}", interface)) + .collect() +} + +async fn setup_firewall_rules(config: &NetworkConfig) -> Result<()> { + match std::env::consts::OS { + "macos" => setup_pf_firewall(config).await?, + "linux" => setup_iptables_firewall(config).await?, + "bellandeos" => setup_bell_firewall(config).await?, + _ => anyhow::bail!("Unsupported operating system"), + } + Ok(()) +} + +async fn setup_pf_firewall(config: &NetworkConfig) -> Result<()> { + let pf_rules = generate_pf_rules(config); + std::fs::write("/etc/pf.conf", pf_rules).context("Failed to write PF configuration")?; + + run_command("pfctl", &["-f".to_string(), "/etc/pf.conf".to_string()]) + .await + .context("Failed to load PF rules")?; + run_command("pfctl", &["-e".to_string()]) + .await + .context("Failed to enable PF firewall")?; + + Ok(()) +} + +async fn setup_iptables_firewall(config: &NetworkConfig) -> Result<()> { + run_command("iptables", &["-F".to_string()]).await?; + + for port in &config.allowed_ports { + let port_str = port.to_string(); + let args = vec![ + "-A".to_string(), + "OUTPUT".to_string(), + "-p".to_string(), + "tcp".to_string(), + "--dport".to_string(), + port_str, + "-j".to_string(), + "ACCEPT".to_string(), + ]; + run_command("iptables", &args).await?; + } + + run_command( + "iptables", + &["-P".to_string(), "OUTPUT".to_string(), "DROP".to_string()], + ) + .await +} + +async fn setup_bell_firewall(config: &NetworkConfig) -> Result<()> { + run_command("bellctl", &["firewall".to_string(), "reset".to_string()]).await?; + + for port in &config.allowed_ports { + let port_str = port.to_string(); + let args = vec![ + "firewall".to_string(), + "allow".to_string(), + "port".to_string(), + port_str, + ]; + run_command("bellctl", &args).await?; + } + + run_command( + "bellctl", + &["firewall".to_string(), "default-deny".to_string()], + ) + .await +} + +fn generate_pf_rules(config: &NetworkConfig) -> String { + let mut rules = String::new(); + + rules.push_str("# Generated PF rules\n"); + rules.push_str("set skip on lo0\n"); + rules.push_str("set block-policy drop\n"); + rules.push_str("\n# Default deny all\n"); + rules.push_str("block all\n\n"); + + // Allow DNS to specified servers + rules.push_str("# Allow DNS to specified servers\n"); + for dns in &config.dns_servers { + rules.push_str(&format!("pass out proto udp to {} port 53\n", dns)); + } + + // Allow specified ports + rules.push_str("\n# Allow specified outbound ports\n"); + for port in &config.allowed_ports { + rules.push_str(&format!("pass out proto tcp to any port {}\n", port)); + } + + // Security rules + rules.push_str("\n# Security rules\n"); + rules.push_str("block in quick from urpf-failed\n"); + rules.push_str("block in quick from { 10/8, 172.16/12, 192.168/16 } to any\n"); + rules.push_str("block in quick from any to { 10/8, 172.16/12, 192.168/16 }\n"); + + rules +} + +fn get_default_interface() -> String { + match std::env::consts::OS { + "macos" => "en0".to_string(), + "linux" => "eth0".to_string(), + "bellandeos" => "bell0".to_string(), + _ => "unknown".to_string(), + } +} + +async fn request_dhcp_lease(interface: &str) -> Result<()> { + match std::env::consts::OS { + "macos" => { + run_command( + "ipconfig", + &["set".to_string(), interface.to_string(), "DHCP".to_string()], + ) + .await?; + } + "linux" => { + run_command("dhclient", &[interface.to_string()]).await?; + } + "bellandeos" => { + run_command( + "bellctl", + &["net".to_string(), "dhcp".to_string(), interface.to_string()], + ) + .await?; + } + _ => anyhow::bail!("Unsupported operating system"), + } + Ok(()) +} + +async fn log_audit_event(event_type: &str, user: &str, message: &str) -> Result<()> { + let event = AuditEvent { + timestamp: Utc::now(), + event_type: event_type.to_string(), + user: user.to_string(), + message: message.to_string(), + source_ip: get_source_ip().await, + severity: get_event_severity(event_type), + }; + + info!( + "Audit: {} - {}: {}", + event.event_type, event.user, event.message + ); + write_audit_log(&event).await?; + + if matches!(event.severity, AuditSeverity::Critical) { + flush_audit_log().await?; + } + + Ok(()) +} + +async fn get_source_ip() -> Option { + match local_ip_address::local_ip() { + Ok(ip) => Some(ip.to_string()), + Err(_) => None, + } +} + +fn get_event_severity(event_type: &str) -> AuditSeverity { + match event_type { + "NETWORK_ISOLATION" | "NETWORK_RESTORATION" => AuditSeverity::Warning, + "NETWORK_CHECK" => AuditSeverity::Info, + "SECURITY_VIOLATION" | "NETWORK_ATTACK" => AuditSeverity::Critical, + _ => AuditSeverity::Info, + } +} + +async fn write_audit_log(event: &AuditEvent) -> Result<()> { + let log_path = get_audit_log_path(); + + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + .context(format!("Failed to open audit log file: {:?}", log_path))?; + + let log_entry = serde_json::to_string(&event).context("Failed to serialize audit event")?; + + writeln!(file, "{}", log_entry).context("Failed to write to audit log")?; + + Ok(()) +} + +async fn flush_audit_log() -> Result<()> { + let log_path = get_audit_log_path(); + + let mut file = OpenOptions::new() + .append(true) + .open(&log_path) + .context("Failed to open audit log for flushing")?; + + file.sync_all() + .context("Failed to flush audit log to disk")?; + + Ok(()) +} + +fn get_audit_log_path() -> PathBuf { + match std::env::consts::OS { + "macos" => PathBuf::from("/var/log/security/audit.log"), + "linux" => PathBuf::from("/var/log/audit/audit.log"), + "bellandeos" => PathBuf::from("/bell/logs/audit/system.log"), + _ => PathBuf::from("audit.log"), + } +} + +pub async fn rotate_audit_logs() -> Result<()> { + let log_path = get_audit_log_path(); + + if let Ok(metadata) = std::fs::metadata(&log_path) { + // Rotate if file is larger than 10MB + if metadata.len() > 10_000_000 { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let backup_path = log_path.with_extension(format!("log.{}", timestamp)); + + std::fs::rename(&log_path, &backup_path).context("Failed to rotate audit log")?; + + File::create(&log_path).context("Failed to create new audit log after rotation")?; + + log_audit_event( + "AUDIT_LOG_ROTATION", + "SYSTEM", + &format!("Rotated audit log to {:?}", backup_path), + ) + .await?; + } + } + + Ok(()) +} + +pub async fn is_network_allowed(config: &Config) -> Result { + let local_ip = local_ip_address::local_ip().context("Failed to get local IP address")?; + + for network_str in &config.allowed_networks { + let network: Ipv4Network = network_str + .parse() + .context("Failed to parse network configuration")?; + + if let IpAddr::V4(ipv4) = local_ip { + if network.contains(ipv4) { + log_audit_event( + "NETWORK_CHECK", + "SYSTEM", + &format!("Network allowed: {}", local_ip), + ) + .await?; + return Ok(true); + } + } + } + + log_audit_event( + "NETWORK_CHECK", + "SYSTEM", + &format!("Network denied: {}", local_ip), + ) + .await?; + Ok(false) +} diff --git a/src/user_privilege/mod.rs b/src/user_privilege/mod.rs new file mode 100644 index 0000000..7d7164b --- /dev/null +++ b/src/user_privilege/mod.rs @@ -0,0 +1,2 @@ +pub mod privilege; +pub mod user; diff --git a/src/user_privilege/privilege.rs b/src/user_privilege/privilege.rs new file mode 100644 index 0000000..d9625d8 --- /dev/null +++ b/src/user_privilege/privilege.rs @@ -0,0 +1,314 @@ +// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::collections::HashMap; +use std::fmt; +use std::hash::Hash; +use std::str::FromStr; +use std::time::{Duration, SystemTime}; + +use anyhow::Result; +use log::error; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::audit::audit::log_audit_event; +use crate::config::config::Config; +use crate::user_privilege::user::User; +use chrono::Timelike; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy)] +pub enum PrivilegeLevel { + User, // Basic user privileges + Group, // Group-based privileges + Administrator, // Administrative privileges + Root, // Root-level access + Bell, // Highest level - system owner +} + +#[derive(Error, Debug)] +pub enum PrivilegeLevelError { + #[error("Invalid privilege level: {0}")] + InvalidPrivilegeLevel(String), + #[error("Insufficient privileges")] + InsufficientPrivileges, + #[error("Expired privileges")] + ExpiredPrivileges, + #[error("Group not found: {0}")] + GroupNotFound(String), + #[error("Permission not found: {0}")] + PermissionNotFound(String), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PrivilegeConfig { + pub elevation_timeout: Duration, + pub require_mfa: bool, + pub allowed_elevation_hours: Vec, + pub max_concurrent_elevations: usize, + pub restricted_commands: HashMap>, +} + +impl Default for PrivilegeConfig { + fn default() -> Self { + Self { + elevation_timeout: Duration::from_secs(3600), + require_mfa: true, + allowed_elevation_hours: (0..24).collect(), + max_concurrent_elevations: 3, + restricted_commands: HashMap::new(), + } + } +} + +#[derive(Debug)] +pub struct PrivilegeManager { + config: PrivilegeConfig, + active_elevations: HashMap>, +} + +#[derive(Debug)] +struct PrivilegeElevation { + level: PrivilegeLevel, + granted_at: SystemTime, + expires_at: SystemTime, + reason: String, +} + +impl FromStr for PrivilegeLevel { + type Err = PrivilegeLevelError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "user" => Ok(PrivilegeLevel::User), + "group" => Ok(PrivilegeLevel::Group), + "admin" | "administrator" => Ok(PrivilegeLevel::Administrator), + "root" => Ok(PrivilegeLevel::Root), + "bell" => Ok(PrivilegeLevel::Bell), + _ => Err(PrivilegeLevelError::InvalidPrivilegeLevel(s.to_string())), + } + } +} + +impl fmt::Display for PrivilegeLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PrivilegeLevel::User => write!(f, "user"), + PrivilegeLevel::Group => write!(f, "group"), + PrivilegeLevel::Administrator => write!(f, "administrator"), + PrivilegeLevel::Root => write!(f, "root"), + PrivilegeLevel::Bell => write!(f, "bell"), + } + } +} + +impl PrivilegeManager { + pub fn new(config: PrivilegeConfig) -> Self { + Self { + config, + active_elevations: HashMap::new(), + } + } + + pub async fn check_permission( + &self, + user: &User, + required_privilege: PrivilegeLevel, + config: &Config, + ) -> Result { + // Direct privilege level check + if user.privilege >= required_privilege { + log_audit_event( + "PRIVILEGE_CHECK", + &user.username, + &format!("Direct privilege granted: {:?}", required_privilege), + ) + .await?; + return Ok(true); + } + + // Check active elevations + if let Some(elevations) = self.active_elevations.get(&user.username) { + for elevation in elevations { + if elevation.level >= required_privilege && SystemTime::now() < elevation.expires_at + { + log_audit_event( + "PRIVILEGE_CHECK", + &user.username, + &format!("Elevation privilege granted: {:?}", required_privilege), + ) + .await?; + return Ok(true); + } + } + } + + // Check group permissions + for group_name in &user.groups { + if let Some(group) = config.groups.iter().find(|g| g.name == *group_name) { + if group.permissions.contains(&required_privilege.to_string()) { + log_audit_event( + "PRIVILEGE_CHECK", + &user.username, + &format!( + "Group privilege granted: {:?} from {}", + required_privilege, group_name + ), + ) + .await?; + return Ok(true); + } + } + } + + log_audit_event( + "PRIVILEGE_CHECK", + &user.username, + &format!("Permission denied for: {:?}", required_privilege), + ) + .await?; + Ok(false) + } + + pub async fn elevate_privilege( + &mut self, + user: &User, + requested_level: PrivilegeLevel, + reason: &str, + mfa_token: Option<&str>, + ) -> Result<()> { + // Check if elevation is allowed at current hour + let current_hour = chrono::Local::now().hour() as u8; + if !self.config.allowed_elevation_hours.contains(¤t_hour) { + return Err(PrivilegeLevelError::InsufficientPrivileges.into()); + } + + // Check MFA requirement + if self.config.require_mfa && mfa_token.is_none() { + return Err(anyhow::anyhow!( + "MFA token required for privilege elevation" + )); + } + + // Check concurrent elevations + let user_elevations = self + .active_elevations + .entry(user.username.clone()) + .or_default(); + if user_elevations.len() >= self.config.max_concurrent_elevations { + return Err(anyhow::anyhow!("Maximum concurrent elevations reached")); + } + + // Create new elevation + let elevation = PrivilegeElevation { + level: requested_level, + granted_at: SystemTime::now(), + expires_at: SystemTime::now() + self.config.elevation_timeout, + reason: reason.to_string(), + }; + + user_elevations.push(elevation); + + log_audit_event( + "PRIVILEGE_ELEVATION", + &user.username, + &format!("Elevated to {:?} for reason: {}", requested_level, reason), + ) + .await?; + + Ok(()) + } + + pub async fn revoke_elevation(&mut self, user: &str, level: PrivilegeLevel) -> Result<()> { + if let Some(elevations) = self.active_elevations.get_mut(user) { + elevations.retain(|e| e.level != level); + log_audit_event( + "PRIVILEGE_REVOCATION", + user, + &format!("Revoked elevation: {:?}", level), + ) + .await?; + } + Ok(()) + } + + pub fn cleanup_expired_elevations(&mut self) { + let now = SystemTime::now(); + for elevations in self.active_elevations.values_mut() { + elevations.retain(|e| e.expires_at > now); + } + } +} + +// OS-specific privilege checks +pub async fn check_os_specific_privileges( + user: &User, + required_privilege: PrivilegeLevel, +) -> Result { + match std::env::consts::OS { + "macos" => check_macos_privileges(user, required_privilege).await, + "linux" => check_linux_privileges(user, required_privilege).await, + "bellandeos" => check_bellande_privileges(user, required_privilege).await, + _ => Ok(false), + } +} + +async fn check_macos_privileges(user: &User, required_privilege: PrivilegeLevel) -> Result { + // Check admin group membership + if required_privilege >= PrivilegeLevel::Administrator { + let output = std::process::Command::new("dseditgroup") + .args(&["-o", "checkmember", "-m", &user.username, "admin"]) + .output()?; + + if !output.status.success() { + return Ok(false); + } + } + + Ok(true) +} + +async fn check_linux_privileges(user: &User, required_privilege: PrivilegeLevel) -> Result { + // Check sudo group membership + if required_privilege >= PrivilegeLevel::Administrator { + let output = std::process::Command::new("groups") + .arg(&user.username) + .output()?; + + let groups = String::from_utf8_lossy(&output.stdout); + if !groups.contains("sudo") && !groups.contains("wheel") { + return Ok(false); + } + } + + Ok(true) +} + +async fn check_bellande_privileges( + user: &User, + required_privilege: PrivilegeLevel, +) -> Result { + // Check BellandeOS specific privileges + let output = std::process::Command::new("bellctl") + .args(&[ + "user", + "check-privilege", + &user.username, + &required_privilege.to_string(), + ]) + .output()?; + + Ok(output.status.success()) +} diff --git a/src/user_privilege/user.rs b/src/user_privilege/user.rs new file mode 100644 index 0000000..2b6afc1 --- /dev/null +++ b/src/user_privilege/user.rs @@ -0,0 +1,675 @@ +// Copyright (C) 2024 Bellande Architecture Mechanism Research Innovation Center, Ronaldson Bellande + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::time::Duration; + +use anyhow::{Context, Result}; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; +use chrono::{DateTime, Utc}; +use log::error; +use serde::{Deserialize, Serialize}; +use std::io::Write; +use thiserror::Error; +use totp_rs::Secret; + +use crate::audit::audit::log_audit_event; +use crate::config::config::Config; +use crate::user_privilege::privilege::PrivilegeLevel; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct User { + pub username: String, + pub password_hash: String, + pub privilege: PrivilegeLevel, + pub totp_secret: String, + pub groups: Vec, + pub created_at: DateTime, + pub last_login: Option>, + pub password_changed_at: DateTime, + pub failed_login_attempts: u32, + pub locked_until: Option>, + pub settings: UserSettings, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserSettings { + pub require_mfa: bool, + pub password_expiry_days: u32, + pub max_failed_attempts: u32, + pub lockout_duration: Duration, + pub allowed_ip_ranges: Vec, +} + +#[derive(Error, Debug)] +pub enum UserError { + #[error("User not found: {0}")] + UserNotFound(String), + #[error("User already exists: {0}")] + UserExists(String), + #[error("Invalid password: {0}")] + InvalidPassword(String), + #[error("Account locked: {0}")] + AccountLocked(String), + #[error("Password expired")] + PasswordExpired, + #[error("Invalid group: {0}")] + InvalidGroup(String), +} + +impl Default for UserSettings { + fn default() -> Self { + Self { + require_mfa: true, + password_expiry_days: 90, + max_failed_attempts: 5, + lockout_duration: Duration::from_secs(1800), // 30 minutes + allowed_ip_ranges: vec!["127.0.0.1/8".to_string()], + } + } +} + +impl User { + pub fn new(username: &str, password: &str, privilege: PrivilegeLevel) -> Result { + let password_hash = hash_password(password)?; + let totp_secret = generate_totp_secret(); + let now = Utc::now(); + + Ok(Self { + username: username.to_string(), + password_hash, + privilege, + totp_secret, + groups: Vec::new(), + created_at: now, + last_login: None, + password_changed_at: now, + failed_login_attempts: 0, + locked_until: None, + settings: UserSettings::default(), + }) + } + + pub fn is_locked(&self) -> bool { + if let Some(locked_until) = self.locked_until { + Utc::now() < locked_until + } else { + false + } + } + + pub fn password_expired(&self) -> bool { + let expiry = chrono::Duration::days(self.settings.password_expiry_days as i64); + Utc::now() - self.password_changed_at > expiry + } + + pub fn record_login_attempt(&mut self, success: bool) { + if success { + self.last_login = Some(Utc::now()); + self.failed_login_attempts = 0; + self.locked_until = None; + } else { + self.failed_login_attempts += 1; + if self.failed_login_attempts >= self.settings.max_failed_attempts { + self.locked_until = Some( + Utc::now() + + chrono::Duration::from_std(self.settings.lockout_duration).unwrap(), + ); + } + } + } +} + +pub async fn add_user( + config: &mut Config, + username: &str, + password: &str, + privilege: PrivilegeLevel, +) -> Result<()> { + // Check if user already exists + if config.users.iter().any(|u| u.username == username) { + return Err(UserError::UserExists(username.to_string()).into()); + } + + // Create new user + let new_user = User::new(username, password, privilege)?; + + // Create OS-specific user account + create_os_user(username, privilege).await?; + + config.users.push(new_user.clone()); + config.save()?; + + log_audit_event( + "USER_ADDED", + "SYSTEM", + &format!("Added user: {} with privilege: {:?}", username, privilege), + ) + .await?; + + println!( + "User added successfully. TOTP secret: {}", + new_user.totp_secret + ); + Ok(()) +} + +pub async fn remove_user(config: &mut Config, username: &str) -> Result<()> { + // Check if user exists + if !config.users.iter().any(|u| u.username == username) { + return Err(UserError::UserNotFound(username.to_string()).into()); + } + + // Remove OS-specific user account + remove_os_user(username).await?; + + config.users.retain(|u| u.username != username); + config.save()?; + + log_audit_event( + "USER_REMOVED", + "SYSTEM", + &format!("Removed user: {}", username), + ) + .await?; + + println!("User removed successfully."); + Ok(()) +} + +pub async fn change_password( + config: &mut Config, + username: &str, + new_password: &str, +) -> Result<()> { + // Validate password complexity first + validate_password_complexity(new_password)?; + + // Find user index + let user_index = config + .users + .iter() + .position(|u| u.username == username) + .ok_or_else(|| UserError::UserNotFound(username.to_string()))?; + + // Update password + let new_hash = hash_password(new_password)?; + + // Update the user's password + { + let user = &mut config.users[user_index]; + user.password_hash = new_hash; + user.password_changed_at = Utc::now(); + } + + // Update OS-specific password + update_os_password(username, new_password).await?; + + config.save()?; + + log_audit_event( + "PASSWORD_CHANGED", + username, + "Password changed successfully", + ) + .await?; + + println!("Password changed successfully."); + Ok(()) +} + +pub async fn change_privilege( + config: &mut Config, + username: &str, + new_privilege: PrivilegeLevel, +) -> Result<()> { + // Find user index first + let user_index = config + .users + .iter() + .position(|u| u.username == username) + .ok_or_else(|| UserError::UserNotFound(username.to_string()))?; + + // Get the values we need before modifying the user + let old_privilege = config.users[user_index].privilege; + let username_clone = config.users[user_index].username.clone(); + + // Update the privilege + config.users[user_index].privilege = new_privilege; + + // Update OS-specific privileges + update_os_privileges(username, new_privilege).await?; + + // Save the configuration + config.save()?; + + // Log the audit event + log_audit_event( + "PRIVILEGE_CHANGED", + &username_clone, + &format!( + "Privilege changed from {:?} to {:?}", + old_privilege, new_privilege + ), + ) + .await?; + + println!("Privilege level changed successfully."); + Ok(()) +} + +pub async fn add_user_to_group( + config: &mut Config, + username: &str, + group_name: &str, +) -> Result<()> { + // Check if group exists first + if !config.groups.iter().any(|g| g.name == group_name) { + return Err(UserError::InvalidGroup(group_name.to_string()).into()); + } + + // Find user index + let user_index = config + .users + .iter() + .position(|u| u.username == username) + .ok_or_else(|| UserError::UserNotFound(username.to_string()))?; + + // Check if user is already in group + let already_in_group = config.users[user_index] + .groups + .contains(&group_name.to_string()); + + if !already_in_group { + // Get username for audit log before modification + let username_clone = config.users[user_index].username.clone(); + + // Add user to group + config.users[user_index].groups.push(group_name.to_string()); + + // Update OS-specific group membership + add_os_user_to_group(username, group_name).await?; + + // Save configuration + config.save()?; + + // Log audit event + log_audit_event( + "USER_ADDED_TO_GROUP", + &username_clone, + &format!("Added to group: {}", group_name), + ) + .await?; + + println!("User added to group successfully."); + } else { + println!("User is already in this group."); + } + + Ok(()) +} + +pub async fn remove_user_from_group( + config: &mut Config, + username: &str, + group_name: &str, +) -> Result<()> { + // Find user index + let user_index = config + .users + .iter() + .position(|u| u.username == username) + .ok_or_else(|| UserError::UserNotFound(username.to_string()))?; + + // Get username for audit log before modification + let username_clone = config.users[user_index].username.clone(); + + // Remove the group + config.users[user_index].groups.retain(|g| g != group_name); + + // Update OS-specific group membership + remove_os_user_from_group(username, group_name).await?; + + // Save configuration + config.save()?; + + // Log audit event + log_audit_event( + "USER_REMOVED_FROM_GROUP", + &username_clone, + &format!("Removed from group: {}", group_name), + ) + .await?; + + println!("User removed from group successfully."); + Ok(()) +} + +// Helper functions +fn hash_password(password: &str) -> Result { + // Generate a random salt + let salt = SaltString::generate(&mut OsRng); + + // Create default Argon2 instance + let argon2 = Argon2::default(); + + // Hash the password + Ok(argon2 + .hash_password(password.as_bytes(), &salt)? + .to_string()) +} + +// And here's a corresponding verify function you'll need +fn verify_password(hash: &str, password: &str) -> Result { + use argon2::password_hash::PasswordHash; + use argon2::PasswordVerifier; + + // Parse the hash string into a PasswordHash instance + let parsed_hash = PasswordHash::new(hash)?; + + // Verify the password against the hash + Ok(Argon2::default() + .verify_password(password.as_bytes(), &parsed_hash) + .is_ok()) +} + +fn generate_totp_secret() -> String { + Secret::generate_secret().to_string() +} + +fn validate_password_complexity(password: &str) -> Result<()> { + if password.len() < 12 { + return Err(UserError::InvalidPassword("Password too short".to_string()).into()); + } + + let has_uppercase = password.chars().any(|c| c.is_uppercase()); + let has_lowercase = password.chars().any(|c| c.is_lowercase()); + let has_digit = password.chars().any(|c| c.is_digit(10)); + let has_special = password.chars().any(|c| !c.is_alphanumeric()); + + if !(has_uppercase && has_lowercase && has_digit && has_special) { + return Err(UserError::InvalidPassword( + "Password does not meet complexity requirements".to_string(), + ) + .into()); + } + + Ok(()) +} + +// OS-specific functions +async fn create_os_user(username: &str, privilege: PrivilegeLevel) -> Result<()> { + match std::env::consts::OS { + "macos" => create_macos_user(username, privilege).await, + "linux" => create_linux_user(username, privilege).await, + "bellandeos" => create_bellande_user(username, privilege).await, + _ => Ok(()), + } +} + +async fn remove_os_user(username: &str) -> Result<()> { + match std::env::consts::OS { + "macos" => remove_macos_user(username).await, + "linux" => remove_linux_user(username).await, + "bellandeos" => remove_bellande_user(username).await, + _ => Ok(()), + } +} + +async fn update_os_password(username: &str, password: &str) -> Result<()> { + match std::env::consts::OS { + "macos" => update_macos_password(username, password).await, + "linux" => update_linux_password(username, password).await, + "bellandeos" => update_bellande_password(username, password).await, + _ => Ok(()), + } +} + +async fn update_os_privileges(username: &str, privilege: PrivilegeLevel) -> Result<()> { + match std::env::consts::OS { + "macos" => update_macos_privileges(username, privilege).await, + "linux" => update_linux_privileges(username, privilege).await, + "bellandeos" => update_bellande_privileges(username, privilege).await, + _ => Ok(()), + } +} + +// OS-specific implementations for macOS, Linux, and BellandeOS... +async fn create_macos_user(username: &str, privilege: PrivilegeLevel) -> Result<()> { + let mut cmd = std::process::Command::new("sysadminctl"); + cmd.args(&["-addUser", username]); + + match privilege { + PrivilegeLevel::Administrator => { + cmd.arg("-admin"); + } + _ => {} + } + + cmd.output().context("Failed to create macOS user")?; + Ok(()) +} + +async fn create_linux_user(username: &str, privilege: PrivilegeLevel) -> Result<()> { + let mut cmd = std::process::Command::new("useradd"); + cmd.arg(username); + + match privilege { + PrivilegeLevel::Administrator => { + cmd.args(&["-G", "sudo"]); + } + _ => {} + } + + cmd.output().context("Failed to create Linux user")?; + Ok(()) +} + +async fn create_bellande_user(username: &str, privilege: PrivilegeLevel) -> Result<()> { + let mut cmd = std::process::Command::new("bellctl"); + cmd.args(&["user", "create", username]); + + match privilege { + PrivilegeLevel::Administrator => { + cmd.arg("--admin"); + } + PrivilegeLevel::Root => { + cmd.arg("--root"); + } + PrivilegeLevel::Bell => { + cmd.arg("--bell"); + } + _ => {} + } + + cmd.output().context("Failed to create BellandeOS user")?; + Ok(()) +} + +async fn remove_macos_user(username: &str) -> Result<()> { + std::process::Command::new("sysadminctl") + .args(&["-deleteUser", username]) + .output() + .context("Failed to remove macOS user")?; + Ok(()) +} + +async fn remove_linux_user(username: &str) -> Result<()> { + std::process::Command::new("userdel") + .args(&["-r", username]) // -r flag removes home directory and mail spool + .output() + .context("Failed to remove Linux user")?; + Ok(()) +} + +async fn remove_bellande_user(username: &str) -> Result<()> { + std::process::Command::new("bellctl") + .args(&["user", "remove", username]) + .output() + .context("Failed to remove BellandeOS user")?; + Ok(()) +} + +async fn update_macos_password(username: &str, password: &str) -> Result<()> { + std::process::Command::new("dscl") + .args(&[".", "-passwd", &format!("/Users/{}", username), password]) + .output() + .context("Failed to update macOS password")?; + Ok(()) +} + +async fn update_linux_password(username: &str, password: &str) -> Result<()> { + let passwd_input = format!("{}:{}", username, password); + let mut child = std::process::Command::new("chpasswd") + .stdin(std::process::Stdio::piped()) + .spawn() + .context("Failed to spawn chpasswd")?; + + if let Some(mut stdin) = child.stdin.take() { + stdin + .write_all(passwd_input.as_bytes()) + .context("Failed to write to chpasswd stdin")?; + } + + child.wait().context("Failed to wait for chpasswd")?; + Ok(()) +} + +async fn update_bellande_password(username: &str, password: &str) -> Result<()> { + let mut child = std::process::Command::new("bellctl") + .args(&["user", "set-password", username]) + .stdin(std::process::Stdio::piped()) + .spawn() + .context("Failed to spawn bellctl")?; + + if let Some(mut stdin) = child.stdin.take() { + stdin + .write_all(password.as_bytes()) + .context("Failed to set BellandeOS password")?; + } + + child.wait().context("Failed to wait for bellctl")?; + Ok(()) +} + +async fn update_macos_privileges(username: &str, privilege: PrivilegeLevel) -> Result<()> { + match privilege { + PrivilegeLevel::Administrator | PrivilegeLevel::Root | PrivilegeLevel::Bell => { + std::process::Command::new("dseditgroup") + .args(&["-o", "edit", "-a", username, "-t", "user", "admin"]) + .output() + .context("Failed to update macOS privileges")?; + } + _ => { + std::process::Command::new("dseditgroup") + .args(&["-o", "edit", "-d", username, "-t", "user", "admin"]) + .output() + .context("Failed to update macOS privileges")?; + } + } + Ok(()) +} + +async fn update_linux_privileges(username: &str, privilege: PrivilegeLevel) -> Result<()> { + match privilege { + PrivilegeLevel::Administrator | PrivilegeLevel::Root => { + std::process::Command::new("usermod") + .args(&["-aG", "sudo", username]) + .output() + .context("Failed to update Linux privileges")?; + } + PrivilegeLevel::Bell => { + std::process::Command::new("usermod") + .args(&["-aG", "sudo,adm,root", username]) + .output() + .context("Failed to update Linux privileges")?; + } + _ => { + std::process::Command::new("deluser") + .args(&[username, "sudo"]) + .output() + .context("Failed to update Linux privileges")?; + } + } + Ok(()) +} + +async fn update_bellande_privileges(username: &str, privilege: PrivilegeLevel) -> Result<()> { + let privilege_str = match privilege { + PrivilegeLevel::User => "user", + PrivilegeLevel::Group => "group", + PrivilegeLevel::Administrator => "admin", + PrivilegeLevel::Root => "root", + PrivilegeLevel::Bell => "bell", + }; + + std::process::Command::new("bellctl") + .args(&["user", "set-privilege", username, privilege_str]) + .output() + .context("Failed to update BellandeOS privileges")?; + Ok(()) +} + +async fn add_os_user_to_group(username: &str, group: &str) -> Result<()> { + match std::env::consts::OS { + "macos" => { + std::process::Command::new("dseditgroup") + .args(&["-o", "edit", "-a", username, "-t", "user", group]) + .output() + .context("Failed to add macOS user to group")?; + } + "linux" => { + std::process::Command::new("usermod") + .args(&["-aG", group, username]) + .output() + .context("Failed to add Linux user to group")?; + } + "bellandeos" => { + std::process::Command::new("bellctl") + .args(&["user", "add-to-group", username, group]) + .output() + .context("Failed to add BellandeOS user to group")?; + } + _ => {} + } + Ok(()) +} + +async fn remove_os_user_from_group(username: &str, group: &str) -> Result<()> { + match std::env::consts::OS { + "macos" => { + std::process::Command::new("dseditgroup") + .args(&["-o", "edit", "-d", username, "-t", "user", group]) + .output() + .context("Failed to remove macOS user from group")?; + } + "linux" => { + std::process::Command::new("deluser") + .args(&[username, group]) + .output() + .context("Failed to remove Linux user from group")?; + } + "bellandeos" => { + std::process::Command::new("bellctl") + .args(&["user", "remove-from-group", username, group]) + .output() + .context("Failed to remove BellandeOS user from group")?; + } + _ => {} + } + Ok(()) +}