├── .gitignore ├── .licenseinfo ├── LICENSE.APACHE ├── LICENSE.LGPL ├── README.md ├── config-intel.py ├── config.py ├── instructions.md ├── plugins ├── __init__.py ├── config │ ├── __init__.py │ ├── global_macros.py │ ├── risky_rules.py │ ├── te_macros.py │ ├── unnecessary_rules.py │ └── user_neverallows.py ├── global_macros.py ├── risky_rules.py ├── te_macros.py ├── unnecessary_rules.py └── user_neverallows.py ├── policysource ├── __init__.py ├── macro.py ├── macro_plugins │ ├── __init__.py │ ├── global_macros.py │ └── te_macros.py ├── mapping.py └── policy.py └── selint /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Vim swap files 60 | *.swp 61 | -------------------------------------------------------------------------------- /.licenseinfo: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """TODO: file docstring""" 18 | -------------------------------------------------------------------------------- /LICENSE.APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE.LGPL: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SELint 2 | SELint is an SEAndroid policy analysis tool. It performs a series of checks on a source policy, suggesting improvements to the policy developer. 3 | 4 | ## Obtaining SELint 5 | SELint may be obtained by cloning this repository. From the command line, do: 6 | 7 | ``` 8 | $ git clone git@github.com:seandroid-analytics/selint.git 9 | ``` 10 | 11 | You may also [download the latest release](https://github.com/seandroid-analytics/selint/releases). 12 | 13 | ## Requirements 14 | SELint requires the `setools` library from [SEToolsv4](https://github.com/TresysTechnology/setools), and the `future` Python module. 15 | 16 | You can install the `future` module with Pip: 17 | ``` 18 | $ pip install --upgrade future 19 | ``` 20 | 21 | ### Using `setools4` from the AOSP tree (recommended) 22 | The `setools` library is distributed as part of the [AOSP tree](https://source.android.com/source/index.html), where it is bundled as a prebuilt. 23 | After [downloading the AOSP tree](https://source.android.com/source/downloading.html) in ``, the `setools` package will be in 24 | ``` 25 | /prebuilts/python/linux-x86/2.7.5/lib/python2.7/site-packages 26 | ``` 27 | To use this package, add this path to your `$PYTHONPATH`; for example, on Ubuntu 14.04 LTS add this to your `.profile`: 28 | ``` 29 | export PYTHONPATH="/prebuilts/python/linux-x86/2.7.5/lib/python2.7/site-packages:$PYTHONPATH" 30 | ``` 31 | 32 | ### Using `setools4` from the official git repository 33 | You may also use the latest version of the `setools` library from the [official git repo](https://github.com/TresysTechnology/setools). 34 | Follow the instructions to download and install the software on the SETools repo. 35 | 36 | ## Running SELint 37 | Before running SELint, you need to configure it to work on the desired SEAndroid policy source files. 38 | Please follow the [configuration instructions](instructions.md). 39 | 40 | After configuring the software, you can run it: 41 | ``` 42 | $ ./selint [OPTIONS] 43 | ``` 44 | 45 | ### Usage 46 | You may obtain the full list of command line options by running: 47 | ``` 48 | $ ./selint -h 49 | usage: selint [-h] [-l] [-w [ ...] | -b 50 | [ ...]] [-D NAME[=VALUE] [NAME[=VALUE] ...]] 51 | [--dumppolicyconf ] [--listpolicyfiles] [-v ] 52 | [-c ] 53 | 54 | SELinux source policy analysis tool. 55 | 56 | optional arguments: 57 | -h, --help show this help message and exit 58 | -l, --list list the available plugins and exit. 59 | -w [ ...], --whitelist [ ...] 60 | specify the plugins to run [Default: run all]. 61 | -b [ ...], --blacklist [ ...] 62 | specify the plugins not to run [Default: run all]. 63 | -D NAME[=VALUE] [NAME[=VALUE] ...], --define NAME[=VALUE] [NAME[=VALUE] ...] 64 | Pass additional definitions to M4 when expanding the 65 | policy. Identical to the -D option in m4. 66 | --dumppolicyconf 67 | write the policy.conf to a user-specified file. If the 68 | file already exists, IT WILL BE OVERWRITTEN. 69 | --listpolicyfiles List all the recognized policy files and exit. 70 | -v , --verbosity 71 | Be verbose. Supported levels are 0-4, with 0 being the 72 | default. 73 | -c , --config 74 | Source the specified config file [Default: config.py]. 75 | 76 | If not differently specified, all available plugins will be run. 77 | ``` 78 | 79 | ### Example usages 80 | Here are some example usages of SELint which you may find useful. 81 | 82 | See all available plugins: 83 | ``` 84 | $ ./selint -l 85 | ``` 86 | 87 | Run `global_macros` and `te_macros` plugins, using a user-provided configuration file: 88 | ``` 89 | $ ./selint -c user-config.py -w global_macros te_macros 90 | ``` 91 | 92 | Run all plugins except `global_macros` and `te_macros`, using a user-provided configuration file: 93 | ``` 94 | $ ./selint -c user-config.py -b global_macros te_macros 95 | ``` 96 | 97 | List all the recognized policy files from a user-provided configuration file and exit: 98 | ``` 99 | $ ./selint -c user-config.py --listpolicyfiles 100 | ``` 101 | 102 | ## Known issues 103 | 104 | ### `xperms` rules 105 | The version of `setools` from your Android tree might not support `xperms` rules. 106 | If you are using `setools` from your Android tree and your policy contains `xperms` rules, `setools` may crash when trying to parse the policy. 107 | 108 | To avoid this issue, you can either [use `setools` from the GitHub repo](#using-setools4-from-the-official-git-repository) or comment out the affected lines in your policy. 109 | 110 | ## Reporting bugs 111 | You can report bugs in the project [issue tracker](https://github.com/seandroid-analytics/selint/issues). 112 | 113 | ## License 114 | Copyright 2015 Aalto University 115 | 116 | The SELint program and its plugins are licensed under the Apache License 2.0 (see [LICENSE.APACHE](LICENSE.APACHE)). 117 | The `policysource` library is licensed under the GNU Lesser General Public License (see [LICENSE.LGPL](LICENSE.LGPL)). 118 | All files distributed with this package indicate the appropriate license to use. 119 | 120 | SELint is an open source project being developed by Filippo Bonazzi and Elena Reshetova from the [Secure Systems research group (SSG)](http://cse.aalto.fi/en/research/secure-systems/) at Aalto University. 121 | The project is part of the [Intel Collaborative Research Institute for Secure Computing (ICRI-SC)](http://www.icri-sc.org). 122 | 123 | You can find more information on the [project site](https://ssg.aalto.fi/projects/selint/). 124 | -------------------------------------------------------------------------------- /config-intel.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """SELint configuration parameters for the Intel tree""" 18 | 19 | # Location of the Android tree 20 | # All paths expressed in other variables will be relative to this 21 | BASE_DIR_GLOBAL = "~/extra/android-ia" 22 | 23 | # Policy source file directories 24 | # This list contains directories to be searched for policy files specified in 25 | # POLICY_FILES below. This is the rough equivalent of the BOARD_SEPOLICY_DIRS 26 | # variable in the BoardConfig.mk makefile, except it also specifies the AOSP 27 | # sepolicy directory. 28 | # This list can contain both strings and tuples (string, bool), where a bool 29 | # value of True means that the directory must be searched recursively. E.g.: 30 | # POLICY_DIRS = ["external/sepolicy", 31 | # ("device/intel/sepolicy", True)] 32 | # Directories will be processed in the order in which they are specified. 33 | POLICY_DIRS = ["external/sepolicy", 34 | ("device/intel/common/sepolicy", True), 35 | "build/target/board/generic/sepolicy"] 36 | 37 | # Policy source files 38 | # This list contains file names to be searched in the POLICY_DIRS specified 39 | # above. This is the rough equivalent of the sepolicy_build_files variable in 40 | # the sepolicy Android.mk makefile. 41 | # This list contains strings. It supports UNIX shell-style patterns ("*", ...) 42 | # Files will be processed in the order in which they are specified. 43 | POLICY_FILES = ["security_classes", 44 | "initial_sids", 45 | "access_vectors", 46 | "global_macros", 47 | "neverallow_macros", 48 | "mls_macros", 49 | "mls", 50 | "policy_capabilities", 51 | "te_macros", 52 | "attributes", 53 | "ioctl_macros", 54 | "*.te", 55 | "roles", 56 | "users", 57 | "initial_sid_contexts", 58 | "fs_use", 59 | "genfs_contexts", 60 | "port_contexts"] 61 | 62 | # Extra definitions for M4 macro expansion 63 | # These will be passed to M4 with the "-D" option 64 | # Additional definitions can also be specified on the command line: they will 65 | # be combined with these 66 | # e.g. 67 | # EXTRA_DEFS = ['mls_num_sens=1', 'mls_num_cats=1024', 68 | # 'target_build_variant=user'] 69 | EXTRA_DEFS = ['mls_num_sens=1', 'mls_num_cats=1024', 70 | 'target_build_variant=user'] 71 | 72 | # Verbosity level 73 | # 0: critical [default] 74 | # 1: error 75 | # 2: warning 76 | # 3: info 77 | # 4: debug 78 | # Can be overridden on the command line 79 | # VERBOSITY = 4 80 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """SELint configuration parameters""" 18 | 19 | # Location of the Android tree 20 | # All paths expressed in other variables will be relative to this 21 | BASE_DIR_GLOBAL = "~/workspace" 22 | 23 | # Policy source file directories 24 | # This list contains directories to be searched for policy files specified in 25 | # POLICY_FILES below. This is the rough equivalent of the BOARD_SEPOLICY_DIRS 26 | # variable in the BoardConfig.mk makefile, except it also specifies the AOSP 27 | # sepolicy directory. 28 | # This list can contain both strings and tuples (string, bool), where a bool 29 | # value of True means that the directory must be searched recursively. E.g.: 30 | # POLICY_DIRS = ["external/sepolicy", 31 | # ("device/intel/sepolicy", True)] 32 | # Directories will be processed in the order in which they are specified. 33 | POLICY_DIRS = ["system/sepolicy", 34 | "build/target/board/generic/sepolicy"] 35 | 36 | # Policy source files 37 | # This list contains file names to be searched in the POLICY_DIRS specified 38 | # above. This is the rough equivalent of the sepolicy_build_files variable in 39 | # the sepolicy Android.mk makefile. 40 | # This list contains strings. It supports UNIX shell-style patterns ("*", ...) 41 | # Files will be processed in the order in which they are specified. 42 | POLICY_FILES = ["security_classes", 43 | "initial_sids", 44 | "access_vectors", 45 | "global_macros", 46 | "neverallow_macros", 47 | "mls_macros", 48 | "mls", 49 | "policy_capabilities", 50 | "te_macros", 51 | "attributes", 52 | "ioctl_macros", 53 | "*.te", 54 | "roles", 55 | "users", 56 | "initial_sid_contexts", 57 | "fs_use", 58 | "genfs_contexts", 59 | "port_contexts"] 60 | 61 | # Extra definitions for M4 macro expansion 62 | # These will be passed to M4 with the "-D" option 63 | # Additional definitions can also be specified on the command line: they will 64 | # be combined with these 65 | # e.g. 66 | # EXTRA_DEFS = ['mls_num_sens=1', 'mls_num_cats=1024', 67 | # 'target_build_variant=user'] 68 | EXTRA_DEFS = ['mls_num_sens=1', 'mls_num_cats=1024', 69 | 'target_build_variant=user'] 70 | 71 | # Verbosity level 72 | # 0: critical [default] 73 | # 1: error 74 | # 2: warning 75 | # 3: info 76 | # 4: debug 77 | # Can be overridden on the command line 78 | # VERBOSITY = 4 79 | -------------------------------------------------------------------------------- /instructions.md: -------------------------------------------------------------------------------- 1 | #Configure SELint 2 | You need to configure SELint to use your Android tree. 3 | Two sample configuration files are shipped with SELint, `config.py` and `config-intel.py`: you can modify them directly or use them as reference. 4 | The available configuration parameters are also described below. 5 | 6 | **BASE_DIR_GLOBAL**: 7 | The location of the Android tree. All other paths expressed in other variables below are relative to this. 8 | This variable is a string; it is a UNIX path, e.g. it can contain `~` and `..`. 9 | 10 | **POLICY_DIRS**: The directories containing policy source files. 11 | This is roughly the equivalent of the `BOARD_SEPOLICY_DIRS` variable in the BoardConfig.mk makefile, except it also specifies the AOSP sepolicy directory. 12 | The paths are relative to `BASE_DIR_GLOBAL`. 13 | This variable is a list; it can contain both strings and tuples `(string, bool)`, where a bool value of `True` means that the directory must be searched recursively. 14 | If the element is a simple string, an implicit value of `False` is assumed, and the directory is not searched recursively. 15 | E.g.: 16 | ```python 17 | POLICY_DIRS = ["external/sepolicy", ("device/intel/sepolicy", True)] 18 | ``` 19 | Directories will be processed in the order in which they are specified. 20 | 21 | **POLICY_FILES**: The names of the policy source files. 22 | This is roughly the equivalent of the `sepolicy_build_files` variable in the `sepolicy` Android.mk makefile. 23 | This variable is a list of strings; it supports UNIX shell-style patterns ("\*", ...). 24 | E.g.: 25 | ```python 26 | POLICY_FILES =["attributes", "*.te"] 27 | ``` 28 | Files will be processed in the order in which they are specified. 29 | 30 | **EXTRA_DEFS**: The extra definitions for M4 macro expansion. 31 | These correspond to the options which are found in the M4 invocation in the `sepolicy` Android.mk makefile. 32 | This variable is a list of strings. 33 | E.g.: 34 | ```python 35 | EXTRA_DEFS = ["mls_num_sens=1", "mls_num_cats=1024", "target_build_variant=user"] 36 | ``` 37 | Additional definitions can also be specified on the command line: they will be combined with the options specified here. 38 | 39 | **VERBOSITY**: The verbosity level. This variable is an integer; available values are: 40 | ``` 41 | 0: critical [default] 42 | 1: error 43 | 2: warning 44 | 3: info 45 | 4: debug 46 | ``` 47 | E.g.: 48 | ```python 49 | VERBOSITY = 4 50 | ``` 51 | This value can be overridden on the command line. 52 | 53 | # Configure SELint plugins 54 | SELint plugins must be configured to adapt to your SEAndroid policy. 55 | Plugins are found in the `plugins` directory, and their configuration files in `plugins/config`. 56 | You can modify the existing configuration files to adapt them to your needs. 57 | The available configuration options for each plugin are presented in the sections below. 58 | 59 | Currently, plugins automatically load the configuration file with the same name as the plugin. 60 | We are planning to allow different plugin configuration files to be individually specified for each plugin in the future. 61 | 62 | # Existing plugins 63 | The following sections describe the existing plugins, how to configure them and how to interpret their output. 64 | ## Global_macros 65 | The `global_macros` plugin suggests new usages of global macros. 66 | Using M4 macros where applicable produces a more compact and readable policy. It also reduces the possibility of error. 67 | #### Configuration 68 | The plugin configuration file can contain the following variables. 69 | 70 | **RULE_IGNORE_PATHS**: Do not suggest M4 macros in rules coming from these paths. 71 | This variable is a list: it contains paths relative to `BASE_DIR_GLOBAL` defined in the global SELint configuration file. 72 | 73 | **SUPPORTED_RULE_TYPES**: Only suggest M4 macros in rules of these types. 74 | This variable is a tuple: it contains rule types as strings. E.g.: 75 | ```python 76 | SUPPORTED_RULE_TYPES = ("allow",) 77 | ``` 78 | If there is only one element in the tuple, insert a trailing comma as in the example to indicate the variable is in fact a tuple. 79 | 80 | **SUGGESTION_THRESHOLD**: Only suggest macros that match above this threshold. 81 | This variable is a number between 0 and 1. E.g.: 82 | ```python 83 | SUGGESTION_THRESHOLD = 0.8 84 | ``` 85 | 86 | **SUGGESTION_MAX_NO**: Suggest at most N partial macro matches. 87 | This variable is an integer. E.g.: 88 | ```python 89 | SUGGESTION_MAX_NO = 3 90 | ``` 91 | 92 | **IGNORED_RULES**: Do not suggest global macros in these rules. 93 | This variable is a list; it contains rule masks up to the class. Matching rules will be ignored. E.g.: 94 | ```python 95 | IGNORED_RULES = ["allow somedomain sometype:someclass"] 96 | ``` 97 | 98 | #### Output 99 | The plugin produces this output: 100 | ``` 101 | The following macros match a rule on these lines: 102 | .../file.te:29 103 | .../file.te:102 104 | Full match: 105 | ra_file_perms 106 | Suggested usage: 107 | allow somedomain sometype:file ra_file_perms; 108 | ``` 109 | This means that the rules found at lines 29 and 102 in `file.te` can be expressed more compactly by using the `ra_file_perms` macro. 110 | If you agree with the suggestion, you can insert the suggested usage in the policy. 111 | If you don't want this suggestion to be visible anymore, you can add the rule mask up to the class to the `IGNORED_RULES` configuration variable. 112 | 113 | ## Te_macros 114 | The `te_macros` plugin suggests new usages of TE macros. 115 | Using M4 macros where applicable produces a more compact and readable policy. It also reduces the possibility of error. 116 | #### Configuration 117 | The plugin configuration file can contain the following variables. 118 | 119 | **RULE_IGNORE_PATHS**: Do not suggest M4 macros in rules coming from these paths. 120 | This variable is a list: it contains paths relative to `BASE_DIR_GLOBAL` defined in the global SELint configuration file. 121 | 122 | **SUPPORTED_RULE_TYPES**: Only suggest M4 macros in rules of these types. 123 | This variable is a tuple: it contains rule types as strings. E.g.: 124 | ```python 125 | SUPPORTED_RULE_TYPES = ("allow", "type_transition") 126 | ``` 127 | If there is only one element in the tuple, insert a trailing comma to indicate the variable is in fact a tuple. 128 | 129 | **MACRO_IGNORE**: Never suggest these M4 macros with any arguments. 130 | This variable is a list of strings. 131 | This variable should contain all TE macros which do not expand into regular `allow` and `type_transition` rules. 132 | For example, conditional macros and macros which only define types/domains should be ignored. 133 | E.g.: 134 | ```python 135 | MACRO_IGNORE = ["recovery_only", "userdebug_or_eng", "print", "eng", "net_domain"] 136 | ``` 137 | 138 | **SUGGESTION_THRESHOLD**: Only suggest macros that match above this threshold. 139 | This variable is a number between 0 and 1. E.g.: 140 | ```python 141 | SUGGESTION_THRESHOLD = 0.8 142 | ``` 143 | 144 | **USAGES_IGNORE**: Do not suggest these specific macro usages with these specific arguments. 145 | This variable is a list of strings. 146 | You can use this variable to blacklist specific macro usages which you do not want SELint to suggest. 147 | E.g.: 148 | ```python 149 | USAGES_IGNORE = ["some_macro(arg1, arg2)"] 150 | ``` 151 | 152 | #### Output 153 | The plugin produces this output: 154 | ``` 155 | These lines could be substituted by macro unix_socket_send(system_server, thermal, init) (100.0%): 156 | .../system_server.te:9: allow system_server thermal_socket:sock_file write; 157 | .../system_server.te:5: allow system_server init:unix_dgram_socket sendto; 158 | Corresponding rules in the macro expansion: 159 | allow system_server thermal_socket:sock_file write; 160 | allow system_server init:unix_dgram_socket sendto; 161 | ``` 162 | This means that the rules found at lines 5 and 9 of the `system_server.te` file could be expressed by using the `unix_socket_send(system_server, thermal, init)` macro. 163 | If you agree with the suggestion, you can insert the macro usage in the policy. 164 | 165 | ## Risky_rules 166 | The `risky_rules` plugin assigns a score to every rule by combining the partial scores of its elements. 167 | The partial scores must be defined for each policy in the plugin configuration file. 168 | 169 | #### Configuration 170 | The plugin configuration file can contain the following variables. 171 | 172 | **SCORING_SYSTEM**: The scoring system. This variable is a string; it can be one of `risk`, `trust_lh`, `trust_hl`, `trust_hh`, `trust_ll`. 173 | Rule elements are assigned a `risk` score, which denotes their level of risk, and a `trust` score, which denotes their level of trust. 174 | The `risk` scoring system makes use of the `risk` partial scores, and the `trust` scoring system makes use of the `trust` partial scores. 175 | 176 | Depending on the selected scoring system, the plugin will output different rules with different associated scores. 177 | The default configuration value is `risk`. You can select a different scoring system depending on what you are looking for in the policy. E.g.: 178 | ```python 179 | SCORING_SYSTEM = "trust_lh" 180 | ``` 181 | 182 | The `risk` scoring system will assign a higher score to rules whose elements have higher combined `risk` scores. 183 | This scoring system takes into account the domain, type, permission and capabilities of an `allow` rule, and the domain and default type of a `type_transition` rule. 184 | 185 | The `trust_lh` scoring system will assign a higher score to rules whose domain has a low `trust` score and whose type has a high `trust` score. 186 | Conversely, the `trust_hl` scoring system will assign a higher score to rules whose domain has a high `trust` score and whose type has a low `trust` score. 187 | The `trust_hh` scoring system will assign a higher score to rules whose domain and type both have a high `trust` score. 188 | The `trust_ll` scoring system will assign a higher score to rules whose domain and type both have a low `trust` score; this is perhaps the least useful of the scoring systems. 189 | 190 | **SUPPORTED_RULE_TYPES**: Only assign a score to rules of these types. 191 | This variable is a tuple: it contains rule types as strings. E.g.: 192 | ```python 193 | SUPPORTED_RULE_TYPES = ("allow", "type_transition") 194 | ``` 195 | If there is only one element in the tuple, insert a trailing comma to indicate the variable is in fact a tuple. 196 | 197 | **RULE_IGNORE_PATHS**: Ignore rules coming from these paths. 198 | This variable is a list: it contains paths relative to `BASE_DIR_GLOBAL` defined in the global SELint configuration file. 199 | 200 | **MAXIMUM_SCORE**: The maximum score a rule can have. This value is used to normalize scores between 0 and 1. 201 | Following from the formula for computing the score in both the `risk` and `trust` scoring systems, this value must be double the highest partial score assigned to a domain/type. 202 | The default configuration uses a `MAXIMUM_SCORE` value of 60, and a highest partial score of 30; these values can be changed if necessary. 203 | 204 | **TYPES**: The classification of types. This variable is a dictionary {string: list}: it classifies types into "*bins*" identified by a semantic label. 205 | Bins group types with similar roles in the policy, to simplify the scoring. 206 | Bins are assigned both a `risk` and a `trust` score: these are filed in the `SCORE_RISK` and `SCORE_TRUST` dictionaries under the bin's label. 207 | 208 | The default configuration defines 5 bins for classifying types: `security_sensitive`, `user_app`, `core_domains`, `default_types` and `sensitive`. 209 | The `security_sensitive` bin contains types directly related to system security functionality (e.g. `tee`, `keystore`, ...). 210 | Since misuse of these types could result in severe consequences, this bin is assigned the highest `risk` score; since these are very important system types, this bin is assigned the highest `trust` score. 211 | The `user_app` bin contains types assigned to user-installed apps (e.g. `untrusted_app`). 212 | Since user applications can come from a variety of sources and can access most user data, this bin is assigned the highest `risk` score; since these applications are completely untrusted by the system, this bin is assigned the lowest `trust` score. 213 | The `core_domains` bin contains core system domains (e.g. `adbd`, `installd`, `kernel`, ...). 214 | Since these are important but not directly security-critical domains, this bin is assigned a medium `risk` score and a medium-low `trust` score. 215 | The `default_types` bin contains default types, used to label objects which are not assigned any specific label in the policy. 216 | Default types should almost never be used outside of the AOSP policy, therefore this bin is assigned the highest `risk` score; since these types are assigned automatically, they are assigned a low `trust` score. 217 | The `sensitive` bin contains sensitive types which do not qualify as directly security-critical, but are still somewhat more important than core system domains. 218 | This bin is assigned a medium-high `risk` score and a medium-low `trust` score. 219 | 220 | You may add extra ones to suit your needs: simply add an entry to the `TYPES`, `SCORE_RISK` and `SCORE_TRUST` dictionaries. E.g.: 221 | ```python 222 | # User-defined bin 1 223 | SCORE_TRUST["user_bin_1"] = 10 224 | SCORE_RISK["user_bin_1"] = 30 225 | TYPES["user_bin_1"] = ["user_type_1", "user_type_2", "user_type_3"] 226 | ``` 227 | 228 | **SCORE_TRUST**: The `trust` scores. This variable is a dictionary {string: integer}, where the string is the label of a `TYPES` bin and the integer is the associated `trust` score for that bin. 229 | 230 | **SCORE_RISK**: The `risk` scores. This variable is a dictionary {string: integer}, where the string is the label of a `TYPES` bin and the integer is the associated `risk` score for that bin. 231 | 232 | **PERMS**: The classification of permissions. This variable is a dictionary {string: set}: it classifies permissions into sets identified by a semantic label. These sets group permissions with similar levels of risk, to simplify the scoring. These sets are assigned a `risk` coefficient, filed in the `SCORE` dictionary under the set's label. Permissions are not assigned a `trust` score, as that scoring system only deals with types. 233 | 234 | The default configuration defines 3 sets for classifying permissions. You may add extra ones to suit your needs: simply add an entry to the `PERMS` and `SCORE` dictionaries. E.g.: 235 | ```python 236 | # User-defined set 1 237 | SCORE["perms_user_1"] = 0.8 238 | PERMS["perms_user_1"] = set(["perm1", "perm2"]) 239 | ``` 240 | A permission must be present in only one set. 241 | 242 | **SCORE**: The `risk` scores of non-type rule elements (permissions, capabilities). This variable is a dictionary {string: float}, where the string is the label of a `PERMS` or `CAPABILITIES` set, and the number is the associated `risk` coefficient or score for that set. 243 | 244 | Permissions are assigned a `risk` coefficient, which is multiplied by the sum of the domain and type `risk` scores to compute the overall rule score. 245 | 246 | Capabilities are assigned a `risk` score of their own, and they are treated as types when found in a rule: their score is added to the domain score to compute the overall rule score. 247 | Currently we are targeting capabilities in an aggregate way (we assign a higher score to rules which grant any capabilities): in the future we may target capabilities individually, by dividing them into sets according to their level of risk. 248 | 249 | **CAPABILITIES**: The classes associated with capabilities. This variable should be changed only if further classes were to be associated with capabilities in the future (e.g. a `capability3` class). In the future we may change how we handle capabilities. 250 | 251 | **SCORE_THRESHOLD**: Don't report rules which score below this threshold. 252 | This variable is a number between 0 and 1. E.g.: 253 | ```python 254 | SCORE_THRESHOLD = 0.8 255 | ``` 256 | 257 | **REVERSE_SORT**: Print the results in reverse order. This variable is a Boolean value. 258 | 259 | **IGNORED_RULES**: Never report these rules. This variable is a list of strings. The rules must match exactly as strings. 260 | 261 | #### Output 262 | The plugin produces this output: 263 | ``` 264 | 1.00: .../file.te:28: allow some_domain some_type:some_class { perm1 perm2 perm3 }; 265 | ``` 266 | This means that, by combining the partial scores of its elements according to the selected scoring system, the rule has been assigned score 1 (maximum). 267 | 268 | ## unnecessary_rules 269 | The `unnecessary_rules` plugin searches the policy for rules which are ineffective or unnecessary. 270 | It also looks for debug rules mistakenly visible in the user policy. 271 | 272 | #### Configuration 273 | The plugin configuration file can contain the following variables. 274 | 275 | **RULE_IGNORE_PATHS**: Do not make suggestions on rules coming from files in these paths. 276 | This variable is a list: it contains paths relative to `BASE_DIR_GLOBAL` defined in the global SELint configuration file. 277 | 278 | **SUPPORTED_RULE_TYPES**: Only make suggestions on the following rule types. 279 | This variable is a tuple: it contains rule types as strings. E.g.: 280 | ```python 281 | SUPPORTED_RULE_TYPES = ("allow",) 282 | ``` 283 | If there is only one element in the tuple, insert a trailing comma as in the example to indicate the variable is in fact a tuple. 284 | 285 | **RULES_TUPLES**: Tuples of rules which must be always found together. 286 | This variable is a list of tuples: the tuples contain rules as strings. 287 | The rules can contain numbered placeholder arguments in the format `@@ARGN@@`. E.g.: 288 | ```python 289 | RULES_TUPLES = [("type_transition @@ARG0@@ @@ARG1@@:process @@ARG2@@;", 290 | "allow @@ARG0@@ @@ARG1@@:file execute;", 291 | "allow @@ARG2@@ @@ARG1@@:file entrypoint;", 292 | "allow @@ARG0@@ @@ARG2@@:process transition;")] 293 | ``` 294 | If a rule is found matching the first rule in the tuple, the arguments are extracted and substituted in the remaining rules; each of these rules must then be found in the policy. 295 | 296 | The first rule in the tuple must contain all the placeholder arguments used in the tuple. 297 | 298 | **DEBUG_TYPES**: The debug types. 299 | This variable is a list of strings. It contains the debug types used in the policy. 300 | If one of these types is found in a rule in the policy, the rule is reported. 301 | 302 | **REQUIRED_PERMS**: The minimum permissions which must be granted for a specific class. 303 | This variable is a dictionary {class: tuple}. The class is a policy class represented as a string (`file`, `dir`, ...). 304 | The tuple contains two sets and a dictionary. 305 | The two sets contain permissions represented as strings (`write`, `open`, ...); the dictionary is a dictionary {class: set}, where the class is again a string and the set is again a set of permissions as strings. 306 | E.g.: 307 | ```python 308 | REQUIRED_PERMS = {"file": (set(["write", "read", "append", "ioctl"]), 309 | set(["open"]), 310 | {"fd": set(["use"])}) 311 | } 312 | ``` 313 | If a rule is found granting the `file` class any permission in the first set (`write`, `read`, `append`, `ioctl`), then it must either grant all the permissions in the second set (`open`), or other rules in the policy must, for every class in the second dictionary (`fd`), grant the class all the permissions found in the associated set (`use`). 314 | If neither of these two conditions is met, then the rule is reported. 315 | 316 | **IGNORED_RULES**: Never report these rules. 317 | This variable is a list of strings; it contains full policy rules. 318 | E.g.: 319 | ```python 320 | IGNORED_RULES = ["allow domain type:class permission;"] 321 | ``` 322 | Rules in this variable will be ignored when looking for the first rule in a `RULES_TUPLE` tuple, a rule containing a `DEBUG_TYPE` type, and a rule not satisfying the conditions expressed in `REQUIRED_PERMS`. 323 | Rules in this variable will still be detected when looking for matching additional rules in a `RULES_TUPLE` tuple. 324 | 325 | 326 | ## user_neverallows 327 | The `user_neverallows` plugin verifies that the `neverallow` rules provided in its configuration file are respected by the policy. 328 | 329 | #### Configuration 330 | The plugin configuration file can contain the following variables. 331 | 332 | **SUPPORTED_RULE_TYPES**: Only analyse rules of these types. 333 | This variable is a tuple: it contains rule types as strings. E.g.: 334 | ```python 335 | SUPPORTED_RULE_TYPES = ("allow",) 336 | ``` 337 | If there is only one element in the tuple, insert a trailing comma to indicate the variable is in fact a tuple. 338 | 339 | This plugin only supports `allow` rules, since it enforces `neverallow` rules by checking that no corresponding `allow` rules exist. 340 | Therefore, this variable must not be changed. 341 | 342 | **NEVERALLOWS**: The `neverallow` rules to enforce. 343 | This variable is a list: it contains full `neverallow` rules a strings. 344 | The `neverallow` rules can contain `global_macros`. E.g.: 345 | ```python 346 | NEVERALLOWS = ["neverallow some_domain some_type:some_class { w_file_perms create };"] 347 | ``` 348 | 349 | #### Output 350 | With the `NEVERALLOWS` variable configured as in the example above, the plugin produces this output: 351 | ``` 352 | Rule grants neverallowed permissions: "append lock open" 353 | allow some_domain some_type:some_class { append getattr ioctl lock open read }; 354 | .../file.te:10: allow some_domain some_type:some_class { getattr ioctl lock open read }; 355 | .../file.te:13: allow some_domain some_type:some_class append; 356 | ``` 357 | This means that the rules found at lines 10 and 13 of `file.te`, combine to grant a number of permissions: of these, `append`, `lock` and `open` are forbidden by the `neverallow` rule. 358 | 359 | # Develop new SELint plugins 360 | You can develop new plugins to implement additional analysis functionality. 361 | SELint plugins are regular Python files. They must declare a `main(policy, config)` function and a `REQUIRED_RULES` tuple. 362 | 363 | The `main(policy, config)` function is passed the loaded policy as a `SourcePolicy` object, and the selected global SELint configuration file as the `config` module. 364 | 365 | The `REQUIRED_RULES` tuple must contain the rule types that the plugin intends to work on. 366 | 367 | You can put a configuration file for your plugin in the `plugins/config` directory: it must have the same name as the plugin. 368 | You can then import the configuration file as a module in your plugin: 369 | ```python 370 | import plugins.config. as plugin_conf 371 | ``` 372 | 373 | The plugin configuration file will be available as the `plugin_conf` module. 374 | -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """TODO: file docstring""" 18 | 19 | # Necessary for Python 2/3 compatibility 20 | from __future__ import absolute_import 21 | 22 | import os 23 | import os.path 24 | import sys 25 | import keyword 26 | import inspect 27 | import logging 28 | 29 | # Setup logging 30 | LOG = logging.getLogger(__name__) 31 | 32 | # Recognize plugins 33 | available_plugins = [] 34 | __plugins = {} 35 | for plugin_file in os.listdir(os.path.dirname(__file__)): 36 | if plugin_file.endswith(u".py"): 37 | plugin = os.path.splitext(plugin_file)[0] 38 | if not plugin.startswith(u"_") and not keyword.iskeyword(plugin): 39 | try: 40 | __import__(__name__ + u"." + plugin) 41 | except: 42 | e = sys.exc_info() 43 | print(e) 44 | LOG.debug(u"Found invalid plugin \"%s\"", plugin) 45 | else: 46 | if inspect.isfunction(locals()[plugin].main) and\ 47 | hasattr(locals()[plugin], "REQUIRED_RULES") and\ 48 | isinstance(locals()[plugin].REQUIRED_RULES, tuple): 49 | available_plugins.append(plugin) 50 | __plugins[plugin] = locals()[plugin] 51 | LOG.debug(u"Found valid plugin \"%s\"", plugin) 52 | else: 53 | LOG.debug(u"Found invalid plugin \"%s\"", plugin) 54 | available_plugins.sort() 55 | 56 | 57 | def get_plugin(name): 58 | """Get a plugin by name.""" 59 | if name in __plugins: 60 | return __plugins[name] 61 | else: 62 | return None 63 | -------------------------------------------------------------------------------- /plugins/config/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """The plugin configuration module.""" 18 | -------------------------------------------------------------------------------- /plugins/config/global_macros.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Configuration file for the global_macros plugin.""" 18 | 19 | # Do not make suggestions on rules coming from files in these paths 20 | # 21 | # e.g. to ignore AOSP: 22 | # RULE_IGNORE_PATHS = ["external/sepolicy"] 23 | RULE_IGNORE_PATHS = ["external/sepolicy", 24 | "build/target/board/generic/sepolicy"] 25 | 26 | # Only make suggestions for the following rule types 27 | # SUPPORTED_RULE_TYPES = ("allow", "auditallow", "dontaudit", "neverallow") 28 | # This must be a tuple: if there is only one element, insert a trailing comma 29 | SUPPORTED_RULE_TYPES = ("allow",) 30 | 31 | # Parameters for partial match macro suggestions 32 | # Only suggest macros that match above this threshold [0-1] 33 | SUGGESTION_THRESHOLD = 0.8 34 | # Make up to this number of suggestions 35 | SUGGESTION_MAX_NO = 3 36 | 37 | # Do not suggest global macros in these rules. 38 | # Specify rule masks up to the class, e.g.: 39 | # IGNORED_RULES = ["allow a b:c", "allow somedomain sometype:someclass"] 40 | # Matching rules will be ignored. 41 | # WARNING: Be careful what you put in here. 42 | IGNORED_RULES = [] 43 | -------------------------------------------------------------------------------- /plugins/config/risky_rules.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Configuration file for the risky_rules plugin. 18 | 19 | Assign a score to rules using different scoring systems, "risk", and "trust" in 20 | all its High/Low combinations. The types, permissions and their weights must be 21 | defined in the plugin configuration file. 22 | """ 23 | 24 | # Which scoring system do you want? 25 | # Available: risk, trust_hl, trust_lh, trust_hh, trust_ll 26 | # risk: 27 | # score rules by the potential risk associated with the components, using 28 | # the SCORE_RISK values. Scored components are domains, types, 29 | # permissions, capabilities for allow rules, domains and default types 30 | # for type_transition rules 31 | # trust_XX: 32 | # score rules by the trust level associated with their domain and type, 33 | # using the SCORE_TRUST values. 34 | # The scoring can highlight rules which go from a "low" domain to a 35 | # "high" type (lh), from "high" to "low" (hl), from high to high (hh) and 36 | # from "low" to "low" (ll). 37 | SCORING_SYSTEM = "risk" 38 | 39 | # Only score the following type of rules 40 | # This must be a tuple: if there is only one element, insert a trailing comma 41 | SUPPORTED_RULE_TYPES = ("allow", "type_transition") 42 | 43 | # Dictionary containing the classification of types 44 | TYPES = {} 45 | # Dictionary containing the classification of permissions 46 | PERMS = {} 47 | # Dictionary containing the generic score for non-type entries 48 | SCORE = {} 49 | # Dictionary containing the trust score for each entry in types 50 | SCORE_TRUST = {} 51 | # Dictionary containing the risk score for each entry in types 52 | SCORE_RISK = {} 53 | # Maximum score a rule can obtain (2x highest scoring type) 54 | MAXIMUM_SCORE = 60 55 | 56 | # Security sensitive types 57 | # These are the types that directly impact system security, and as such must be 58 | # closely guarded. 59 | SCORE_TRUST["security_sensitive"] = 30 60 | SCORE_RISK["security_sensitive"] = 30 61 | TYPES["security_sensitive"] = ["proc_security", "security_file", 62 | "security_prop", "securityfile_service", 63 | "securitymanager_service", "tee", 64 | "tee_data_file", "tee_device", "tee_exec", 65 | "tee_tmpfs", "keystore", "keystore_data_file", 66 | "keystore_exec", "keystore_service", 67 | "kmem_device", "keystore_tmpfs", 68 | "keychain_data_file", "vpn_data_file"] 69 | # Types assigned to user-installed apps 70 | # These types must not be granted eccessive permissions. 71 | # On most devices, there is only one of these types, "untrusted_app". 72 | SCORE_TRUST["user_app"] = 0 73 | SCORE_RISK["user_app"] = 30 74 | TYPES["user_app"] = ["untrusted_app"] 75 | # Core system domains 76 | # These types cover core system services launched by the init system. 77 | # While not directly involved with security, these services are very important. 78 | SCORE_TRUST["core_domains"] = 20 79 | SCORE_RISK["core_domains"] = 15 80 | TYPES["core_domains"] = ["adbd", "adbd_socket", "init", "init_shell", 81 | "init_tmpfs", "installd", "installd_exec", 82 | "installd_socket", "installd_tmpfs", "radio", 83 | "radio_data_file", "radio_device", "radio_prop", 84 | "radio_service", "radio_tmpfs", "vold", "vold_exec", 85 | "vold_prop", "vold_socket", "vold_tmpfs", "drmserver", 86 | "drmserver_exec", "drmserver_service", 87 | "drmserver_socket", "drmserver_tmpfs", 88 | "drm_data_file", "kernel", "netd", "netd_exec", 89 | "netd_socket", "netd_tmpfs", "rild", 90 | "rild_debug_socket", "rild_exec", "rild_socket", 91 | "rild_tmpfs", "system_server", 92 | "system_server_service", "system_server_tmpfs", 93 | "ueventd", "ueventd_tmpfs", "zygote", "zygote_exec", 94 | "zygote_socket", "zygote_tmpfs"] 95 | # Default types 96 | # These types are used to label objects in absence of any specific label 97 | # applied to them in the policy. 98 | # These should not be used most of the time. 99 | SCORE_TRUST["default_types"] = 5 100 | SCORE_RISK["default_types"] = 30 101 | TYPES["default_types"] = ["device", "unlabeled", "default_android_service", 102 | "socket_device", "default_property", "system_file", 103 | "system_data_file", "default_prop"] 104 | # Sensitive types 105 | # These types are non directly security-related, but protect key parts of the 106 | # system such as the graphics device memory. 107 | SCORE_TRUST["sensitive"] = 10 108 | SCORE_RISK["sensitive"] = 20 109 | TYPES["sensitive"] = ["graphics_device", "ram_device"] 110 | 111 | # TODO: add instructions on how to add a new bucket 112 | 113 | # TODO: add socket permissions, other permissions from access_vectors 114 | # High-risk permissions 115 | SCORE["perms_high"] = 1 116 | PERMS["perms_high"] = set(["ioctl", "write", "setattr", "relabelfrom", 117 | "mounton", "relabelto", "append", "rename", 118 | "execute", "entrypoint", "execute_no_trans", 119 | "execmod", "transition", "bind", "name_bind", 120 | "connect", "sendto", "setopt", "unix_write", 121 | "mount", "remount", "quotamod"]) 122 | # Medium-risk permissions 123 | SCORE["perms_med"] = 0.9 124 | PERMS["perms_med"] = set(["read", "create", "swapon", "quotaon", "unlink", 125 | "link", "use", "fork", "listen", "accept", 126 | "associate", "unix_read", "unmount"]) 127 | # Low-risk permissions 128 | SCORE["perms_low"] = 0.5 129 | PERMS["perms_low"] = set(["search", "getattr", "lock", "audit_access", "rmdir", 130 | "open", "getopt", "shutdown", "destroy", "recvfrom", 131 | "recv_msg", "send_msg", "drop", "quotaget"]) 132 | 133 | 134 | # Capabilities 135 | SCORE["capability"] = 30 136 | SCORE["capability2"] = 30 137 | CAPABILITIES = ["capability", "capability2"] 138 | 139 | # Ignore rules coming from files in these paths 140 | # e.g. to ignore AOSP: 141 | # RULE_IGNORE_PATHS = ["external/sepolicy"] 142 | RULE_IGNORE_PATHS = ["external/sepolicy", 143 | "build/target/board/generic/sepolicy"] 144 | # RULE_IGNORE_PATHS = [] 145 | 146 | # Don't report rules that score below this threshold 147 | SCORE_THRESHOLD = 0.8 148 | 149 | # Print the results in reverse order (highest first) 150 | REVERSE_SORT = False 151 | 152 | # Do not report these rules 153 | # The rules must match exactly as strings 154 | # e.g. 155 | # IGNORED_RULES = ["allow domain type:class permission;"] 156 | IGNORED_RULES = [] 157 | -------------------------------------------------------------------------------- /plugins/config/te_macros.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Configuration file for the te_macros plugin.""" 18 | 19 | # Do not make suggestions on rules coming from files in these paths 20 | # 21 | # e.g. to ignore AOSP: 22 | # RULE_IGNORE_PATHS = ["external/sepolicy"] 23 | RULE_IGNORE_PATHS = ["external/sepolicy", 24 | "build/target/board/generic/sepolicy"] 25 | 26 | # Only make suggestions for the following rule types 27 | # SUPPORTED_RULE_TYPES = ("allow", "type_transition", "neverallow") 28 | # This must be a tuple: if there is only one element, insert a trailing comma 29 | SUPPORTED_RULE_TYPES = ("allow", "type_transition") 30 | 31 | # Do not try to reconstruct these macros 32 | MACRO_IGNORE = ["recovery_only", "non_system_app_set", "userdebug_or_eng", 33 | "print", "permissive_or_unconfined", "userfastboot_only", 34 | "notuserfastboot", "eng", "binder_service", "net_domain", 35 | "unconfined_domain", "bluetooth_domain"] 36 | 37 | # Only suggest macros that match above this threshold [0-1] 38 | SUGGESTION_THRESHOLD = 0.8 39 | 40 | # Do not suggest these usages 41 | # WARNING: Be careful what you put in here. 42 | USAGES_IGNORE = [] 43 | -------------------------------------------------------------------------------- /plugins/config/unnecessary_rules.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Configuration file for the unnecessary_rules plugin.""" 18 | 19 | # Do not make suggestions on rules coming from files in these paths 20 | # 21 | # e.g. to ignore AOSP: 22 | # RULE_IGNORE_PATHS = ["external/sepolicy"] 23 | RULE_IGNORE_PATHS = ["external/sepolicy", 24 | "build/target/board/generic/sepolicy"] 25 | 26 | # Only make suggestions for the following rule types 27 | # SUPPORTED_RULE_TYPES = ("allow", "type_transition", "neverallow") 28 | # This must be a tuple: if there is only one element, insert a trailing comma 29 | SUPPORTED_RULE_TYPES = ("allow", "type_transition") 30 | 31 | # Functionality 1 32 | # Tuples of rules that must always be found together. If the first rule in the 33 | # tuple is found in the policy, report any other rule in the tuple which is 34 | # not in the policy. 35 | # 36 | # The plugin supports searching with placeholder arguments, e.g. you can 37 | # specify such a tuple: 38 | # ("type_transition @@ARG0@@ @@ARG1@@:process @@ARG2@@;", 39 | # "allow @@ARG0@@ @@ARG1@@:file execute;", 40 | # "allow @@ARG2@@ @@ARG1@@:file entrypoint;", 41 | # "allow @@ARG0@@ @@ARG2@@:process transition;") 42 | # which will match the following set of rules: 43 | # 44 | # type_transition initrc_t acct_exec_t:process acct_t; 45 | # allow initrc_t acct_exec_t:file execute; 46 | # allow acct_t acct_exec_t:file entrypoint; 47 | # allow initrc_t acct_t:process transition; 48 | # 49 | # N.B. the first rule in the tuple MUST contain all the placeholder arguments 50 | # used in later rules. 51 | RULES_TUPLES = [("type_transition @@ARG0@@ @@ARG1@@:process @@ARG2@@;", 52 | "allow @@ARG0@@ @@ARG1@@:file execute;", 53 | "allow @@ARG2@@ @@ARG1@@:file entrypoint;", 54 | "allow @@ARG0@@ @@ARG2@@:process transition;"), 55 | ("type_transition @@ARG0@@ @@ARG1@@:file @@ARG2@@;", 56 | "allow @@ARG0@@ @@ARG1@@:dir { search add_name write };", 57 | "allow @@ARG0@@ @@ARG2@@:file { create write };")] 58 | 59 | # Functionality 2 60 | # List of debug types, which must not be in the user policy 61 | # The plugin will report any rule which contains any of these types. 62 | DEBUG_TYPES = [""] 63 | 64 | # Functionality 3 65 | # Dictionary of class-specific permissions required for a rule to make sense 66 | # The plugin will report any rule which grants any permission from the first 67 | # set without granting at least the required perms in the second set, or an 68 | # additional set of permissions on another class. 69 | # The dictionary key is the name of the class as a string; the dictionary value 70 | # is a tuple of two sets of strings and a dictionary containing a set. 71 | # E.g.: 72 | # REQUIRED_PERMS = {"file": (set(["write", " read", "append", "ioctl"]), # SET1 73 | # set(["open"]), # SET2 74 | # {"fd": set(["use"])}) # ADD 75 | # } 76 | # means: for class "file" 77 | # IF any permission from SET1 is granted AND NOT ( 78 | # (all the permissions in SET2 are granted) 79 | # OR 80 | # (for each class in the ADD dictionary, all permissions in the relative 81 | # set are granted) 82 | # ): 83 | # REPORT RULE 84 | REQUIRED_PERMS = {"file": (set(["write", "read", "append", "ioctl"]), 85 | set(["open"]), 86 | {"fd": set(["use"])}) 87 | } 88 | 89 | # Do not report these rules 90 | # Rules in this configuration parameter will be ignored when looking for the 91 | # first rule in a tuple (functionality 1), when looking for rules containing 92 | # debug types (functionality 2) and when looking for rules not granting desired 93 | # permissions (functionality 3). 94 | # They WILL be detected, if applicable, when looking for matching additional 95 | # rules in tuples (functionality 1). 96 | # The rules must match exactly as strings 97 | # e.g. 98 | # IGNORED_RULES = ["allow domain type:class permission;"] 99 | IGNORED_RULES = [] 100 | -------------------------------------------------------------------------------- /plugins/config/user_neverallows.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Configuration file for the user_neverallow plugin.""" 18 | 19 | # Only analyse rules of these types 20 | # This must be a tuple: if there is only one element, insert a trailing comma 21 | SUPPORTED_RULE_TYPES = ("allow",) 22 | 23 | # Report rules that do not obey these neverallow rules 24 | # The neverallow rules can contain global_macros 25 | # e.g. 26 | # NEVERALLOWS = ["neverallow domain type:class permission;"] 27 | NEVERALLOWS = ["neverallow adbd shell:process noatsecure;"] 28 | -------------------------------------------------------------------------------- /plugins/global_macros.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2015 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | u"""Plugin to analyse usage of global macros and suggest new ones.""" 18 | 19 | # Necessary for Python 2/3 compatibility 20 | from __future__ import absolute_import 21 | from __future__ import division 22 | from future.utils import iteritems 23 | from builtins import range 24 | 25 | import itertools 26 | import os 27 | import os.path 28 | import logging 29 | import policysource 30 | import policysource.policy 31 | import policysource.mapping 32 | import plugins.config.global_macros as plugin_conf 33 | 34 | # Required by selint 35 | REQUIRED_RULES = plugin_conf.SUPPORTED_RULE_TYPES 36 | 37 | 38 | class GlobalMacroSuggestion(object): 39 | u"""A global_macro usage suggestion for a specific combination of lines.""" 40 | 41 | def __init__(self, macro_name, perm_set, rules, score, rutc, permset): 42 | self.name = macro_name 43 | self.macro_perms = perm_set 44 | self.rules = rules 45 | self.score = score 46 | self.filelines = frozenset((r.fileline for r in rules)) 47 | self.applies_to = rutc 48 | self.original_permset = permset 49 | 50 | def __repr__(self): 51 | return self.name + u":\n" + u"\n".join(self.filelines) 52 | 53 | def __eq__(self, other): 54 | return self.name == other.name and self.filelines == other.filelines 55 | 56 | def __ne__(self, other): 57 | return not self == other 58 | 59 | def __lt__(self, other): 60 | return self.score < other.score 61 | 62 | def __le__(self, other): 63 | return self.score <= other.score 64 | 65 | def __gt__(self, other): 66 | return self.score > other.score 67 | 68 | def __ge__(self, other): 69 | return self.score >= other.score 70 | 71 | 72 | def main(policy, config): 73 | """Suggest new usages of global_macros.""" 74 | # Check that we have been fed a valid policy 75 | if not isinstance(policy, policysource.policy.SourcePolicy): 76 | raise ValueError(u"Invalid policy") 77 | # Setup logging 78 | # log = logging.getLogger(__name__) 79 | 80 | # Compute the absolute ignore paths 81 | FULL_IGNORE_PATHS = tuple(os.path.join(config.FULL_BASE_DIR, p) 82 | for p in plugin_conf.RULE_IGNORE_PATHS) 83 | 84 | # Suggestions: {frozenset(filelines): [suggestions]} 85 | suggestions = {} 86 | 87 | # Prepare macro definition dictionaries 88 | macroset_dict = {} 89 | macroset_labels = {} 90 | for m in policy.macro_defs: 91 | if policy.macro_defs[m].file_defined.endswith(u"global_macros"): 92 | exp = policy.macro_defs[m].expand() 93 | args = frozenset(x for x in exp.split() if x not in u"{}") 94 | macroset_dict[m] = args 95 | macroset_labels[args] = m 96 | # Prepare macro usages dictionaries 97 | macrousages_dict = {} 98 | for m in policy.macro_usages: 99 | fileline = m.file_used + u":" + str(m.line_used) 100 | if fileline in macrousages_dict: 101 | macrousages_dict[fileline].append(m) 102 | else: 103 | macrousages_dict[fileline] = [m] 104 | 105 | # Initialize a set fitter 106 | sf = SetFitter(macroset_dict) 107 | # Cache results for set fitting 108 | cached_fits = {} 109 | for rutc in policy.mapping.rules: 110 | # Only match supported rules 111 | if not rutc.startswith(plugin_conf.SUPPORTED_RULE_TYPES): 112 | continue 113 | # Skip rutcs purposefully ignored by the user 114 | if rutc in plugin_conf.IGNORED_RULES: 115 | continue 116 | # Get the different rules applying to the same Rule Up To the Class 117 | rules = policy.mapping.rules[rutc] 118 | permset = set() 119 | # Merge the various permission sets deriving from these rules 120 | filtered_rules = [] 121 | for r in rules: 122 | # Discard rules coming from ignored paths 123 | if not r.fileline.startswith(FULL_IGNORE_PATHS): 124 | # Save the rule 125 | filtered_rules.append(r) 126 | # Get the permissions from the rule 127 | perms = r.rule[len(rutc):].strip(u" {};").split() 128 | # Update the permission set 129 | permset.update(perms) 130 | # If there are no rules left or the permset is empty, process the next 131 | # set of rules 132 | if not filtered_rules or not permset: 133 | continue 134 | # Extract the class from the rutc 135 | tclass = rutc.split(":")[1] 136 | # Get up to one full match (combination of one or more macros which 137 | # combined fit the permset exactly), and a list of macros that fit the 138 | # permset partially 139 | # Cache set fitting results for speed 140 | # Convert the permset to a frozen set to use it as a dictionary key 141 | # together with the class. 142 | permset_frozen = frozenset(permset) 143 | if (tclass, permset_frozen) in cached_fits: 144 | # If the result is cached, use it 145 | (winner, part) = cached_fits[(tclass, permset_frozen)] 146 | else: 147 | # Fit the permset 148 | (winner, part) = sf.fit(permset, tclass) 149 | # This computation was relatively expensive: cache it 150 | cached_fits[(tclass, permset_frozen)] = (winner, part) 151 | # TODO: refactor next part, merge winner/part handling where possible 152 | # If we have a winner, we have a full (multi)set match 153 | if winner: 154 | suggest_this = True 155 | # Check if the winner meets all requirements 156 | for r in filtered_rules: 157 | # Check if there are macros used on this fileline at all 158 | if r.fileline not in macrousages_dict: 159 | # No macro used on this fileline, check the next 160 | continue 161 | macros_at_line = macrousages_dict[r.fileline] 162 | # Check that no other macros are involved 163 | for m in macros_at_line: 164 | if not m.macro.file_defined.endswith(u"global_macros"): 165 | # There are other macros at play, do not suggest 166 | suggest_this = False 167 | break 168 | if not suggest_this: 169 | # Also break out of the outer loop 170 | break 171 | # Do not suggest macro usages that are already in the policy 172 | # TODO: this WILL NOT SUGGEST a valid macro if there are 173 | # multiple rules on one line (i.e. rules separated only by 174 | # semicolon and not by newline), and the rules could use 175 | # identical macros 176 | # Remove already used macros from the winner 177 | winner = [x for x in winner if x.name not in ( 178 | x.name for x in macros_at_line)] 179 | if not winner: 180 | # If the winner is now empty, we don't care about this 181 | # suggestion anymore 182 | suggest_this = False 183 | break 184 | if suggest_this: 185 | for x in winner: 186 | # Create the Suggestion object 187 | g = GlobalMacroSuggestion(x.name, x.values, filtered_rules, 188 | x.score, rutc, permset) 189 | # Add it to the suggestions dictionary 190 | if g.filelines not in suggestions: 191 | suggestions[g.filelines] = [g] 192 | elif g not in suggestions[g.filelines]: 193 | suggestions[g.filelines].append(g) 194 | # Suggest close matches based on a threshold 195 | if part: 196 | suggest_this = True 197 | # Check if the partial suggestions meet all requirements 198 | # for being suggested 199 | for r in filtered_rules: 200 | # Check if there are macros used on this fileline at all 201 | if r.fileline not in macrousages_dict: 202 | # No macro used on this fileline, check the next 203 | continue 204 | else: 205 | # There are other macros at play, do not suggest 206 | suggest_this = False 207 | break 208 | if suggest_this: 209 | # Select the top SUGGESTION_MAX_NO suggestions 210 | # above SUGGESTION_THRESHOLD from the results, which are not 211 | # purposefully ignored by the user 212 | sgs = sorted([x for x in part if 213 | x.score >= plugin_conf.SUGGESTION_THRESHOLD], 214 | reverse=True)[:plugin_conf.SUGGESTION_MAX_NO] 215 | # For each of the selected close-matching suggestions, create 216 | # the Suggestion object and add it to the dictionary 217 | for x in sgs: 218 | g = GlobalMacroSuggestion(x.name, x.values, filtered_rules, 219 | x.score, rutc, permset) 220 | if g.filelines not in suggestions: 221 | suggestions[g.filelines] = [g] 222 | elif g not in suggestions[g.filelines]: 223 | suggestions[g.filelines].append(g) 224 | # Print the suggestions 225 | for filelines, sgs in iteritems(suggestions): 226 | full = [] 227 | part = [] 228 | for x in sgs: 229 | if x.score == 1: 230 | full.append(x) 231 | else: 232 | part.append(x) 233 | part.sort(reverse=True) 234 | if full or part: 235 | print(u"The following macros match a rule on these lines:") 236 | print(u"\n".join(filelines)) 237 | if full: 238 | # Print full match suggestion(s) 239 | print(u"Full match:") 240 | print(u", ".join((x.name for x in full))) 241 | # Compute suggested usage 242 | rutc = full[0].applies_to 243 | orig_permset = full[0].original_permset 244 | permset = set() 245 | for x in full: 246 | permset.update(x.macro_perms) 247 | extra_perms = orig_permset - permset 248 | usage = rutc 249 | if len(full) > 1 or extra_perms: 250 | usage += u" { " + u" ".join([x.name for x in full]) 251 | if extra_perms: 252 | usage += u" " + u" ".join(extra_perms) 253 | usage += u" };" 254 | else: 255 | usage += u" " + full[0].name + u";" 256 | print(u"Suggested usage:") 257 | print(usage) 258 | if part: 259 | # Print partial match suggestion(s) 260 | print(u"Partial match:") 261 | print(u"\n".join([ 262 | u"{}: {}%".format(x.name, x.score * 100) for x in part])) 263 | # Compute suggested usage 264 | rutc = part[0].applies_to 265 | orig_permset = part[0].original_permset 266 | permset = set() 267 | for x in part: 268 | permset.update(x.macro_perms) 269 | extra_perms = orig_permset - permset 270 | usage = rutc 271 | if len(part) > 1 or extra_perms: 272 | usage += u" { " + u" ".join([x.name for x in part]) 273 | if extra_perms: 274 | usage += u" " + u" ".join(extra_perms) 275 | usage += u" };" 276 | else: 277 | usage += u" " + part[0].name + u";" 278 | print(u"Suggested usage:") 279 | print(usage) 280 | if full or part: 281 | print(u"") 282 | 283 | 284 | class SetFitter(object): 285 | u"""Cover a given set with the minimum number of known sets. 286 | 287 | Pass the sets in as dict: {label: set}""" 288 | 289 | class RichSet(object): 290 | u"""A dict with an associated score for each element""" 291 | 292 | def __init__(self, name, values): 293 | self.name = name 294 | self.values = values 295 | self.tally = {} 296 | for elem in self.values: 297 | self.tally[elem] = 0 298 | self.nonzero = 0 299 | self.score = 0 300 | 301 | def contains(self, elem): 302 | """Check if the set contains a given element.""" 303 | return elem in self.values 304 | 305 | def incr(self, elem): 306 | """Add an element to the set. 307 | Increment the number of occurrences of this element in the set.""" 308 | if elem in self.tally: 309 | if self.tally[elem] == 0: 310 | # First match, update score 311 | self.nonzero += 1 312 | self.score = self.nonzero / len(self.tally) 313 | self.tally[elem] += 1 314 | 315 | def print_full(self): 316 | """Print the full string representation of the set.""" 317 | print(self.name + u" ({}/{})".format(self.score, len(self.tally))) 318 | for k, v in iteritems(self.tally): 319 | print(k + u" ({})".format(v)) 320 | 321 | def __repr__(self): 322 | return self.name + u": " + str(self.score) 323 | 324 | def __eq__(self, other): 325 | return self.score == other.score 326 | 327 | def __ne__(self, other): 328 | return self.score != other.score 329 | 330 | def __lt__(self, other): 331 | return self.score < other.score 332 | 333 | def __le__(self, other): 334 | return self.score <= other.score 335 | 336 | def __gt__(self, other): 337 | return self.score > other.score 338 | 339 | def __ge__(self, other): 340 | return self.score >= other.score 341 | 342 | def __hash__(self): 343 | return hash(str(self)) 344 | 345 | def __init__(self, d): 346 | u"""Initialise a SetFitter with a dictionary of the available sets. 347 | 348 | d - A dictionary {name: set} with the available sets accessible by 349 | name. 350 | """ 351 | self.d = d 352 | 353 | def fit(self, s, tclass=None): 354 | u"""Fit a set with the pre-supplied available sets. 355 | 356 | Only use sets which match a given class.""" 357 | # Heuristics to filter by class 358 | if not tclass: 359 | cl = None 360 | elif u"dir" in tclass: 361 | cl = u"dir" 362 | elif u"file" in tclass: 363 | cl = u"file" 364 | else: 365 | cl = None 366 | # Initialise a new list of rich sets 367 | rich_sets = [] 368 | for (key, value) in iteritems(self.d): 369 | # If the class is either None 370 | # or assigned and contained in the macro name 371 | if not cl or cl in key: 372 | rich_sets.append(SetFitter.RichSet(key, value)) 373 | # Fit the set 374 | for elem in s: 375 | for each in rich_sets: 376 | each.incr(elem) 377 | ones = [] 378 | part = [] 379 | for x in rich_sets: 380 | if x.score == 1: 381 | ones.append(x) 382 | else: 383 | part.append(x) 384 | # Compute all combinations of full macros 385 | combinations = [] 386 | for i in range(1, len(ones) + 1): 387 | combinations.extend(itertools.combinations(ones, i)) 388 | # Find the one that leaves the smallest extra set 389 | extra_dim = {} 390 | for c in combinations: 391 | # Set of macro combinations e.g. [r_file, w_file] 392 | # The combination is a tuple, make it a set 393 | c = set(c) 394 | # The set of permissions that results from the combination 395 | c_permset = set() 396 | for x in c: 397 | c_permset.update(x.values) 398 | # Compute the extra permissions in the set that are not covered by 399 | # the expansion of the selected combination of macros 400 | extra = s - c_permset 401 | # Index the combinations by score (number of extra elements, lower 402 | # is better) 403 | if len(extra) in extra_dim: 404 | extra_dim[len(extra)].append(c) 405 | else: 406 | extra_dim[len(extra)] = [c] 407 | # Find the smallest combination of macros that leaves the smallest 408 | # number of extra permissions 409 | if extra_dim: 410 | m = min(extra_dim) 411 | winner = min(extra_dim[m], key=len) 412 | else: 413 | winner = [] 414 | return (winner, part) 415 | -------------------------------------------------------------------------------- /plugins/risky_rules.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | u"""Assign a score to OEM rules depending on various criteria such as source and 18 | target types, permission sets, ... .""" 19 | 20 | # Necessary for Python 2/3 compatibility 21 | from __future__ import absolute_import 22 | from __future__ import division 23 | from future.utils import itervalues 24 | 25 | import logging 26 | import os.path 27 | import plugins.config.risky_rules as plugin_conf 28 | import policysource 29 | import policysource.mapping 30 | 31 | # Required by selint 32 | REQUIRED_RULES = plugin_conf.SUPPORTED_RULE_TYPES 33 | 34 | 35 | def score_terule(rule): 36 | u"""Assign a score to a TE rule depending on the scoring system.""" 37 | score = 0 38 | # START of the additive scoring system 39 | # Match the source 40 | # For each bucket 41 | for crit in plugin_conf.TYPES: 42 | # If the source is in the bucket 43 | if rule.source in plugin_conf.TYPES[crit]: 44 | # Assign a score depending on the scoring system: 45 | # Risk 46 | if plugin_conf.SCORING_SYSTEM == u"risk": 47 | # Simply add the risk score 48 | score += plugin_conf.SCORE_RISK[crit] 49 | # Trust, low 50 | elif plugin_conf.SCORING_SYSTEM in (u"trust_lh", u"trust_ll"): 51 | # Add the score with inverted weight wrt the max type value 52 | # i.e. give a "high" score to a type marked with a "low" score 53 | score += (plugin_conf.MAXIMUM_SCORE / 2) \ 54 | - plugin_conf.SCORE_TRUST[crit] 55 | # Trust, high 56 | elif plugin_conf.SCORING_SYSTEM in (u"trust_hl", u"trust_hh"): 57 | # Simply add the trust score 58 | score += plugin_conf.SCORE_TRUST[crit] 59 | break 60 | # This is a type transition: the target type does not mean much 61 | # Match the default type instead 62 | # For each bucket 63 | for crit in plugin_conf.TYPES: 64 | # If the default type is in the bucket 65 | if rule.deftype in plugin_conf.TYPES[crit]: 66 | # Assign a score depending on the scoring system: 67 | # Risk 68 | if plugin_conf.SCORING_SYSTEM == u"risk": 69 | # Simply add the risk score 70 | score += plugin_conf.SCORE_RISK[crit] 71 | # Trust, low 72 | elif plugin_conf.SCORING_SYSTEM in (u"trust_hl", u"trust_ll"): 73 | # Add the score with inverted weight wrt the max type value 74 | # i.e. give a "high" score to a type marked with a "low" score 75 | score += (plugin_conf.MAXIMUM_SCORE / 2) \ 76 | - plugin_conf.SCORE_TRUST[crit] 77 | # Trust, high 78 | elif plugin_conf.SCORING_SYSTEM in (u"trust_lh", u"trust_hh"): 79 | # Simply add the trust score 80 | score += plugin_conf.SCORE_TRUST[crit] 81 | break 82 | # END of the additive scoring system 83 | # Normalise score 84 | score /= plugin_conf.MAXIMUM_SCORE 85 | return score 86 | 87 | 88 | def score_avrule(rule): 89 | u"""Assign a score to an AV rule depending on the scoring system.""" 90 | score = 0 91 | # START of the additive scoring system 92 | # Match the source 93 | # For each bucket 94 | for crit in plugin_conf.TYPES: 95 | # If the source is in the bucket 96 | if rule.source in plugin_conf.TYPES[crit]: 97 | # Assign a score depending on the scoring system: 98 | # Risk 99 | if plugin_conf.SCORING_SYSTEM == u"risk": 100 | # Simply add the risk score 101 | score += plugin_conf.SCORE_RISK[crit] 102 | # Trust, low 103 | elif plugin_conf.SCORING_SYSTEM in (u"trust_lh", u"trust_ll"): 104 | # Add the score with inverted weight wrt the max type value 105 | # i.e. give a "high" score to a type marked with a "low" score 106 | score += (plugin_conf.MAXIMUM_SCORE / 2) \ 107 | - plugin_conf.SCORE_TRUST[crit] 108 | # Trust, high 109 | elif plugin_conf.SCORING_SYSTEM in (u"trust_hl", u"trust_hh"): 110 | # Simply add the trust score 111 | score += plugin_conf.SCORE_TRUST[crit] 112 | break 113 | # Match the target 114 | # Risk 115 | if plugin_conf.SCORING_SYSTEM == u"risk": 116 | # If the rule allows a capability, the second type is always going to 117 | # be "self", and as such meaningless for scoring purposes. 118 | if rule.tclass in plugin_conf.CAPABILITIES: 119 | # Add the score for the capability instead 120 | score += plugin_conf.SCORE[rule.tclass] 121 | else: 122 | # This is a normal allow rule, match the target 123 | for crit in plugin_conf.TYPES: 124 | if rule.target in plugin_conf.TYPES[crit]: 125 | score += plugin_conf.SCORE_RISK[crit] 126 | break 127 | else: 128 | # Trust 129 | for crit in plugin_conf.TYPES: 130 | if rule.target in plugin_conf.TYPES[crit]: 131 | # Trust, low 132 | if plugin_conf.SCORING_SYSTEM in (u"trust_hl", u"trust_ll"): 133 | # Add the score with inverted weight wrt the max type value 134 | # i.e. give a "high" score to a type marked with "low" 135 | score += (plugin_conf.MAXIMUM_SCORE / 2) \ 136 | - plugin_conf.SCORE_TRUST[crit] 137 | # Trust, high 138 | if plugin_conf.SCORING_SYSTEM in (u"trust_lh", u"trust_hh"): 139 | # Simply add the trust score 140 | score += plugin_conf.SCORE_TRUST[crit] 141 | # END of additive scoring system 142 | # START of multiplicative, if applicable 143 | if plugin_conf.SCORING_SYSTEM == u"risk": 144 | perm_score = 0 145 | # Compute score for the permission set 146 | for crit in plugin_conf.PERMS: 147 | # If the rule has any permission in common with set "crit" 148 | if rule.permset & plugin_conf.PERMS[crit]: 149 | # Update the permission coefficient for the rule to 150 | # the one of the "crit" set, if not already higher 151 | if perm_score < plugin_conf.SCORE[crit]: 152 | perm_score = plugin_conf.SCORE[crit] 153 | if perm_score: 154 | score *= perm_score 155 | # END of multiplicative scoring system 156 | # Normalise score 157 | score /= plugin_conf.MAXIMUM_SCORE 158 | return score 159 | 160 | 161 | def score_rule(rule): 162 | u"""Assign a score to a generic rule.""" 163 | if rule.rtype in policysource.mapping.AVRULES: 164 | return score_avrule(rule) 165 | elif rule.rtype in policysource.mapping.TERULES: 166 | return score_terule(rule) 167 | else: 168 | # This should not happen 169 | return None 170 | 171 | 172 | def main(policy, config): 173 | u"""Score OEM rules depending on a scoring system.""" 174 | # Check that we have been fed a valid policy 175 | if not isinstance(policy, policysource.policy.SourcePolicy): 176 | raise ValueError(u"Invalid policy") 177 | # Setup logging 178 | log = logging.getLogger(__name__) 179 | # Check that we are using a supported scoring system 180 | if plugin_conf.SCORING_SYSTEM not in (u"risk", u"trust_hl", u"trust_lh", 181 | u"trust_hh", u"trust_ll"): 182 | log.critical(u"Unsupported scoring system \"%s\". Aborting...", 183 | plugin_conf.SCORING_SYSTEM) 184 | return 185 | else: 186 | log.info(u"Scoring rules with \"%s\" scoring system...", 187 | plugin_conf.SCORING_SYSTEM) 188 | # Compute the absolute ignore paths 189 | FULL_IGNORE_PATHS = tuple(os.path.join(config.FULL_BASE_DIR, p) 190 | for p in plugin_conf.RULE_IGNORE_PATHS) 191 | 192 | mapper = policysource.mapping.Mapper( 193 | policy.policyconf, policy.attributes, policy.types, policy.classes) 194 | printouts = [] 195 | # Score the rules 196 | for rls in itervalues(policy.mapping.rules): 197 | for r in rls: 198 | # If this rule comes from an ignored path or its type is not 199 | # supported, ignore it 200 | if r.fileline.startswith(FULL_IGNORE_PATHS)\ 201 | or not r.rule.startswith(plugin_conf.SUPPORTED_RULE_TYPES)\ 202 | or str(r) in plugin_conf.IGNORED_RULES: 203 | continue 204 | # Generate the corresponding AV/TErule object 205 | rule = mapper.rule_factory(r.rule) 206 | # Get the score for the rule, according to the scoring system 207 | score = score_rule(rule) 208 | # Print rule 209 | if score >= plugin_conf.SCORE_THRESHOLD: 210 | printouts.append(u"{:.2f}: {}".format(score, r)) 211 | print(u"\n".join(sorted(printouts, reverse=plugin_conf.REVERSE_SORT))) 212 | -------------------------------------------------------------------------------- /plugins/unnecessary_rules.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | u"""Report rules matching some criteria specified in the configuration file. 18 | Specifically, this plugin provides 3 functionalities, described here. 19 | 20 | # Functionality 1 21 | Detect missing rules from a (ordered) tuple of rules. 22 | Some rules are expected to be found together: type_transition rules require 23 | specific associated allow rules to be meaningful, etc. 24 | This functionality looks for rules matching the first rule in a tuple specified 25 | in the configuration file, and verifies that all other rules in the tuple are 26 | actually present: if not, they are reported. 27 | 28 | Rules can be matched using simple placeholders: see the configuration file for 29 | examples. 30 | 31 | # Functionality 2 32 | Detect rules containing debug types. 33 | The user can define some types as "debug types" in the configuration file, and 34 | this functionality will report any rule found in the policy using those types. 35 | 36 | # Functionality 3 37 | Detect rules that grant some permission over some class without granting some 38 | necessary or required permission, e.g. a rule which grants "write" but not 39 | "open" over a "file" class, without granting "use" over the corresponding "fd" 40 | class. 41 | 42 | More precisely, this functionality reports rules that grant at least one 43 | permission over an object of class F from set F_A, without granting at least 44 | all permissions in set F_B or some extra permissions on some other class. 45 | All these permission sets can be specified by the user in the configuration 46 | file. 47 | 48 | """ 49 | 50 | # Necessary for Python 2/3 compatibility 51 | from __future__ import absolute_import 52 | from builtins import range 53 | from future.utils import iteritems 54 | 55 | import logging 56 | import re 57 | import os.path 58 | from setools.terulequery import TERuleQuery as TERuleQuery 59 | import plugins.config.unnecessary_rules as plugin_conf 60 | import policysource 61 | import policysource.mapping 62 | 63 | # Required by selint 64 | REQUIRED_RULES = plugin_conf.SUPPORTED_RULE_TYPES 65 | 66 | # Global variable to hold the log 67 | LOG = None 68 | 69 | # Global variable to hold the mapper 70 | MAPPER = None 71 | 72 | # Global variable to hold the full ignored paths 73 | FULL_IGNORE_PATHS = None 74 | 75 | # Global variable to hold the supported non-ignored rules mapping 76 | NON_IGNORED_MAPPING = {} 77 | 78 | # Regex for a valid argument in m4 79 | VALID_ARG_R = r"[a-zA-Z0-9_-]+" 80 | 81 | 82 | def query_for_rule(policy, r): 83 | u"""Query a policy for rules matching a given rule. 84 | The rule may contain regex fields.""" 85 | # Mark whether a query parameter is a regex or a string 86 | sr = r"[a-zA-Z0-9_-]+" in r.source 87 | tr = r"[a-zA-Z0-9_-]+" in r.target 88 | cr = r"[a-zA-Z0-9_-]+" in r.tclass 89 | # Handle self 90 | if r.target == u"self": 91 | # Override the target to match everything 92 | xtarget = VALID_ARG_R 93 | tr = True 94 | else: 95 | xtarget = r.target 96 | # Query for an AV rule 97 | if r.rtype in policysource.mapping.AVRULES: 98 | query = TERuleQuery(policy=policy.policy, ruletype=[r.rtype], 99 | source=r.source, source_regex=sr, 100 | source_indirect=False, 101 | target=xtarget, target_regex=tr, 102 | target_indirect=False, 103 | tclass=[r.tclass], tclass_regex=cr, 104 | perms=r.permset, perms_subset=True) 105 | # Query for a TE rule 106 | elif r.rtype in policysource.mapping.TERULES: 107 | dr = r"[a-zA-Z0-9_-]+" in r.deftype 108 | query = TERuleQuery(policy=policy.policy, ruletype=[r.rtype], 109 | source=r.source, source_regex=sr, 110 | source_indirect=False, 111 | target=xtarget, target_regex=tr, 112 | target_indirect=False, 113 | tclass=[r.tclass], tclass_regex=cr, 114 | default=r.deftype, default_regex=dr) 115 | else: 116 | # We should have no other rules, as they are already filtered 117 | # when creating the list with the rule_factory method 118 | LOG.warning(u"Unsupported rule: \"%s\"", r) 119 | return None 120 | # Filter all rules 121 | if r.target == u"self": 122 | # Discard rules whose mask contained "self" as a target, 123 | # but whose result's source and target are different 124 | results = [x for x in query.results() if x.source == x.target] 125 | else: 126 | results = list(query.results()) 127 | filtered_results = [] 128 | # Discard rules coming from explicitly ignored paths 129 | for x in results: 130 | x_str = str(x) 131 | rule = MAPPER.rule_factory(x_str) 132 | rutc = rule.up_to_class 133 | # Get the MappedRule(s) corresponding to this rutc 134 | rls = [y for y in policy.mapping.rules[rutc]] 135 | if len(rls) == 1: 136 | # If this rule comes from a single place, this is easy. 137 | # Drop the rule if the path it comes from is ignored 138 | if not rls[0].fileline.startswith(FULL_IGNORE_PATHS): 139 | filtered_results.append(x) 140 | NON_IGNORED_MAPPING[x_str] = [rls[0].fileline] 141 | else: 142 | # If this rule comes from multiple places, this is more complex. 143 | # Check that all rules that make up the specific rule we found 144 | # come from non-ignored paths. If not, drop the rule. 145 | if rule.rtype in policysource.mapping.AVRULES: 146 | # Check that the permission set of the "x" rule is covered by 147 | # non-ignored rules. If not, drop the rule. 148 | tmpset = set() 149 | for each in rls: 150 | if not each.fileline.startswith(FULL_IGNORE_PATHS): 151 | prmstr = MAPPER.rule_split_after_class(each.rule)[1] 152 | tmpset.update(prmstr.strip(u" {};").split()) 153 | if x_str in NON_IGNORED_MAPPING: 154 | NON_IGNORED_MAPPING[x_str].append(each.fileline) 155 | else: 156 | NON_IGNORED_MAPPING[x_str] = [each.fileline] 157 | if tmpset >= rule.permset: 158 | # The set of permissions created by non-ignored rules is 159 | # sufficient 160 | filtered_results.append(x) 161 | else: 162 | NON_IGNORED_MAPPING.pop(x_str, None) 163 | elif rule.rtype in policysource.mapping.TERULES: 164 | # Check for every type_transition rule individually 165 | for each in rls: 166 | if not each.fileline.startswith(FULL_IGNORE_PATHS): 167 | filtered_results.append(x) 168 | if x_str in NON_IGNORED_MAPPING: 169 | NON_IGNORED_MAPPING[x_str].append(each.fileline) 170 | else: 171 | NON_IGNORED_MAPPING[x_str] = [each.fileline] 172 | return filtered_results 173 | 174 | 175 | def substitute_args(rule, args): 176 | u"""Substitute placeholder arguments in a rule with their actual values. 177 | 178 | The rule must be passed in as a string. 179 | e.g. 180 | rule = "allow @@ARG0@@ sometype:class perm;" 181 | args = {"arg0": "somedomain"} 182 | -> returns "allow somedomain sometype:class perm;" 183 | """ 184 | modified_args = {} 185 | for (k, v) in iteritems(args): 186 | modified_args[u"@@" + k.upper() + u"@@"] = v 187 | for (k, v) in iteritems(modified_args): 188 | rule = rule.replace(k, v) 189 | return rule 190 | 191 | 192 | def accumulate_perms(rutc, rules): 193 | u"""Accumulate the permissions found in rules having a common subprefix up 194 | to the class (rutc).""" 195 | found_perms = set() 196 | for x in rules: 197 | # If a rule comes from an ignored path, not only ignore it, but 198 | # ignore the whole rutc 199 | if x.fileline.startswith(FULL_IGNORE_PATHS): 200 | found_perms = None 201 | break 202 | # Get the permission string, strip it, split it, burn it, rip it, 203 | # drag and drop it, zip - unzip it and update the permission set 204 | found_perms.update(x.rule[len(rutc):].strip(u" {};").split()) 205 | return found_perms 206 | 207 | 208 | def main(policy, config): 209 | u"""Find unnecessary or missing rules in the policy.""" 210 | # Check that we have been fed a valid policy 211 | if not isinstance(policy, policysource.policy.SourcePolicy): 212 | raise ValueError(u"Invalid policy") 213 | # Setup logging 214 | log = logging.getLogger(__name__) 215 | global LOG 216 | LOG = log 217 | 218 | # Compute the absolute ignore paths 219 | global FULL_IGNORE_PATHS 220 | FULL_IGNORE_PATHS = tuple(os.path.join(config.FULL_BASE_DIR, p) 221 | for p in plugin_conf.RULE_IGNORE_PATHS) 222 | 223 | # Create a global mapper to expand the rules 224 | global MAPPER 225 | MAPPER = policysource.mapping.Mapper( 226 | policy.policyconf, policy.attributes, policy.types, policy.classes) 227 | 228 | # Compile the regex for speed 229 | rule_w_placeholder_r = re.compile( 230 | r".*" + ArgExtractor.placeholder_r + r".*") 231 | # Functionality 1 232 | # Look for missing rules in predetermined tuples 233 | print(u"Checking for missing rules") 234 | for t in plugin_conf.RULES_TUPLES: 235 | log.debug(u"Checking tuple containing these rules:") 236 | for x in t: 237 | log.debug(x) 238 | placeholder_sub = False 239 | # Ignore tuples with a single element. We should not have any, anyway 240 | if len(t) < 2: 241 | continue 242 | # Ignore tuples that begin with an unsupported rule 243 | if not t[0].startswith(policysource.mapping.ONLY_MAP_RULES): 244 | continue 245 | # If the first rule in the tuple contains at least one placeholder 246 | if rule_w_placeholder_r.match(t[0]): 247 | placeholder_sub = True 248 | # Initialise an extractor with the placeholder rule 249 | e = ArgExtractor(t[0]) 250 | # Substitute the positional placeholder arguments with a 251 | # regex matching valid argument characters 252 | l_r = re.sub(r"@@ARG[0-9]+@@", VALID_ARG_R, t[0]) 253 | tmp = MAPPER.rule_factory(l_r) 254 | # Get the rules matching the query for the rule with regexes 255 | # N.B. this already discards rules coming from ignored paths 256 | rules = query_for_rule(policy, tmp) 257 | if not rules: 258 | continue 259 | log.debug(u"Found rules:") 260 | for x in rules: 261 | log.debug(str(x)) 262 | else: 263 | # If the first rule contains no placeholder, simply use it as a 264 | # string 265 | rules = [t[0]] 266 | # For each rule matching the query 267 | for r in rules: 268 | # Skip rules purposefully ignored by the user 269 | if r in plugin_conf.IGNORED_RULES: 270 | continue 271 | # For each additional rule in the tuple, check that it is in the 272 | # policy, substituting placeholders if necessary. 273 | missing_rules = [] 274 | if placeholder_sub: 275 | # Get the arguments from the rule 276 | args = e.extract(r) 277 | # For each additional rule in the tuple 278 | for each_rule in t[1:]: 279 | # Ignore unsupported rules 280 | if not each_rule.startswith(policysource.mapping.ONLY_MAP_RULES): 281 | continue 282 | if placeholder_sub: 283 | nec_rule = substitute_args(each_rule, args) 284 | else: 285 | nec_rule = each_rule 286 | nec_rule_full = MAPPER.rule_factory(nec_rule) 287 | # Shorter variable name 288 | nrfutc = nec_rule_full.up_to_class 289 | # If the rule up to the class is in the mapping 290 | if nrfutc in policy.mapping.rules: 291 | # Check if the rule is actually present by possibly 292 | # combining the existing rules in the mapping 293 | if nec_rule_full.rtype in policysource.mapping.AVRULES: 294 | # If we are looking for an allow rule, combine 295 | # existing allow rules and check if the resulting 296 | # rule is a superset of the rule we are looking 297 | # for 298 | permset = set() 299 | for x in policy.mapping.rules[nrfutc]: 300 | x_f = MAPPER.rule_factory(x.rule) 301 | permset.update(x_f.permset) 302 | # If not a subset, print the rule and the missing 303 | # permissions 304 | if not nec_rule_full.permset <= permset: 305 | missing = u" (missing \"" 306 | missing += u" ".join(nec_rule_full.permset - 307 | permset) 308 | missing += u"\")" 309 | missing_rules.append(nec_rule + missing) 310 | if nec_rule_full.rtype in policysource.mapping.TERULES: 311 | # If we are looking for a TE rule, check for an 312 | # identical match 313 | if nec_rule not in policy.mapping.rules[nrfutc]: 314 | missing_rules.append(nec_rule) 315 | # If the rule is not even in the mapping 316 | else: 317 | # The rule is completely missing from the policy 318 | missing_rules.append(nec_rule) 319 | if missing_rules: 320 | # TODO: print fileline 321 | rutc = MAPPER.rule_split_after_class(str(r))[0] 322 | print(u"Rule:") 323 | if len(policy.mapping.rules[rutc]) > 1: 324 | print(u" " + str(r)) 325 | print(u"made up of rules:") 326 | for x in policy.mapping.rules[rutc]: 327 | print(u" " + str(x)) 328 | else: 329 | print(u" " + str(policy.mapping.rules[rutc][0])) 330 | print(u"is missing associated rule(s):") 331 | for x in missing_rules: 332 | print(u" " + str(x)) 333 | # Functionality 2 334 | # Look for debug types 335 | print(u"Checking for rules containing debug types") 336 | for rutc in policy.mapping.rules: 337 | for dbt in plugin_conf.DEBUG_TYPES: 338 | if dbt and dbt in rutc: 339 | print(u"Rule contains debug type \"{}\":".format(dbt)) 340 | for each in policy.mapping.rules[rutc]: 341 | eachstr = str(each) 342 | # Skip rules purposefully ignored by the user 343 | if eachstr not in plugin_conf.IGNORED_RULES: 344 | print(u" " + eachstr) 345 | 346 | # Functionality 3 347 | # Look for rules not granting minimum permissions 348 | print(u"Checking for rules not granting minimum required permissions") 349 | # Check that the configuration value of REQUIRED_PERMS is valid 350 | if hasattr(plugin_conf, u"REQUIRED_PERMS") and \ 351 | isinstance(plugin_conf.REQUIRED_PERMS, dict) and\ 352 | plugin_conf.REQUIRED_PERMS: 353 | # The REQUIRED_PERMS dictionary exists and is not empty 354 | rmv = [] 355 | for (k, v) in iteritems(plugin_conf.REQUIRED_PERMS): 356 | if not(isinstance(k, str) and isinstance(v, tuple) and 357 | isinstance(v[0], set) and isinstance(v[1], set) and 358 | isinstance(v[2], dict)): 359 | # If the format is invalid 360 | log.error("Ignoring invalid REQUIRED_PERMS value \"%s\"", k) 361 | log.error(" %s", v) 362 | rmv.append(k) 363 | for rm in rmv: 364 | del plugin_conf.REQUIRED_PERMS[rm] 365 | for rutc in policy.mapping.rules: 366 | # Filter the rules by type (beginning of the "rule up to class") 367 | if not rutc.startswith(u"allow"): 368 | continue 369 | # Get the rule class and pre-class part 370 | pre_cls, cls = rutc.split(u":") 371 | # Check if there are any constraint for this class 372 | if cls not in plugin_conf.REQUIRED_PERMS: 373 | # If not, skip this rule 374 | continue 375 | # Get the "interesting" perms and the minimum perms required by them 376 | perms, req_perms, add_perms = plugin_conf.REQUIRED_PERMS[cls] 377 | # Accumulate the permissions granted by all the rules under "rutc" 378 | found_perms = accumulate_perms(rutc, policy.mapping.rules[rutc]) 379 | # If found_perms has been set to None, skip this rule 380 | if found_perms is None: 381 | continue 382 | report = False 383 | all_extras_granted = True 384 | # If a rule for this class grants some permission from the first 385 | # set, but does not grant at least the required permission(s) 386 | if found_perms & perms and not found_perms >= req_perms: 387 | # Check if it grants the additional permissions over additional 388 | # classes instead 389 | # Preliminarily mark the rule to be reported: correct this if the 390 | # rule grants all extra required permissions 391 | report = True 392 | for k, v in iteritems(add_perms): 393 | # Check if the found additional permissions (found_ap) are a 394 | # superset of the required additional permissions (v) 395 | found_ap = None 396 | # Search for the new rule composed of OLD_RULE:new class 397 | new_rutc = pre_cls + ":" + k 398 | if new_rutc in policy.mapping.rules: 399 | found_ap = accumulate_perms( 400 | rutc, policy.mapping.rules[new_rutc]) 401 | if found_ap is None or not found_ap >= v: 402 | # If new_rutc is not in the mapping, or if all its rules 403 | # come from ignored paths, or the set of found 404 | # permissions is not a superset of the required 405 | # permissions, mark false and break 406 | all_extras_granted = False 407 | break 408 | # If all extra required permissions have been granted, do not 409 | # report the rule 410 | if all_extras_granted: 411 | report = False 412 | if report: 413 | res_str = rutc + u" " 414 | if len(found_perms) > 1: 415 | res_str += u"{ " + u" ".join(found_perms) + u" };" 416 | else: 417 | res_str += u" ".join(found_perms) + u";" 418 | # Skip rules purposefully ignored by the user 419 | if res_str in plugin_conf.IGNORED_RULES: 420 | continue 421 | print(u"Permissions in rule:") 422 | print(res_str) 423 | rutc = MAPPER.rule_split_after_class(res_str)[0] 424 | for each in policy.mapping.rules[rutc]: 425 | print(u" " + each.fileline) 426 | print(u"require additional permissions: " 427 | u"\"{}\"".format(u" ".join(req_perms - found_perms))) 428 | print(u"or permissions over different classes:") 429 | for k, v in iteritems(add_perms): 430 | extra_str = u"\"" + k + u" " 431 | if len(v) > 1: 432 | extra_str += u"{ " + u" ".join(v) + u" }\"" 433 | else: 434 | extra_str += u" ".join(v) + u"\"" 435 | print(u" " + extra_str) 436 | print(u"") 437 | 438 | 439 | class ArgExtractor(object): 440 | u"""Extract macro arguments from an expanded rule according to a regex.""" 441 | placeholder_r = r"@@ARG[0-9]+@@" 442 | 443 | def __init__(self, rule): 444 | u"""Initialise the ArgExtractor with the rule expanded with the named 445 | placeholders. 446 | 447 | e.g.: "allow @@ARG0@@ @@ARG0@@_tmpfs:file execute;" 448 | """ 449 | self.rule = rule 450 | # Convert the rule to a regex that matches it and extracts the groups 451 | self.regex = re.sub(self.placeholder_r, 452 | u"(" + VALID_ARG_R + u")", self.rule) 453 | self.regex_blocks = policysource.mapping.Mapper.rule_parser(self.regex) 454 | self.regex_blocks_c = {} 455 | # Save precompiled regex blocks 456 | for blk in self.regex_blocks: 457 | if VALID_ARG_R in blk: 458 | self.regex_blocks_c[blk] = re.compile(blk) 459 | # Save pre-computed rule permission set 460 | if self.regex_blocks[0] in policysource.mapping.AVRULES: 461 | if any(x in self.regex_blocks[4] for x in u"{}"): 462 | self.regex_perms = set( 463 | self.regex_blocks[4].strip(u"{}").split()) 464 | else: 465 | self.regex_perms = set([self.regex_blocks[4]]) 466 | else: 467 | self.regex_perms = None 468 | # Save the argument names as "argN" 469 | self.args = [x.strip(u"@").lower() 470 | for x in re.findall(self.placeholder_r, self.rule)] 471 | 472 | def extract(self, rule): 473 | u"""Extract the named arguments from a matching rule.""" 474 | matches = self.match_rule(rule) 475 | retdict = {} 476 | if matches: 477 | # The rule matches the regex: extract the matches 478 | for i in range(len(matches)): 479 | # Handle multiple occurrences of the same argument in a rule 480 | # If the occurrences don't all have the same value, this rule 481 | # does not actually match the placeholder rule 482 | if self.args[i] in retdict: 483 | # If we have found this argument already 484 | if retdict[self.args[i]] != matches[i]: 485 | # If the value we just found is different 486 | # The rule does not actually match the regex 487 | raise ValueError(u"Rule does not match ArgExtractor" 488 | u"expression: \"{}\"".format( 489 | self.regex)) 490 | else: 491 | retdict[self.args[i]] = matches[i] 492 | return retdict 493 | else: 494 | # The rule does not match the regex 495 | raise ValueError(u"Rule does not match ArgExtractor expression: " 496 | u"\"{}\"".format(self.regex)) 497 | 498 | def match_rule(self, rule): 499 | u"""Perform a rich comparison between the provided rule and the rule 500 | expected by the extractor. 501 | The rule must be passed in as a setools AV/TERule object. 502 | 503 | Return True if the rule satisfies (at least) all constraints imposed 504 | by the extractor.""" 505 | matches = [] 506 | rule_objname = None 507 | # Shorter name -> shorter lines 508 | regex_blocks = self.regex_blocks 509 | regex_blocks_c = self.regex_blocks_c 510 | # Only call the rule methods once, cache values locally 511 | rule_blocks = [] 512 | rule_blocks.append(str(rule.ruletype)) 513 | if rule_blocks[0] == u"type_transition": 514 | if len(regex_blocks) == 6: 515 | # Name transition 516 | try: 517 | rule_objname = str(rule.filename) 518 | except: 519 | return None 520 | elif rule_blocks[0] not in policysource.mapping.AVRULES: 521 | # Not an allow rule, not a type_transition rule 522 | return None 523 | # Match the rule block by block 524 | ##################### Match block 0 (ruletype) ###################### 525 | # No macro arguments here, no regex match 526 | if rule_blocks[0] != regex_blocks[0]: 527 | return None 528 | ################################################################## 529 | rule_blocks.append(str(rule.source)) 530 | ##################### Match block 1 (source) ##################### 531 | if regex_blocks[1] in regex_blocks_c: 532 | # The domain contains an argument, match the regex 533 | m = regex_blocks_c[regex_blocks[1]].match(rule_blocks[1]) 534 | if m: 535 | matches.append(m.group(1)) 536 | else: 537 | return None 538 | else: 539 | # The domain contains no argument, match the string 540 | if rule_blocks[1] != regex_blocks[1]: 541 | return None 542 | ################################################################## 543 | rule_blocks.append(str(rule.target)) 544 | ##################### Match block 2 (target) ##################### 545 | if regex_blocks[2] in regex_blocks_c: 546 | # The type contains an argument, match the regex 547 | m = regex_blocks_c[regex_blocks[2]].match(rule_blocks[2]) 548 | if m: 549 | matches.append(m.group(1)) 550 | else: 551 | return None 552 | else: 553 | # The type contains no argument, match the string 554 | if regex_blocks[2] == u"self" and rule_blocks[2] != u"self": 555 | # Handle "self" expansion case 556 | # TODO: check if this actually happens 557 | if rule_blocks[2] != rule_blocks[1]: 558 | return None 559 | elif rule_blocks[2] != regex_blocks[2]: 560 | return None 561 | ################################################################## 562 | rule_blocks.append(str(rule.tclass)) 563 | ##################### Match block 3 (tclass) ##################### 564 | if regex_blocks[3] in regex_blocks_c: 565 | # The class contains an argument, match the regex 566 | # This should never happen, however 567 | m = regex_blocks_c[regex_blocks[3]].match(rule_blocks[3]) 568 | if m: 569 | matches.append(m.group(1)) 570 | else: 571 | return None 572 | else: 573 | # The class contains no argument 574 | # Match a (super)set of what is required by the regex 575 | if rule_blocks[3] != regex_blocks[3]: 576 | # Simple class, match the string 577 | return None 578 | ################################################################## 579 | ##################### Match block 4 (variable) ################### 580 | if rule_blocks[0] in policysource.mapping.AVRULES: 581 | ################ Match an AV rule ################ 582 | # Block 4 is the permission set 583 | # Match a (super)set of what is required by the regex 584 | if not self.regex_perms <= rule.perms: 585 | # If the perms in the rule are not at least those in 586 | # the regex 587 | return None 588 | ################################################## 589 | elif rule_blocks[0] == u"type_transition": 590 | ################ Match a type_transition rule ################# 591 | # Block 4 is the default type 592 | rule_default = str(rule.default) 593 | if regex_blocks[4] in regex_blocks_c: 594 | # The default type contains an argument, match the regex 595 | m = regex_blocks_c[regex_blocks[4]].match(rule_default) 596 | if m: 597 | matches.append(m.group(1)) 598 | else: 599 | return None 600 | else: 601 | # The default type contains no argument, match the string 602 | if rule_default != regex_blocks[4]: 603 | return None 604 | ################################################## 605 | ################################################################## 606 | ##################### Match block 5 (name trans) ################# 607 | if rule_objname: 608 | # If this type transition has 6 fields, it is a name transition 609 | # Block 5 is the object name 610 | if regex_blocks[5] in regex_blocks_c: 611 | # The object name contains an argument, match the regex 612 | m = regex_blocks_c[regex_blocks[5]].match(rule_objname) 613 | if m: 614 | matches.append(m.group(1)) 615 | else: 616 | return None 617 | else: 618 | # The object name contains no argument, match the string 619 | if rule_objname.strip(u"\"") != regex_blocks[5].strip(u"\""): 620 | return None 621 | ################################################################## 622 | ######################## All blocks match ######################## 623 | return matches 624 | -------------------------------------------------------------------------------- /plugins/user_neverallows.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | u"""Verify that custom user-defined neverallow rules are obeyed.""" 18 | 19 | # Necessary for Python 2/3 compatibility 20 | from __future__ import absolute_import 21 | from __future__ import division 22 | from future.utils import iteritems 23 | import sys 24 | 25 | # import logging 26 | import plugins.config.user_neverallows as plugin_conf 27 | import policysource 28 | import policysource.mapping 29 | 30 | # Required by selint 31 | REQUIRED_RULES = plugin_conf.SUPPORTED_RULE_TYPES 32 | 33 | 34 | def get_user_rules(expander, mapper): 35 | u"""Get the user-supplied rules from the configuration file. 36 | 37 | Return a dictionary {RUTC: AVRule}""" 38 | supplied_rules = {} 39 | for r in plugin_conf.NEVERALLOWS: 40 | # Convert the rules to unicode 41 | # If this is Python 2 and this is a str, convert to unicode 42 | if isinstance(r, str) and (sys.version_info < (3, 0)): 43 | r = r.decode("utf-8") 44 | # Expand the possible global_macros in the rule 45 | exp_r = expander.expand(r) 46 | # If the expansion failed, process the next rule 47 | if not exp_r: 48 | continue 49 | # Generate a dictionary {rutc: full} containing all rules deriving from 50 | # the attribute, set and complement expansion in the rule. 51 | resulting_rules = mapper.expand_rule(exp_r) 52 | # Generate an AVRule object from the string representation of each rule 53 | # Generate a dictionary {allow: AVRule}, where the key is the allow 54 | # rule corresponding to the neverallow, and the value is the full 55 | # neverallow rule as an AVRule object 56 | for (k, v) in iteritems(resulting_rules): 57 | supplied_rules[k[5:]] = mapper.rule_factory(v) 58 | return supplied_rules 59 | 60 | 61 | def main(policy, config): 62 | u"""Check that the policy obeys custom user-defined neverallow rules.""" 63 | # Check that we have been fed a valid policy 64 | if not isinstance(policy, policysource.policy.SourcePolicy): 65 | raise ValueError(u"Invalid policy") 66 | # Setup logging 67 | # log = logging.getLogger(__name__) 68 | 69 | mapper = policysource.mapping.Mapper( 70 | policy.policyconf, policy.attributes, policy.types, policy.classes) 71 | # Process the user-submitted neverallow rules into a dictionary of 72 | # {RUTC: AVRule} for easier handling 73 | user_rules = get_user_rules(policy._expander, mapper) 74 | # Check the rules 75 | for rutc, rls in iteritems(policy.mapping.rules): 76 | if not rutc.startswith(plugin_conf.SUPPORTED_RULE_TYPES): 77 | continue 78 | # If an allow rule matches some user-specified neverallow rule 79 | if rutc in user_rules: 80 | allowed_perms = set() 81 | for r in rls: 82 | # Generate the AVrule object for the allow rule coming from 83 | # the policy 84 | rule = mapper.rule_factory(r.rule) 85 | # Combine the permissions 86 | allowed_perms.update(rule.permset) 87 | # If the rule allows any permission in the neverallow, report it 88 | if allowed_perms & user_rules[rutc].permset: 89 | print(u"Rule grants neverallowed permissions: \"{}\"".format( 90 | u" ".join(allowed_perms & user_rules[rutc].permset))) 91 | full_rule = rutc + " " 92 | if len(allowed_perms) > 1: 93 | full_rule += u"{ " + u" ".join(allowed_perms) + u" };" 94 | else: 95 | full_rule += u" ".join(allowed_perms) + u";" 96 | print(u" " + full_rule) 97 | for r in rls: 98 | print(u" " + str(r)) 99 | -------------------------------------------------------------------------------- /policysource/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # This file is part of the policysource library. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as 9 | # published by the Free Software Foundation, either version 2.1 of 10 | # the License, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program. If not, see 19 | # . 20 | # 21 | """policysource - classes and methods to process a source SELinux policy 22 | policysource.policy - a source SELinux policy abstraction library 23 | policysource.macro - a M4 macro handling library""" 24 | -------------------------------------------------------------------------------- /policysource/macro.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # This file is part of the policysource library. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as 9 | # published by the Free Software Foundation, either version 2.1 of 10 | # the License, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program. If not, see 19 | # . 20 | # 21 | """Classes providing abstractions for m4 macros.""" 22 | 23 | # Necessary for Python 2/3 compatibility 24 | from __future__ import absolute_import 25 | from io import open 26 | from builtins import range 27 | 28 | import os 29 | import re 30 | import tempfile 31 | import subprocess 32 | import logging 33 | 34 | 35 | class Error(Exception): 36 | """Custom error base class.""" 37 | 38 | def __init__(self, message): 39 | super(Error, self).__init__(message) 40 | 41 | 42 | class M4MacroError(Error): 43 | """Exception raised by the M4Macro constructor if a macro is not valid""" 44 | pass 45 | 46 | 47 | class M4MacroExpanderError(Error): 48 | """Exception raised by the M4MacroExpander constructor""" 49 | pass 50 | 51 | 52 | class M4FreezeFileError(Error): 53 | """Exception raised by the M4FreezeFile constructor if file creation 54 | failed""" 55 | pass 56 | 57 | 58 | class M4MacroExpander(object): 59 | """Class providing a way to expand m4 macros.""" 60 | 61 | def __init__(self, macro_files, tmpdir, extra_defs): 62 | """Initialize a macro expander. 63 | 64 | Create a freeze file with the supplied macro definition files, 65 | and set it up to be used to expand m4 macros.""" 66 | # Setup logger 67 | self.log = logging.getLogger(self.__class__.__name__) 68 | # Setup temporary directory 69 | if (tmpdir and os.access(tmpdir, os.F_OK) and 70 | os.access(tmpdir, os.R_OK | os.W_OK | os.X_OK)): 71 | # We have been provided with a valid directory 72 | # We do not manage it (do not destroy it!) 73 | self._tmpdir = tmpdir 74 | self._tmpdir_managed = False 75 | else: 76 | # Create a temporary directory 77 | self._tmpdir = tempfile.mkdtemp() 78 | self.log.debug( 79 | u"Created temporary directory \"%s\".", self._tmpdir) 80 | # We manage it (we must destroy it when we're done) 81 | self._tmpdir_managed = True 82 | # Setup freeze file 83 | try: 84 | self.freeze_file = M4FreezeFile( 85 | macro_files, self.tmpdir, extra_defs) 86 | except M4FreezeFileError as e: 87 | # We failed to generate the freeze file, abort 88 | self.log.error(u"%s", e.message) 89 | raise M4MacroExpanderError(e.message) 90 | # Create a temporary file that will contain, at each request, the 91 | # macro to be expanded by m4. This is better than piping input to m4. 92 | # mkstemp() returns a tuple containing a handle to an open file 93 | # and the absolute pathname of that file, in that order 94 | tpl = tempfile.mkstemp(dir=self.tmpdir) 95 | os.close(tpl[0]) 96 | self._tmp = tpl[1] 97 | self.log.debug(u"Created temporary file \"%s\".", self.tmp) 98 | # Define the expansion command 99 | self.expansion_command = [u"m4", u"-R", 100 | self.freeze_file.freeze_file, self.tmp] 101 | 102 | def __del__(self): 103 | """Clean up the temporary file, the freeze file and the 104 | temporary directory""" 105 | # Remove the temporary file 106 | try: 107 | os.remove(self.tmp) 108 | except OSError: 109 | self.log.warning(u"Trying to remove the temporary file" 110 | u" \"%s\"... failed!", self.tmp) 111 | else: 112 | self.log.debug(u"Trying to remove the temporary file" 113 | u" \"%s\"... done!", self.tmp) 114 | # Force removal of the freeze file 115 | del self.freeze_file 116 | # Try to remove the temporary directory if managed 117 | if self.tmpdir_managed: 118 | try: 119 | os.rmdir(self.tmpdir) 120 | except OSError: 121 | self.log.warning(u"Trying to remove the temporary directory" 122 | u" \"%s\"... failed!", self.tmpdir) 123 | else: 124 | self.log.debug(u"Trying to remove the temporary directory" 125 | u" \"%s\"... done!", self.tmpdir) 126 | 127 | def expand(self, text): 128 | """Expand a string of text representing a m4 macro.""" 129 | # Write the macro to the temporary file 130 | with open(self.tmp, u"w", encoding=u'utf-8') as mfile: 131 | mfile.write(text) 132 | # Try to get the macro expansion with m4 133 | try: 134 | expansion = subprocess.check_output(self.expansion_command) 135 | except subprocess.CalledProcessError as e: 136 | # Log the error and change the function return value to None 137 | self.log.warning(u"%s", e.output) 138 | expansion = None 139 | else: 140 | expansion = expansion.decode("utf-8") 141 | return expansion 142 | 143 | def dump(self, text): 144 | """Dump the definition of a m4 macro.""" 145 | # Write the command to a temporary file 146 | with open(self.tmp, u"w", encoding=u'utf-8') as mfile: 147 | mfile.write(u"dumpdef(`{}')".format(text)) 148 | # Run the m4 command 149 | try: 150 | definition = subprocess.check_output(self.expansion_command, 151 | stderr=subprocess.STDOUT) 152 | except subprocess.CalledProcessError as e: 153 | # Log the error and change the function return value to None 154 | self.log.warning(u"%s", e.output) 155 | definition = None 156 | else: 157 | definition = definition.decode("utf-8") 158 | return definition 159 | 160 | @property 161 | def tmpdir(self): 162 | """Get the temporary directory used by the expander.""" 163 | return self._tmpdir 164 | 165 | @property 166 | def tmpdir_managed(self): 167 | """Check if the temporary directory used by the expander is 168 | managed by the expander.""" 169 | return self._tmpdir_managed 170 | 171 | @property 172 | def tmp(self): 173 | """Get the temporary file used by the expander.""" 174 | return self._tmp 175 | 176 | 177 | class M4FreezeFile(object): 178 | 179 | """Class for handling an m4 freeze file.""" 180 | 181 | def __init__(self, files, tmpdir, extra_defs=[], name="freezefile"): 182 | """Generate the freeze file with all macro definitions.""" 183 | # Setup logger 184 | self.log = logging.getLogger(self.__class__.__name__) 185 | # Setup parameters 186 | self.files = files 187 | self.tmpdir = tmpdir 188 | self.extra_defs = extra_defs 189 | self.name = name 190 | self.freeze_file = os.path.join(tmpdir, name) 191 | self.command = [u"m4"] 192 | for definition in self.extra_defs: 193 | self.command.extend([u"-D", definition]) 194 | self.command.extend([u"-s"]) 195 | self.command.extend(self.files) 196 | self.command.extend([u"-F", self.freeze_file]) 197 | self.log.debug(u"Trying to generate freeze file \"%s\"...", 198 | self.freeze_file) 199 | self.log.debug(u"$ %s", u" ".join(self.command)) 200 | try: 201 | # Generate the freeze file 202 | with open(os.devnull, u"w", encoding=u'utf-8') as devnull: 203 | subprocess.check_call(self.command, stdout=devnull) 204 | except subprocess.CalledProcessError: 205 | # We failed to generate the freeze file, abort 206 | self.log.error( 207 | u"Failed to generate freeze file \"%s\".", self.freeze_file) 208 | raise M4FreezeFileError(u"Failed to generate freeze file") 209 | else: 210 | # We successfully generated the freeze file 211 | self.log.debug(u"Successfully generated freeze file \"%s\".", 212 | self.freeze_file) 213 | 214 | def __del__(self): 215 | """Delete the freeze file""" 216 | try: 217 | os.remove(self.freeze_file) 218 | except OSError: 219 | self.log.debug(u"Trying to remove the freeze file " 220 | u"\"%s\"... failed!", self.freeze_file) 221 | else: 222 | self.log.debug(u"Trying to remove the freeze file " 223 | u"\"%s\"... done!", self.freeze_file) 224 | 225 | 226 | class M4Macro(object): 227 | 228 | """Class providing an abstraction for a m4 macro.""" 229 | operators = (u"ifelse(", u"incr(", u"decr(", u"errprint(") 230 | 231 | def __init__(self, name, expander, file_defined, args=[], comments=[]): 232 | # Check if we have enough data 233 | if (not name or not expander or not file_defined or args is None 234 | or comments is None): 235 | if not name: 236 | name = u"noname" 237 | if args is None: 238 | args = [u"noargs"] 239 | raise M4MacroError(u"Bad parameters for macro \"{}({})\"".format( 240 | name, u", ".join(args))) 241 | # Initialize the macro 242 | self._name = name 243 | self._expander = expander 244 | self._expansion = None 245 | self._expansion_static = None 246 | self._dump = None 247 | self._file_defined = file_defined 248 | self._args = args 249 | self._comments = comments 250 | 251 | @property 252 | def name(self): 253 | """Get the macro name.""" 254 | return self._name 255 | 256 | def expand(self, args=None): 257 | """Get the macro expansion using the provided arguments.""" 258 | # Check whether the M4 expansion is static (simple variable 259 | # substitution) or dynamic (ifelses, ...) 260 | if self._expansion_static is None: 261 | # If the macro definition contains any m4 operator 262 | if any(s in self.dump for s in M4Macro.operators): 263 | self._expansion_static = False 264 | else: 265 | self._expansion_static = True 266 | if self.nargs == 0: 267 | # Ignore supplied arguments 268 | # Macro without arguments: the expansion is static. Save it for 269 | # faster access 270 | if not self._expansion: 271 | self._expansion = self._expander.expand(self.name) 272 | return self._expansion 273 | if args is None: 274 | # Return the expansion as defined in the macro definition 275 | # Don't expand the macro with the placeholder arguments, as that 276 | # breaks on macros that expect numeric arguments 277 | # e.g. "gen_sens(N)" will not terminate if N is not a number 278 | return self.dump 279 | # Sanity check 280 | if len(args) != len(self.args): 281 | return None 282 | # Expand the macro 283 | if not self.expansion_static: 284 | # If the expansion is dynamic, we have to call m4 every time 285 | text = self.name + u"(" + u", ".join(args) + u")" 286 | return self._expander.expand(text) 287 | else: 288 | # If the expansion is static, we can call m4 only once, and then 289 | # perform Python string formatting all the other times. 290 | if not self._expansion: 291 | # Get the expansion with placeholders, if we don't have it 292 | placeholders = [] 293 | for i in range(self.nargs): 294 | placeholders.append(u"@@ARG{}@@".format(i)) 295 | tmp = self.name + u"(" + u", ".join(placeholders) + u")" 296 | expansion = self._expander.expand(tmp) 297 | # Double all curly brackets to make them literal 298 | expansion = re.sub(r"([{}])", r"\1\1", expansion) 299 | # Substitute @@ARGN@@ with {N} to format the argument for 300 | # Python string formatting 301 | self._expansion = re.sub( 302 | r"@@ARG([0-9]+)@@", r"{\1}", expansion) 303 | # Expand using the given arguments 304 | return self._expansion.format(*args) 305 | 306 | @property 307 | def expansion_static(self): 308 | """Check whether the macro expansion is static (simple string 309 | substitution) or dynamic (variable length depending on arguments).""" 310 | return self._expansion_static 311 | 312 | @property 313 | def dump(self): 314 | """Get the macro definition.""" 315 | if not self._dump: 316 | dump = self._expander.dump(self.name) 317 | # Remove the first line ("name:") 318 | self._dump = u"\n".join(dump.splitlines()[1:]) 319 | return self._dump 320 | 321 | @property 322 | def file_defined(self): 323 | """Get the file where the macro was defined.""" 324 | return self._file_defined 325 | 326 | @property 327 | def args(self): 328 | """Get the names of the macro arguments.""" 329 | return self._args 330 | 331 | @property 332 | def nargs(self): 333 | """Get the number of macro arguments.""" 334 | return len(self._args) 335 | 336 | @property 337 | def comments(self): 338 | """Get the macro comments as a list of lines""" 339 | return self._comments 340 | 341 | def __repr__(self): 342 | tmp = self.name 343 | if self.nargs > 0: 344 | tmp += u"(" + u", ".join(self.args) + u")" 345 | return tmp 346 | 347 | def __eq__(self, other): 348 | # If they have the same representation 349 | # And the same expansion 350 | # And they are defined in the same file 351 | # And they have the same number of arguments 352 | # And the same comments 353 | return str(self) == str(other)\ 354 | and self.expand() == other.expand()\ 355 | and self.file_defined == other.file_defined\ 356 | and self.nargs == other.nargs\ 357 | and len(self.comments) == len(other.comments)\ 358 | and u"".join(self.comments) == u"".join(other.comments) 359 | 360 | def __ne__(self, other): 361 | return not self == other 362 | 363 | 364 | class MacroInPolicy(object): 365 | 366 | """Class providing an abstraction for an usage of a m4 macro.""" 367 | 368 | @staticmethod 369 | def parse_usage(string): 370 | """Parse a macro usage to extract the name and arguments. 371 | 372 | Return a tuple (name, [args]).""" 373 | i = string.index(u"(") 374 | name = string[:i] 375 | args = [x.strip() for x in string[i:].strip(u"()").split(u",")] 376 | return (name, args) 377 | 378 | def __init__(self, existing_macros, file_used, line_used, name, args=[]): 379 | # Check that we have enough data 380 | if (not existing_macros or not file_used or line_used is None or 381 | not isinstance(line_used, int) or int(line_used) < 0 or 382 | not name or args is None): 383 | if not name: 384 | name = u"noname" 385 | if args is None: 386 | args = [u"noargs"] 387 | raise M4MacroError( 388 | u"Bad parameters for macro \"{}({})\"".format( 389 | name, u", ".join(args))) 390 | # Initialize the macro 391 | # If the macro is a valid macro 392 | if name in existing_macros and existing_macros[name]\ 393 | and existing_macros[name].nargs == len(args): 394 | # Link this macro usage with the macro definition 395 | self.macro = existing_macros[name] 396 | # Record the specific arguments for this macro usage 397 | self._args = args 398 | # Record the file for this usage 399 | self._file_used = file_used 400 | # Record the line number for this usage 401 | self._line_used = int(line_used) 402 | else: 403 | raise M4MacroError( 404 | u"Invalid macro \"{}({})\"".format(name, ", ".join(args))) 405 | 406 | @property 407 | def name(self): 408 | """Get the macro name""" 409 | return self.macro.name 410 | 411 | @property 412 | def expansion(self): 413 | """Get the macro expansion using the specific usage's arguments.""" 414 | # If we have not generated the macro expansion yet, generate it on the 415 | # fly and save it for future use 416 | if not hasattr(self, u"_expansion"): 417 | # TODO: warning: expand() can return None if the number of 418 | # arguments is wrong. This should not happen, maybe put some more 419 | # checks to be sure? 420 | self._expansion = self.macro.expand(self.args) 421 | self._expansion_linelen = len(self._expansion.splitlines()) 422 | return self._expansion 423 | 424 | @property 425 | def expansion_linelen(self): 426 | """Get the length in lines of the macro expansion with the specific 427 | arguments.""" 428 | # If we have not generated the macro expansion yet, generate it on the 429 | # fly and save it for future use 430 | if not hasattr(self, u"_expansion"): 431 | # TODO: warning: expand() can return None if the number of 432 | # arguments is wrong. This should not happen, maybe put some more 433 | # checks to be sure? 434 | self._expansion = self.macro.expand(self.args) 435 | self._expansion_linelen = len(self._expansion.splitlines()) 436 | return self._expansion_linelen 437 | 438 | @property 439 | def file_used(self): 440 | """Get the file where the macro was used""" 441 | return self._file_used 442 | 443 | @property 444 | def line_used(self): 445 | """Get the line where the macro was used""" 446 | return self._line_used 447 | 448 | @property 449 | def args(self): 450 | """Get the names of the macro arguments""" 451 | return self._args 452 | 453 | @property 454 | def args_descriptions(self): 455 | """Get the descriptions of the macro arguments""" 456 | return self.macro.args 457 | 458 | @property 459 | def nargs(self): 460 | """Get the number of macro arguments""" 461 | return len(self._args) 462 | 463 | def __repr__(self): 464 | tmp = self.name 465 | if self.nargs > 0: 466 | tmp += u"(" + u", ".join(self.args) + u")" 467 | return tmp 468 | 469 | def __eq__(self, other): 470 | # If they are a usage of the same M4Macro instance 471 | # With the same arguments 472 | # And with the same expansion (sort of redundant check) 473 | # Used in the same place 474 | return self.macro == other.macro\ 475 | and self.nargs == other.nargs\ 476 | and self.expansion == other.expansion\ 477 | and self.file_used == other.file_used\ 478 | and self.line_used == other.line_used 479 | 480 | def __ne__(self, other): 481 | return not self == other 482 | -------------------------------------------------------------------------------- /policysource/macro_plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # This file is part of the policysource library. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as 9 | # published by the Free Software Foundation, either version 2.1 of 10 | # the License, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program. If not, see 19 | # . 20 | # 21 | u"""Plugin module implementing file-specific macro parsing functions""" 22 | 23 | # Necessary for Python 2/3 compatibility 24 | from __future__ import absolute_import 25 | from future.utils import itervalues 26 | 27 | import os 28 | from os import path 29 | import sys 30 | import keyword 31 | import inspect 32 | import logging 33 | import policysource.macro 34 | 35 | 36 | __all__ = [] 37 | __plugins__ = {} 38 | for plugin_file in os.listdir(os.path.dirname(__file__)): 39 | if plugin_file.endswith(u".py"): 40 | module = os.path.splitext(plugin_file)[0] 41 | if not module.startswith(u'_') and not keyword.iskeyword(module): 42 | try: 43 | __import__(__name__ + u'.' + module) 44 | except: 45 | e = sys.exc_info() 46 | print(e) 47 | else: 48 | __all__.append(module) 49 | __plugins__[module] = locals()[module] 50 | __all__.sort() 51 | 52 | 53 | class M4MacroParser(object): 54 | 55 | u"""Class providing a m4 file parser. 56 | 57 | The class handles a list of specific macro files through a plugin 58 | architecture defined in the macro_plugin module.""" 59 | 60 | def __init__(self, tmpdir=None, extra_defs=None): 61 | u"""Initialize plugin architecture. 62 | 63 | Find all plugins offered by macro_plugins, check that they implement 64 | the required methods and add them to the plugin dictionary. 65 | 66 | The parser will need a working directory. If the user does not supply a 67 | valid one, the parser will create a temporary directory, which will be 68 | destroyed with the object. 69 | If the user supplies a valid one, it will be up to the user to manage 70 | its lifecycle.""" 71 | # Setup logger 72 | self.log = logging.getLogger(self.__class__.__name__) 73 | # Setup plugins 74 | self.plugins = {} 75 | for mod in __all__: 76 | plugin = __plugins__[mod] 77 | if (inspect.isfunction(plugin.expects) 78 | and inspect.isfunction(plugin.parse)): 79 | self.plugins[mod] = plugin 80 | self.log.debug(u"Found plugin \"%s\"", mod) 81 | else: 82 | self.log.debug(u"Invalid plugin \"%s\"", mod) 83 | # Setup temporary directory passthrough 84 | self._tmpdir = tmpdir 85 | self._tmpdir_managed = False 86 | # Setup macro expander variable 87 | self.macro_expander = None 88 | self.extra_defs = extra_defs 89 | 90 | @property 91 | def tmpdir(self): 92 | u"""Get the temporary directory used by the parser.""" 93 | return self._tmpdir 94 | 95 | @property 96 | def tmpdir_managed(self): 97 | u"""Check if the temporary directory is managed by the parser.""" 98 | return self._tmpdir_managed 99 | 100 | def __get_parser__(self, single_file): 101 | u"""Find the appropriate parser for the given file.""" 102 | for plg in itervalues(self.plugins): 103 | if plg.expects(single_file): 104 | return plg 105 | return None 106 | 107 | def __parse_file__(self, single_file, parser): 108 | u"""Parse a single file""" 109 | f_macros = None 110 | try: 111 | # Parse the file using the appropriate parser 112 | f_macros = parser.parse(single_file, self.macro_expander) 113 | except ValueError as e: 114 | # This really should not happen, since we have already 115 | # checked that the plugin accepts the file. 116 | # Log and skip 117 | self.log.warning(u"%s", e) 118 | self.log.warning(u"Could not parse \"%s\"", single_file) 119 | else: 120 | # File parsed successfully 121 | self.log.info(u"Parsed macros from \"%s\"", single_file) 122 | return f_macros 123 | 124 | def expects(self): 125 | u"""Returns a list of files that the parser can handle.""" 126 | return self.plugins.keys() 127 | 128 | def parse(self, files): 129 | u"""Parses a list of files and returns a dictionary of macros.""" 130 | # Setup the M4MacroExpander 131 | try: 132 | self.macro_expander = policysource.macro.M4MacroExpander( 133 | files, self.tmpdir, self.extra_defs) 134 | except policysource.macro.M4MacroExpanderError as e: 135 | self.log.error(u"%s", e.message) 136 | macros = None 137 | else: 138 | # Parse each file, using the macro expander 139 | macros = {} 140 | for single_file in files: 141 | # Find the appropriate parser 142 | parser = self.__get_parser__(single_file) 143 | if parser: 144 | # We have a parser for this file 145 | self.log.debug(u"Parsing macros from \"%s\" with plugin " 146 | u"\"%s\"", single_file, parser.__name__) 147 | # Parse this file and obtain a dictionary of macros 148 | f_macros = self.__parse_file__(single_file, parser) 149 | if f_macros: 150 | # Update the global macro dictionary 151 | macros.update(f_macros) 152 | else: 153 | # We don't have a parser for this file 154 | self.log.debug(u"No parser for \"%s\"", single_file) 155 | return macros 156 | -------------------------------------------------------------------------------- /policysource/macro_plugins/global_macros.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # This file is part of the policysource library. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as 9 | # published by the Free Software Foundation, either version 2.1 of 10 | # the License, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program. If not, see 19 | # . 20 | # 21 | """Plugin to parse the global_macros file""" 22 | 23 | # Necessary for Python 2/3 compatibility 24 | from __future__ import absolute_import 25 | from io import open 26 | 27 | import os 28 | import re 29 | import logging 30 | import policysource.macro 31 | 32 | MACRO_FILE = u"global_macros" 33 | LOG = logging.getLogger(__name__) 34 | 35 | 36 | def expects(expected_file): 37 | """Return True/False depending on whether the plugin can handle the file""" 38 | return expected_file and os.path.basename(expected_file) == MACRO_FILE 39 | 40 | 41 | def parse(f_to_parse, macro_expander): 42 | """Parse the file and return a dictionary of macros. 43 | 44 | Raise ValueError if unable to handle the file.""" 45 | # Check that we can handle the file we're served 46 | if not f_to_parse or not expects(f_to_parse): 47 | raise ValueError(u"{} can't handle {}.".format(MACRO_FILE, f_to_parse)) 48 | macros = {} 49 | # Parse the global_macros file 50 | macrodef = re.compile(r'^define\(\`([^\']+)\',\s+`([^\']+)\'') 51 | with open(f_to_parse, encoding=u'utf-8') as global_macros_file: 52 | for lineno, line in enumerate(global_macros_file): 53 | # If the line contains a macro, parse it 54 | macro_match = macrodef.search(line) 55 | if macro_match is not None: 56 | # Construct the new macro object 57 | name = macro_match.group(1) 58 | try: 59 | new_macro = policysource.macro.M4Macro( 60 | name, macro_expander, f_to_parse) 61 | except policysource.macro.M4MacroError as e: 62 | # Log the failure and skip 63 | # Find the macro line and report it to the user 64 | LOG.warning(u"%s", e.message) 65 | LOG.warning(u"Macro \"%s\" is at %s:%s", 66 | name, f_to_parse, lineno) 67 | else: 68 | # Add the new macro to the dictionary 69 | macros[name] = new_macro 70 | return macros 71 | -------------------------------------------------------------------------------- /policysource/macro_plugins/te_macros.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # This file is part of the policysource library. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as 9 | # published by the Free Software Foundation, either version 2.1 of 10 | # the License, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program. If not, see 19 | # . 20 | # 21 | """Plugin to parse the te_macros file""" 22 | 23 | # Necessary for Python 2/3 compatibility 24 | from __future__ import absolute_import 25 | from io import open 26 | 27 | import os 28 | import re 29 | import logging 30 | import policysource.macro 31 | 32 | MACRO_FILE = u"te_macros" 33 | LOG = logging.getLogger(__name__) 34 | MDL = r"^#\s[a-zA-Z][a-zA-Z0-9_]*\((?:[a-zA-Z0-9_]+,\s?)*(?:[a-zA-Z0-9_]+)\)$" 35 | BLK_SEP = r"^##+$" 36 | 37 | 38 | class TEBlock(object): 39 | """Helper class to handle a macro block in the te_macros file.""" 40 | 41 | def __init__(self, start, end, content): 42 | """Construct a TEBlock object. 43 | 44 | Keyword arguments: 45 | start -- The starting line index, indexed from 0 46 | end -- The non-inclusive end line index, indexed from 0 47 | content -- A list containing the block lines, i.e. lines[start, end) 48 | """ 49 | # Sanity check 50 | if start + len(content) != end: 51 | raise ValueError(u"Invalid range for the supplied content.") 52 | self._start = start 53 | self._end = end 54 | self._content = content 55 | # Check if the macro definition line is correct 56 | if re.match(MDL, content[1]): 57 | self._valid = True 58 | # Tokenize the macro definition line, removing empy tokens 59 | # "# macro(arg1,arg2)" -> ["macro", "arg1", "arg2"] 60 | definition = [x for x in re.split(r'\W+', content[1]) if x] 61 | self._name = definition[0] # ["macro"] 62 | self._args = definition[1:] # ["arg1", "arg2"] 63 | else: 64 | self._valid = False 65 | # Get comments 66 | self._comments = [] 67 | for line in content[1:]: 68 | if line.startswith('#'): 69 | self._comments.append(line) 70 | 71 | def start(self, line_number=True): 72 | """Return the starting position of the block. 73 | 74 | Keyword arguments: 75 | line_number -- if True, return the line number in the file, 76 | i.e. indexed from 1. 77 | if False, return the index in the array, 78 | i.e. indexed from 0.""" 79 | if line_number: 80 | return self._start + 1 81 | else: 82 | return self._start 83 | 84 | def end(self, line_number=True, inclusive=False): 85 | """Return the ending position of the block. 86 | 87 | Keyword arguments: 88 | line_number -- if True, return the line number in the file, 89 | i.e. indexed from 1. 90 | if False, return the index in the array, 91 | i.e. indexed from 0. 92 | inclusive -- if True, return an inclusive value, i.e. end] 93 | if False, return a non inclusive value, i.e. end)""" 94 | pos = self._end 95 | if line_number: 96 | pos += 1 97 | if inclusive: 98 | pos -= 1 99 | return pos 100 | 101 | def is_valid(self): 102 | """Check if the block contains the correct macro definition line 103 | # name(arg0, arg1, ...)""" 104 | return self._valid 105 | 106 | @property 107 | def mdl(self): 108 | """Return the macro definition line.""" 109 | if self.is_valid(): 110 | return self.content[1] 111 | else: 112 | return None 113 | 114 | @property 115 | def name(self): 116 | """Return the macro name. 117 | 118 | If the macro is not valid the name may be None.""" 119 | return self._name 120 | 121 | @property 122 | def args(self): 123 | """Return a list containing the macro args. 124 | 125 | If the macro is not valid, the list may be None.""" 126 | return self._args 127 | 128 | @property 129 | def nargs(self): 130 | """Return the number of macro arguments.""" 131 | return len(self.args) 132 | 133 | @property 134 | def content(self): 135 | """Return the full block content as a list of lines.""" 136 | return self._content 137 | 138 | @property 139 | def comments(self): 140 | """Return the comments in the block.""" 141 | return self._comments 142 | 143 | def __len__(self): 144 | return len(self.content) 145 | 146 | def __repr__(self): 147 | return "\n".join(self.content) 148 | 149 | 150 | def expects(expected_file): 151 | """Return True/False depending on whether the plugin can handle the file""" 152 | return expected_file and os.path.basename(expected_file) == MACRO_FILE 153 | 154 | 155 | def __split__(file_lines): 156 | """Split the file in blocks.""" 157 | blocks = [] 158 | start = 0 159 | previous_is_empty = False 160 | for i, line in enumerate(file_lines): 161 | if line == u"": 162 | # Mark if we find a blank line 163 | previous_is_empty = True 164 | elif previous_is_empty: 165 | if re.match(BLK_SEP, line): 166 | # The current line is a block separator following an empty line 167 | # We have a block behind us, process it 168 | # List slicing syntax: list[start:end] means [start, end) 169 | tmpblk = TEBlock(start, i, file_lines[start:i]) 170 | blocks.append(tmpblk) 171 | LOG.debug(u"Found block at lines %d-%d (inclusive)", 172 | tmpblk.start(), tmpblk.end(inclusive=True)) 173 | # Mark the start of the new block 174 | start = i 175 | # Reset the previous empty line 176 | previous_is_empty = False 177 | # Handle the last block 178 | tmpblk = TEBlock(start, len(file_lines), file_lines[start:]) 179 | blocks.append(tmpblk) 180 | LOG.debug(u"Found block at lines %d-%d (inclusive)", 181 | tmpblk.start(), tmpblk.end(inclusive=True)) 182 | return blocks 183 | 184 | 185 | def parse(f_to_parse, macro_expander): 186 | """Parse the file and return a dictionary of macros. 187 | 188 | Raise ValueError if unable to handle the file.""" 189 | # Check that we can handle the file we're served 190 | if not f_to_parse or not expects(f_to_parse): 191 | raise ValueError(u"{} can't handle {}.".format(MACRO_FILE, f_to_parse)) 192 | macros = {} 193 | macrodef = re.compile(r'^define\(\`([^\']+)\'') 194 | macroargs = re.compile(r'\$[0-9]+') 195 | # Parse the te_macros file 196 | # Read the te_macros file in as a list of lines 197 | with open(f_to_parse, encoding=u'utf-8') as ftp: 198 | file_lines = ftp.read().splitlines() 199 | # Split the file in blocks 200 | blocks = __split__(file_lines) 201 | # Initialize the M4Macro objects from the blocks 202 | for block in blocks: 203 | if not block.is_valid(): 204 | # Process an invalid blocks 205 | invalid_block_macros = set() 206 | for line in block.content: 207 | macro_match = macrodef.search(line) 208 | if macro_match: 209 | invalid_block_macros.add(macro_match.group(1)) 210 | for m in invalid_block_macros: 211 | dump = macro_expander.dump(m) 212 | args = list(set(macroargs.findall(dump))) 213 | try: 214 | new_macro = policysource.macro.M4Macro(m, 215 | macro_expander, 216 | f_to_parse, 217 | args, 218 | block.comments) 219 | except policysource.macro.M4MacroError as e: 220 | # Log the failure and skip 221 | LOG.warning(u"%s", e.message) 222 | else: 223 | # Add the macro to the macro dictionary 224 | macros[m] = new_macro 225 | else: 226 | # Process a valid block 227 | try: 228 | new_macro = policysource.macro.M4Macro(block.name, 229 | macro_expander, 230 | f_to_parse, block.args, 231 | block.comments) 232 | except policysource.macro.M4MacroError as e: 233 | # Log the failure and skip 234 | LOG.warning(u"%s", e.message) 235 | else: 236 | # Add the macro to the macro dictionary 237 | macros[block.name] = new_macro 238 | return macros 239 | -------------------------------------------------------------------------------- /policysource/policy.py: -------------------------------------------------------------------------------- 1 | # 2 | # Written by Filippo Bonazzi 3 | # Copyright (C) 2016 Aalto University 4 | # 5 | # This file is part of the policysource library. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as 9 | # published by the Free Software Foundation, either version 2.1 of 10 | # the License, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program. If not, see 19 | # . 20 | # 21 | """Class providing an abstraction for a source SEAndroid policy""" 22 | 23 | # Necessary for Python 2/3 compatibility 24 | from __future__ import absolute_import 25 | from builtins import range 26 | from io import open 27 | 28 | from tempfile import mkdtemp 29 | import subprocess 30 | import os.path 31 | import re 32 | import logging 33 | import setools 34 | import setools.policyrep 35 | from policysource.macro import MacroInPolicy, M4MacroError 36 | import policysource.mapping 37 | import policysource.macro_plugins 38 | 39 | 40 | class SourcePolicy(object): 41 | """Class representing a source SELinux policy.""" 42 | # pylint: disable=too-many-instance-attributes 43 | regex_macrodef = re.compile(r'^define\(\`([^\']+)\',') 44 | regex_usageargstring = r'(\(.*\));?' 45 | 46 | def __init__(self, policyfiles, extra_defs, load_neverallows=False): 47 | """Construct a SourcePolicy object by parsing the supplied files. 48 | 49 | Keyword arguments: 50 | policyfiles -- The policy files as a list of absolute paths.""" 51 | # Setup logging 52 | self.log = logging.getLogger(self.__class__.__name__) 53 | # Setup useful infrastructure 54 | self._policyconf = None 55 | # Set up a general-purpose macro expander for internal use 56 | self._expander = None 57 | # Set the extra M4 defs ("-D target_build_variant=user", ...) 58 | self.extra_defs = extra_defs 59 | # Create a temporary work directory 60 | self._tmpdir = mkdtemp() 61 | self.log.debug(u"Created temporary directory \"%s\".", self._tmpdir) 62 | 63 | # Get a list of policy files with full paths 64 | self._policy_files = policyfiles 65 | if not self._policy_files: 66 | raise RuntimeError( 67 | u"Could not find any policy files to parse, aborting...") 68 | # Parse the macros and macro usages in the policy 69 | self._macro_defs = self.__find_macro_defs__(self._policy_files) 70 | if self._macro_defs is None: 71 | raise RuntimeError(u"Error parsing macro definitions, aborting...") 72 | self._macro_usages = self.__find_macro_usages__(self._policy_files, 73 | self._macro_defs) 74 | if self._macro_usages is None: 75 | raise RuntimeError(u"Error parsing macro usages, aborting...") 76 | # Create the policyconf 77 | self._policyconf = self.__create_policyconf__(self._policy_files) 78 | if not self._policyconf: 79 | raise RuntimeError( 80 | u"Could not create the policy.conf file, aborting...") 81 | # Create the actual policy instance 82 | self._policy = setools.policyrep.SELinuxPolicy(self.policyconf) 83 | # Initialise some useful variables 84 | self._attributes = self.__compute_attributes() 85 | self._types = self.__compute_types() 86 | self._classes = self.__compute_classes() 87 | # Build the origin file/line mapping 88 | mapper = policysource.mapping.Mapper(self.policyconf, self.attributes, 89 | self.types, self.classes) 90 | self._mapping = mapper.get_mapping(load_neverallows) 91 | if not self._mapping: 92 | raise RuntimeError( 93 | u"Error creating the file/line mapping, aborting...") 94 | 95 | def __del__(self): 96 | # Remove the macro usages and definitions to remove the associated 97 | # temporary files 98 | del self._macro_usages 99 | del self._macro_defs 100 | # Delete the policy.conf file 101 | if self.policyconf: 102 | try: 103 | os.remove(self.policyconf) 104 | except OSError: 105 | self.log.warning(u"Trying to remove policy.conf file \"%s\"..." 106 | u" failed!", self.policyconf) 107 | else: 108 | self.log.debug(u"Trying to remove policy.conf file \"%s\"... " 109 | u"done!", self.policyconf) 110 | # Try to remove the temporary directory 111 | if self._tmpdir: 112 | try: 113 | os.rmdir(self._tmpdir) 114 | except OSError: 115 | self.log.warning(u"Trying to remove the temporary directory " 116 | u"\"%s\"... failed!", self._tmpdir) 117 | else: 118 | self.log.debug(u"Trying to remove the temporary directory " 119 | u"\"%s\"... done!", self._tmpdir) 120 | 121 | def __create_policyconf__(self, policy_files): 122 | """Process the separate policy files with m4 and return a single 123 | policy.conf file""" 124 | # Prepare the output file 125 | policyconf = os.path.join(self._tmpdir, u"policy.conf") 126 | # Prepare the m4 command line 127 | command = [u'm4'] 128 | for definition in self.extra_defs: 129 | command.extend([u"-D", definition]) 130 | command.extend([u'-s']) 131 | command.extend(policy_files) 132 | # Try to run m4 133 | try: 134 | with open(policyconf, u"w", encoding=u'utf-8') as pcf: 135 | subprocess.check_call(command, stdout=pcf) 136 | except subprocess.CalledProcessError as e: 137 | self.log.error(e.message) 138 | self.log.error( 139 | u"Could not create the policy.conf \"%s\" file", policyconf) 140 | policyconf = None 141 | return policyconf 142 | 143 | def __find_macro_files__(self, policy_files): 144 | """Find files that contain m4 macro definitions.""" 145 | # Regex to match the macro definition string 146 | macro_files = [] 147 | for single_file in policy_files: 148 | with open(single_file, u'r', encoding=u'utf-8') as macro_file: 149 | for line in macro_file: 150 | # If this file contains at least one macro definition, 151 | # append it to the list of macro files and skip to 152 | # the next policy file 153 | if self.regex_macrodef.search(line): 154 | macro_files.append(single_file) 155 | break 156 | return macro_files 157 | 158 | def __find_macro_defs__(self, policy_files): 159 | """Get a dictionary containing all the m4 macros defined in the files. 160 | 161 | The dictionary maps the macro name to a M4Macro object.""" 162 | macro_files = self.__find_macro_files__(policy_files) 163 | parser = policysource.macro_plugins.M4MacroParser( 164 | tmpdir=None, extra_defs=self.extra_defs) 165 | macros = parser.parse(macro_files) 166 | self._expander = parser.macro_expander 167 | return macros 168 | 169 | @staticmethod 170 | def __split_macro_usage_args__(argstring): 171 | """Return a list of macro usage arguments, respecting grouping by 172 | curly brackets and m4-style quotes. 173 | 174 | e.g. The following argstring 175 | "({ appdomain, -isolated_app }, something, `third argument')" 176 | would be split in 3 arguments as such 177 | ["{ appdomain, -isolated_app }", "something", "`third argument'"] 178 | """ 179 | group = u"" 180 | args = [] 181 | nested_curly = 0 182 | nested_quotes = 0 183 | nested_parentheses = 0 184 | for c in argstring: 185 | # Found opening parenthesis 186 | if c == u"(": 187 | # If this is the outermost parenthesis, drop it 188 | # Otherwise keep it 189 | if nested_quotes or nested_curly or nested_parentheses: 190 | group += c 191 | # If we are outside nested quotes or brackets, this is a 192 | # special character 193 | if not nested_quotes and not nested_curly: 194 | nested_parentheses += 1 195 | # Found opening curly bracket 196 | elif c == u"{": 197 | # If we are outside nested quotes, this is a special character 198 | if not nested_quotes: 199 | # Increase nest level 200 | nested_curly += 1 201 | group += c 202 | # Found opening quote 203 | elif c == u"`": 204 | # If we are outside nested curly brackets, this is a special 205 | # character 206 | if not nested_curly: 207 | # Increase nested level 208 | nested_quotes += 1 209 | group += c 210 | # Found closing curly bracket 211 | elif c == u"}": 212 | # If we are outside nested quotes, this is a special character 213 | if not nested_quotes: 214 | # Decrease nested level 215 | nested_curly -= 1 216 | if nested_curly < 0: 217 | # Mismatched brackets 218 | return None 219 | group += c 220 | # Found closing quote 221 | elif c == u"'": 222 | # If we are outside nested curly brackets, this is a special 223 | # character 224 | if not nested_curly: 225 | # Decrease nested level 226 | nested_quotes -= 1 227 | if nested_quotes < 0: 228 | # Mismatched quotes 229 | return None 230 | group += c 231 | # Found closing parenthesis 232 | elif c == u")": 233 | # If we are outside nested quotes or brackets, this is a 234 | # special character 235 | if not nested_quotes and not nested_curly: 236 | nested_parentheses -= 1 237 | # If this is the outermost parenthesis, drop it 238 | # Otherwise keep it 239 | if nested_quotes or nested_curly or nested_parentheses > 0: 240 | group += c 241 | elif nested_parentheses == 0: 242 | # Found outermost closing parenthesis, end of arguments 243 | break 244 | elif nested_parentheses < 0: 245 | # Mismatched parentheses 246 | return None 247 | # Found comma 248 | elif c == u",": 249 | # If we are outside nested curly brackets and quotes, this is 250 | # the separator character 251 | if not nested_curly and not nested_quotes: 252 | # Append the group as a new argument 253 | args.append(group) 254 | # Initialize a new empty group 255 | group = u"" 256 | else: 257 | # Append to the group as a regular character 258 | group += c 259 | # Found space 260 | elif c == u" ": 261 | # If we are outside nested curly brackets and quotes, discard 262 | # spaces 263 | if nested_curly or nested_quotes: 264 | group += c 265 | # Found generic character 266 | else: 267 | group += c 268 | # Save the last block 269 | args.append(group) 270 | return args 271 | 272 | def __get_macro_usage_args__(self, macro, line): 273 | """Get the current macro arguments""" 274 | # The macro is supposed to have nargs arguments 275 | if macro.nargs > 0: 276 | # Check if it is actually used with all its arguments 277 | # Get the usage argstring (whatever is between the parentheses) 278 | usage_r = macro.name + self.regex_usageargstring 279 | tmp = re.match(usage_r, line) 280 | if tmp: 281 | # Get the arguments 282 | args = self.__split_macro_usage_args__(tmp.group(1)) 283 | # Bad usage 284 | if len(args) != macro.nargs: 285 | args = None 286 | else: 287 | # Special case: multiline macros (e.g. "eng(` \n ... \n')") 288 | if u"{}(`".format(macro.name) in line: 289 | args = [] 290 | for i in range(macro.nargs): 291 | args.append(u"multiline") 292 | else: 293 | args = None 294 | else: 295 | # Macro without arguments 296 | args = [] 297 | return args 298 | 299 | def __find_macro_usages__(self, policy_files, macros): 300 | """Get a list of all the m4 macros used in the supplied files. 301 | 302 | The list contains MacroInPolicy objects.""" 303 | macro_usages = [] 304 | for current_file in (x for x in policy_files if x.endswith(u".te")): 305 | # For each .te file 306 | with open(current_file, encoding=u'utf-8') as current_file_content: 307 | for lineno, line in enumerate(current_file_content, 1): 308 | # Remove extra whitespace 309 | line = line.strip() 310 | if line.startswith(u"#"): 311 | # Ignore comments 312 | continue 313 | # Strip end-of-line comments 314 | if u"#" in line: 315 | line = line.split(u"#")[0].strip() 316 | # Search for macros 317 | stripped_line = str(line) 318 | for word in re.split(r'\W+', line): 319 | # Handle usage of the same macro multiple times on the 320 | # same line: only parse the first occurrence, and 321 | # remove each occurrence from the string after parsing 322 | # Find the index of the word in the string 323 | word_index = stripped_line.index(word) 324 | # Discard everything before that 325 | stripped_line = stripped_line[word_index:] 326 | if word in macros: 327 | # We have found a macro 328 | # Get the arguments 329 | args = self.__get_macro_usage_args__( 330 | macros[word], stripped_line) 331 | if args is None: 332 | # The macro usage is not valid 333 | self.log.warning(u"\"%s\" is a macro name but " 334 | u"it is used wrong at:", word) 335 | self.log.warning(u"%s:%s: %s", current_file, 336 | lineno, line.rstrip()) 337 | continue 338 | # Construct the new macro object 339 | try: 340 | n_m = MacroInPolicy(macros, current_file, 341 | lineno, word, args) 342 | except M4MacroError as e: 343 | # Bad macro, skip 344 | self.log.warning(u"%s", e.message) 345 | else: 346 | # Add the new macro to the list 347 | macro_usages.append(n_m) 348 | return macro_usages 349 | 350 | def __compute_attributes(self): 351 | """Get the SELinuxPolicy attributes as a dictionary of sets. 352 | 353 | Return a dictionary (attribute, set(types)).""" 354 | attributes = {} 355 | for attr in self.policy.typeattributes(): 356 | attributes[str(attr)] = set(str(x) for x in attr.expand()) 357 | return attributes 358 | 359 | def __compute_types(self): 360 | """Get the set of SELinuxPolicy types as a set of strings.""" 361 | types = set() 362 | for tpe in self.policy.types(): 363 | types.add(str(tpe)) 364 | return types 365 | 366 | def __compute_classes(self): 367 | """Get the SELinuxPolicy classes as a dictionary of sets. 368 | 369 | Return a dictionary (class, set(perms)). 370 | Each set contains all the permissions for the associated class, 371 | both inherited from commons and directly assigned.""" 372 | classes = {} 373 | for cls in self.policy.classes(): 374 | try: 375 | cmn = cls.common 376 | except setools.policyrep.exception.NoCommon: 377 | cmnset = cls.perms 378 | else: 379 | cmnset = cls.perms.union(self.policy.lookup_common(cmn).perms) 380 | classes[str(cls)] = cmnset 381 | return classes 382 | 383 | @property 384 | def macro_defs(self): 385 | """Get the macros defined in the policy source.""" 386 | return self._macro_defs 387 | 388 | @property 389 | def macro_usages(self): 390 | """Get the macros used in the policy source.""" 391 | return self._macro_usages 392 | 393 | @property 394 | def policy(self): 395 | """Get the SELinuxPolicy policy.""" 396 | return self._policy 397 | 398 | @property 399 | def policyconf(self): 400 | """Get the path to the policy.conf file.""" 401 | return self._policyconf 402 | 403 | @property 404 | def attributes(self): 405 | """Get the SELinuxPolicy attributes. 406 | 407 | Returns a a dictionary (attr, set(types)).""" 408 | return self._attributes 409 | 410 | @property 411 | def types(self): 412 | """Get the SELinuxPolicy types. 413 | 414 | Returns a set of types.""" 415 | return self._types 416 | 417 | @property 418 | def classes(self): 419 | """Get the SELinuxPolicy security classes. 420 | 421 | Returns a dictionary (class, set(permissions))""" 422 | return self._classes 423 | 424 | @property 425 | def mapping(self): 426 | """Get the mapping between policy rules and origin file/line. 427 | 428 | Return a Mapping object.""" 429 | return self._mapping 430 | -------------------------------------------------------------------------------- /selint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Written by Filippo Bonazzi 4 | # Copyright (C) 2016 Aalto University 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | """TODO: file docstring""" 19 | 20 | # Necessary for Python 2/3 compatibility 21 | from __future__ import absolute_import 22 | 23 | import os 24 | import os.path 25 | import sys 26 | import argparse 27 | import logging 28 | import imp 29 | import shutil 30 | # Match filenames 31 | import fnmatch 32 | 33 | import policysource 34 | import policysource.policy 35 | import plugins 36 | 37 | 38 | def get_policy_files(config): 39 | u"""Get a list of policy files given a configuration module with some 40 | specific variables.""" 41 | # Check the base Android tree directory 42 | if not hasattr(config, u"BASE_DIR_GLOBAL"): 43 | # If the config file does not contain the base directory variable 44 | logging.critical(u"Configuration file is missing BASE_DIR_GLOBAL " 45 | u"variable") 46 | return None 47 | if not config.BASE_DIR_GLOBAL: 48 | # If the directory is None or the name is empty 49 | logging.critical(u"Bad BASE_DIR_GLOBAL variable.") 50 | return None 51 | # Expand and sanitize the base Android tree directory name 52 | FULL_BASE_DIR = os.path.abspath(os.path.expanduser(config.BASE_DIR_GLOBAL)) 53 | # If the directory does not exist or is not traversable/readable 54 | if (not os.path.isdir(FULL_BASE_DIR) 55 | or not os.access(FULL_BASE_DIR, os.X_OK | os.R_OK)): 56 | logging.critical(u"Bad BASE_DIR_GLOBAL value: \"%s\" does not exist " 57 | u"or is not readable/traversable!", FULL_BASE_DIR) 58 | return None 59 | # Check that we have at least one file/directory 60 | if not hasattr(config, u"POLICY_DIRS"): 61 | logging.critical(u"Configuration file is missing POLICY_DIRS " 62 | u"variable: no policy directories specified.") 63 | return None 64 | # If the POLICY_DIRS variable is empty or contains empty tuples 65 | if (not config.POLICY_DIRS or not any(x if isinstance(x, str) else x[0] 66 | for x in config.POLICY_DIRS)): 67 | logging.critical( 68 | u"Bad POLICY_DIRS value: no policy directories specified.") 69 | return None 70 | ########################################################################### 71 | # Pick up all the files from the supplied directories 72 | all_files = [] 73 | for x in config.POLICY_DIRS: 74 | list_recursive = False 75 | # Check that the directory exists and is readable, traversable 76 | # Compute the absolute path 77 | # If this is a simple non-empty string, just use the string 78 | if isinstance(x, str) and x: 79 | fp = os.path.abspath(os.path.join(FULL_BASE_DIR, x)) 80 | # If this is a tuple of len=2, made up of a non-empty string and a 81 | # boolean, use only the string 82 | elif isinstance(x, tuple) and len(x) == 2 and\ 83 | isinstance(x[0], str) and isinstance(x[1], bool) and x[0]: 84 | fp = os.path.abspath(os.path.join(FULL_BASE_DIR, x[0])) 85 | list_recursive = x[1] 86 | # If the directory does not exist or is not readable/traversable 87 | if not os.path.isdir(fp) or not os.access(fp, os.X_OK | os.R_OK): 88 | logging.critical(u"Directory \"%s\" does not exist or is not " 89 | u"readable/traversable!", fp) 90 | # Process the next directory 91 | continue 92 | # If a recursive list has been requested, walk the subtree 93 | if list_recursive: 94 | # List all directories under fp, and all files under each, get 95 | # their full name and add them to all_files 96 | all_files.extend(sorted([os.path.join(dp, f) 97 | for dp, dn, fn in os.walk(fp) 98 | for f in fn])) 99 | # If no recursive listing has been requested, just list the directory 100 | else: 101 | # List all files in the directory, get their full name and add them 102 | # to all_files 103 | all_files.extend(sorted([os.path.join(fp, f) 104 | for f in os.listdir(fp)])) 105 | ####################################################################### 106 | # Get all the supplied filenames 107 | supplied_files = [x for x in config.POLICY_FILES if x] 108 | # Filter all_files, keep only files whose name is in supplied_files 109 | filtered_files = [] 110 | # Iterate over supplied_files to maintain the order in which the filenames 111 | # have been specified (important) 112 | for sf in supplied_files: 113 | for f in all_files: 114 | # Compute the basename of the file 115 | bn = os.path.basename(f) 116 | # If a file matches the supplied filename, and is readable, add it 117 | if fnmatch.fnmatch(bn, sf) and os.access(f, os.R_OK): 118 | if f not in filtered_files: 119 | filtered_files.append(f) 120 | # Final sanity check 121 | if not filtered_files: 122 | logging.critical(u"No policy files found.") 123 | return None 124 | return filtered_files 125 | 126 | 127 | # Parse arguments 128 | parser = argparse.ArgumentParser( 129 | description=u"SELinux source policy analysis tool.", 130 | epilog=u"If not differently specified, all available plugins will be run.") 131 | # List the available plugins 132 | parser.add_argument(u"-l", u"--list", action=u"store_true", 133 | help=u"list the available plugins and exit.") 134 | # Select the plugins to run (default: all). Specify either with a whitelist 135 | # or a blacklist, but not both 136 | plugin_group = parser.add_mutually_exclusive_group() 137 | # Plugin whitelist 138 | plugin_group.add_argument( 139 | u"-w", u"--whitelist", metavar=u"", 140 | choices=plugins.available_plugins, nargs=u"+", 141 | help=u"specify the plugins to run [Default: run all].") 142 | # Plugin blacklist 143 | plugin_group.add_argument( 144 | u"-b", u"--blacklist", metavar=u"", 145 | choices=plugins.available_plugins, nargs=u"+", 146 | help=u"specify the plugins not to run [Default: run all].") 147 | # Get additional M4 definitions 148 | parser.add_argument(u"-D", u"--define", metavar=u"NAME[=VALUE]", nargs=u"+", 149 | dest=u"extra_defs", 150 | help=u"Pass additional definitions to M4 when expanding " 151 | u"the policy. Identical to the -D option in m4.") 152 | # Write out the policy.conf file 153 | parser.add_argument(u"--dumppolicyconf", metavar=u"", 154 | help=u"write the policy.conf to a user-specified file. " 155 | u"If the file already exists, IT WILL BE OVERWRITTEN.") 156 | # Write out the full list of recognized policy files to be processed 157 | parser.add_argument(u"--listpolicyfiles", action=u"store_true", 158 | help=u"List all the recognized policy files and exit.") 159 | # Set the verbosity level 160 | parser.add_argument(u"-v", u"--verbosity", metavar=u"", 161 | choices=[0, 1, 2, 3, 4], type=int, default=-1, 162 | help=u"Be verbose. Supported levels are 0-4, " 163 | u"with 0 being the default.") 164 | # Supply a different config file 165 | parser.add_argument(u"-c", u"--config", metavar=u"", type=str, 166 | default=None, help=u"Source the specified config " 167 | u"file [Default: config.py].") 168 | 169 | args = parser.parse_args() 170 | 171 | # Handle "list" option right away 172 | if args.list: 173 | # List available plugins and exit 174 | print(u"Available plugins:") 175 | print(u"\n".join(plugins.available_plugins)) 176 | sys.exit(0) 177 | 178 | # Select the desired plugins, either from a whitelist or a blacklist. 179 | # The whitelist has precedence over the blacklist. 180 | # If no restriction is specified, select all available plugins. 181 | if args.whitelist: 182 | selected_plugins = list(args.whitelist) 183 | elif args.blacklist: 184 | selected_plugins = [ 185 | x for x in plugins.available_plugins if x not in args.blacklist] 186 | else: 187 | selected_plugins = list(plugins.available_plugins) 188 | # Find plugins which require neverallow rules. 189 | # Run them on a different policy. This way all plugins which do not require 190 | # neverallow rules can run on a "slim" policy even when running some plugins 191 | # which do require neverallow rules. 192 | plugins_neverallow = [] 193 | for each in selected_plugins: 194 | plg = plugins.get_plugin(each) 195 | if u"neverallow" in plg.REQUIRED_RULES: 196 | plugins_neverallow.append(each) 197 | 198 | # Import config file as "config" 199 | if args.config is None: 200 | # If the user didn't specify a configuration file, use the default 201 | # "config.py" in the software directory 202 | args.config = os.path.join(os.path.dirname( 203 | os.path.realpath(__file__)), u"config.py") 204 | print(u"Using default configuration file \"{}\"...".format(args.config)) 205 | else: 206 | # Use the user-provided configuration file 207 | print(u"Using configuration file \"{}\"...".format(args.config)) 208 | try: 209 | # Import the configuration file as "config" 210 | config = imp.load_source(u"config", os.path.abspath( 211 | os.path.expanduser(args.config))) 212 | except: 213 | e = sys.exc_info() 214 | print(e) 215 | print(u"CRITICAL: Bad configuration file " 216 | u"\"{}\", aborting ...".format(args.config)) 217 | sys.exit(1) 218 | 219 | # Save verbosity in config 220 | if hasattr(config, u"VERBOSITY"): 221 | # If we have a verbosity configuration value 222 | if args.verbosity == -1: 223 | # User didn't explicitly set verbosity on the command line 224 | if isinstance(config.VERBOSITY, int) and \ 225 | config.VERBOSITY >= 0 and config.VERBOSITY <= 4: 226 | # If we have a valid verbosity in the config file 227 | args.verbosity = config.VERBOSITY 228 | else: 229 | # User explictly set verbosity on the command line 230 | config.VERBOSITY = args.verbosity 231 | else: 232 | # If we have no verbosity configuration value 233 | if args.verbosity == -1: 234 | # User didn't explicitly set verbosity on the command line: reset it 235 | # to the default value 236 | args.verbosity = 0 237 | # Set the configuration value of verbosity 238 | config.VERBOSITY = args.verbosity 239 | # Save extra_defs in config 240 | if hasattr(config, u"EXTRA_DEFS"): 241 | # If we have an extra_defs configuration value 242 | if args.extra_defs is None: 243 | # User didn't explicitly set extra_defs on the command line 244 | args.extra_defs = config.EXTRA_DEFS 245 | else: 246 | # User explictly set extra_defs on the command line: combine the two 247 | args.extra_defs.extend(config.EXTRA_DEFS) 248 | else: 249 | # If we have no extra_defs configuration value 250 | if args.extra_defs is None: 251 | # User didn't explicitly set extra_defs on the command line: set it 252 | # to an empty list 253 | args.extra_defs = [] 254 | # Set the configuration value of extra_defs 255 | config.EXTRA_DEFS = args.extra_defs 256 | 257 | # Setup logging 258 | if args.verbosity == 4: 259 | logging.basicConfig(level=logging.DEBUG) 260 | elif args.verbosity == 3: 261 | logging.basicConfig(level=logging.INFO) 262 | elif args.verbosity == 2: 263 | logging.basicConfig(level=logging.WARNING) 264 | elif args.verbosity == 1: 265 | logging.basicConfig(level=logging.ERROR) 266 | elif args.verbosity == 0: 267 | logging.basicConfig(level=logging.CRITICAL) 268 | 269 | # Compute list of policy files 270 | # TODO: add CLI option in addition to config file 271 | ALL_POLICY_FILES = get_policy_files(config) 272 | if args.listpolicyfiles: 273 | print(u"\n".join(ALL_POLICY_FILES)) 274 | sys.exit(0) 275 | # Save the absolute path of the base directory in config 276 | config.FULL_BASE_DIR = os.path.abspath( 277 | os.path.expanduser(config.BASE_DIR_GLOBAL)) 278 | 279 | # Create policy 280 | # If neverallow rules are required by some plugin, create a "slim" policy 281 | # and a "fat" policy. Run all plugins which do not require neverallow rules on 282 | # the "slim" policy for speed 283 | policy_slim = policysource.policy.SourcePolicy( 284 | ALL_POLICY_FILES, args.extra_defs, load_neverallows=False) 285 | if plugins_neverallow: 286 | policy_fat = policysource.policy.SourcePolicy( 287 | ALL_POLICY_FILES, args.extra_defs, load_neverallows=True) 288 | else: 289 | policy_fat = None 290 | # Write the policy.conf to file, if requested 291 | if args.dumppolicyconf: 292 | try: 293 | shutil.copyfile(policy_slim.policyconf, args.dumppolicyconf) 294 | except shutil.Error as e: 295 | logging.error(u"%s", e) 296 | logging.error(u"Could not dump policy.conf") 297 | except IOError as e: 298 | logging.error(u"%s", e) 299 | logging.error(u"Could not dump policy.conf") 300 | else: 301 | logging.info(u"Wrote policy.conf to %s", args.dumppolicyconf) 302 | 303 | # Run plugins 304 | # Run plugins which require the fat policy first, then delete it to save memory 305 | for plg in plugins_neverallow: 306 | print(u"Running plugin " + plg + u"...") 307 | plugins.get_plugin(plg).main(policy_fat, config) 308 | del policy_fat 309 | for plg in selected_plugins: 310 | if plg not in plugins_neverallow: 311 | print(u"Running plugin " + plg + u"...") 312 | plugins.get_plugin(plg).main(policy_slim, config) 313 | --------------------------------------------------------------------------------