|
| 1 | +/** |
| 2 | + * INTERNAL: Do not use. |
| 3 | + * |
| 4 | + * Has predicates to help find subclasses in library code. Should only be used to aid in |
| 5 | + * the manual library modeling process, |
| 6 | + */ |
| 7 | + |
| 8 | +private import python |
| 9 | +private import semmle.python.dataflow.new.DataFlow |
| 10 | +private import semmle.python.ApiGraphs |
| 11 | +private import semmle.python.filters.Tests |
| 12 | + |
| 13 | +// very much inspired by the draft at https://github.com/github/codeql/pull/5632 |
| 14 | +private module NotExposed { |
| 15 | + // Instructions: |
| 16 | + // This needs to be automated better, but for this prototype, here are some rough instructions: |
| 17 | + // 1) fill out the `getAlreadyModeledClass` body below |
| 18 | + // 2) quick-eval the `quickEvalMe` predicate below, and copy the output to your modeling predicate |
| 19 | + class MySpec extends FindSubclassesSpec { |
| 20 | + MySpec() { this = "MySpec" } |
| 21 | + |
| 22 | + override API::Node getAlreadyModeledClass() { |
| 23 | + // FILL ME OUT ! (but don't commit with any changes) |
| 24 | + none() |
| 25 | + // for example |
| 26 | + // result = API::moduleImport("rest_framework").getMember("views").getMember("APIView") |
| 27 | + } |
| 28 | + } |
| 29 | + |
| 30 | + predicate quickEvalMe(string newImport) { |
| 31 | + newImport = |
| 32 | + "// imports generated by python/frameworks/internal/SubclassFinder.qll\n" + "this = API::" + |
| 33 | + concat(string newModelFullyQualified | |
| 34 | + newModel(any(MySpec spec), newModelFullyQualified, _, _, _) |
| 35 | + | |
| 36 | + fullyQualifiedToAPIGraphPath(newModelFullyQualified), " or this = API::" |
| 37 | + ) |
| 38 | + } |
| 39 | + |
| 40 | + bindingset[fullyQaulified] |
| 41 | + string fullyQualifiedToAPIGraphPath(string fullyQaulified) { |
| 42 | + result = "moduleImport(\"" + fullyQaulified.replaceAll(".", "\").getMember(\"") + "\")" |
| 43 | + } |
| 44 | + |
| 45 | + // -- Specs -- |
| 46 | + bindingset[this] |
| 47 | + abstract class FindSubclassesSpec extends string { |
| 48 | + abstract API::Node getAlreadyModeledClass(); |
| 49 | + } |
| 50 | + |
| 51 | + API::Node newOrExistingModeling(FindSubclassesSpec spec) { |
| 52 | + result = spec.getAlreadyModeledClass() |
| 53 | + or |
| 54 | + exists(string newSubclassName | |
| 55 | + newModel(spec, newSubclassName, _, _, _) and |
| 56 | + result.getPath() = fullyQualifiedToAPIGraphPath(newSubclassName) |
| 57 | + ) |
| 58 | + } |
| 59 | + |
| 60 | + bindingset[fullyQualifiedName] |
| 61 | + predicate alreadyModeled(FindSubclassesSpec spec, string fullyQualifiedName) { |
| 62 | + fullyQualifiedToAPIGraphPath(fullyQualifiedName) = spec.getAlreadyModeledClass().getPath() |
| 63 | + } |
| 64 | + |
| 65 | + predicate isNonTestProjectCode(AstNode ast) { |
| 66 | + not ast.getScope*() instanceof TestScope and |
| 67 | + not ast.getLocation().getFile().getRelativePath().matches("tests/%") and |
| 68 | + exists(ast.getLocation().getFile().getRelativePath()) |
| 69 | + } |
| 70 | + |
| 71 | + predicate hasAllStatement(Module mod) { |
| 72 | + exists(AssignStmt a, GlobalVariable all | |
| 73 | + a.defines(all) and |
| 74 | + a.getScope() = mod and |
| 75 | + all.getId() = "__all__" |
| 76 | + ) |
| 77 | + } |
| 78 | + |
| 79 | + /** |
| 80 | + * Holds if `newAliasFullyQualified` describes new alias originating from the import |
| 81 | + * `from <module> import <member> [as <new-name>]`, where `<module>.<member>` belongs to |
| 82 | + * `spec`. |
| 83 | + * So if this import happened in module `foo.bar`, `newAliasFullyQualified` would be |
| 84 | + * `foo.bar.<member>` (or `foo.bar.<new-name>`). |
| 85 | + * |
| 86 | + * Note that this predicate currently respects `__all__` in sort of a backwards fashion. |
| 87 | + * - if `__all__` is defined in module `foo.bar`, we only allow new aliases where the member name is also in `__all__`. (this doesn't map 100% to the semantics of imports though) |
| 88 | + * - If `__all__` is not defined we don't impose any limitations. |
| 89 | + * |
| 90 | + * Also note that we don't currently consider deleting module-attributes at all, so in the code snippet below, we would consider that `my_module.foo` is a |
| 91 | + * reference to `django.foo`, although `my_module.foo` isn't even available at runtime. (there currently also isn't any code to discover that `my_module.bar` |
| 92 | + * is an alias to `django.foo`) |
| 93 | + * ```py |
| 94 | + * # module my_module |
| 95 | + * from django import foo |
| 96 | + * bar = foo |
| 97 | + * del foo |
| 98 | + * ``` |
| 99 | + */ |
| 100 | + predicate newDirectAlias( |
| 101 | + FindSubclassesSpec spec, string newAliasFullyQualified, ImportMember importMember, Module mod, |
| 102 | + Location loc |
| 103 | + ) { |
| 104 | + importMember = newOrExistingModeling(spec).getAUse().asExpr() and |
| 105 | + importMember.getScope() = mod and |
| 106 | + loc = importMember.getLocation() and |
| 107 | + ( |
| 108 | + mod.isPackageInit() and |
| 109 | + newAliasFullyQualified = mod.getPackageName() + "." + importMember.getName() |
| 110 | + or |
| 111 | + not mod.isPackageInit() and |
| 112 | + newAliasFullyQualified = mod.getName() + "." + importMember.getName() |
| 113 | + ) and |
| 114 | + ( |
| 115 | + not hasAllStatement(mod) |
| 116 | + or |
| 117 | + mod.declaredInAll(importMember.getName()) |
| 118 | + ) and |
| 119 | + not alreadyModeled(spec, newAliasFullyQualified) and |
| 120 | + isNonTestProjectCode(importMember) |
| 121 | + } |
| 122 | + |
| 123 | + /** same as `newDirectAlias` predicate, but handling `from <module> import *`, considering all `<member>`, where `<module>.<member>` belongs to `spec`. */ |
| 124 | + predicate newImportStar( |
| 125 | + FindSubclassesSpec spec, string newAliasFullyQualified, ImportStar importStar, Module mod, |
| 126 | + API::Node relevantClass, string relevantName, Location loc |
| 127 | + ) { |
| 128 | + relevantClass = newOrExistingModeling(spec) and |
| 129 | + loc = importStar.getLocation() and |
| 130 | + importStar.getScope() = mod and |
| 131 | + // WHAT A HACK :D :D |
| 132 | + relevantClass.getPath() = |
| 133 | + relevantClass.getAPredecessor().getPath() + ".getMember(\"" + relevantName + "\")" and |
| 134 | + relevantClass.getAPredecessor().getAUse().asExpr() = importStar.getModule() and |
| 135 | + ( |
| 136 | + mod.isPackageInit() and |
| 137 | + newAliasFullyQualified = mod.getPackageName() + "." + relevantName |
| 138 | + or |
| 139 | + not mod.isPackageInit() and |
| 140 | + newAliasFullyQualified = mod.getName() + "." + relevantName |
| 141 | + ) and |
| 142 | + ( |
| 143 | + not hasAllStatement(mod) |
| 144 | + or |
| 145 | + mod.declaredInAll(relevantName) |
| 146 | + ) and |
| 147 | + not alreadyModeled(spec, newAliasFullyQualified) and |
| 148 | + isNonTestProjectCode(importStar) |
| 149 | + } |
| 150 | + |
| 151 | + /** Holds if `classExpr` defines a new subclass that belongs to `spec`, which has the fully qualified name `newSubclassQualified`. */ |
| 152 | + predicate newSubclass( |
| 153 | + FindSubclassesSpec spec, string newSubclassQualified, ClassExpr classExpr, Module mod, |
| 154 | + Location loc |
| 155 | + ) { |
| 156 | + classExpr = newOrExistingModeling(spec).getASubclass*().getAUse().asExpr() and |
| 157 | + classExpr.getScope() = mod and |
| 158 | + newSubclassQualified = mod.getName() + "." + classExpr.getName() and |
| 159 | + loc = classExpr.getLocation() and |
| 160 | + not alreadyModeled(spec, newSubclassQualified) and |
| 161 | + isNonTestProjectCode(classExpr) |
| 162 | + } |
| 163 | + |
| 164 | + /** |
| 165 | + * Holds if `newModelFullyQualified` describes either a new subclass, or a new alias, belonging to `spec` that we should include in our automated modeling. |
| 166 | + * This new element is defined by `ast`, which is defined at `loc` in the module `mod`. |
| 167 | + */ |
| 168 | + query predicate newModel( |
| 169 | + FindSubclassesSpec spec, string newModelFullyQualified, AstNode ast, Module mod, Location loc |
| 170 | + ) { |
| 171 | + ( |
| 172 | + newSubclass(spec, newModelFullyQualified, ast, mod, loc) |
| 173 | + or |
| 174 | + newDirectAlias(spec, newModelFullyQualified, ast, mod, loc) |
| 175 | + or |
| 176 | + newImportStar(spec, newModelFullyQualified, ast, mod, _, _, loc) |
| 177 | + ) |
| 178 | + } |
| 179 | + // inherint problem with API graphs is that there doesn't need to exist a result for all |
| 180 | + // the stuff we have already modeled... as an example, the following query has no |
| 181 | + // results when evaluated against Django |
| 182 | + // |
| 183 | + // select API::moduleImport("django") |
| 184 | + // .getMember("contrib") |
| 185 | + // .getMember("admin") |
| 186 | + // .getMember("views") |
| 187 | + // .getMember("main") |
| 188 | + // .getMember("ChangeListSearchForm") |
| 189 | +} |
0 commit comments