Skip to content

Commit 13815fe

Browse files
committed
Python: Model known APIView subclasses
Added internal helper `.qll` file as well
1 parent 62d3063 commit 13815fe

File tree

2 files changed

+237
-2
lines changed

2 files changed

+237
-2
lines changed

python/ql/lib/semmle/python/frameworks/RestFramework.qll

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,54 @@ private module RestFramework {
3737
*/
3838
private class ModeledApiViewClasses extends Django::Views::View::ModeledSubclass {
3939
ModeledApiViewClasses() {
40-
this = API::moduleImport("rest_framework").getMember("views").getMember("APIView")
41-
// TODO: Need to model all known subclasses
40+
this = API::moduleImport("rest_framework").getMember("views").getMember("APIView") or
41+
// imports generated by python/frameworks/internal/SubclassFinder.qll
42+
this =
43+
API::moduleImport("rest_framework")
44+
.getMember("authtoken")
45+
.getMember("views")
46+
.getMember("APIView") or
47+
this =
48+
API::moduleImport("rest_framework")
49+
.getMember("authtoken")
50+
.getMember("views")
51+
.getMember("ObtainAuthToken") or
52+
this = API::moduleImport("rest_framework").getMember("decorators").getMember("APIView") or
53+
this = API::moduleImport("rest_framework").getMember("generics").getMember("CreateAPIView") or
54+
this = API::moduleImport("rest_framework").getMember("generics").getMember("DestroyAPIView") or
55+
this = API::moduleImport("rest_framework").getMember("generics").getMember("GenericAPIView") or
56+
this = API::moduleImport("rest_framework").getMember("generics").getMember("ListAPIView") or
57+
this =
58+
API::moduleImport("rest_framework").getMember("generics").getMember("ListCreateAPIView") or
59+
this = API::moduleImport("rest_framework").getMember("generics").getMember("RetrieveAPIView") or
60+
this =
61+
API::moduleImport("rest_framework")
62+
.getMember("generics")
63+
.getMember("RetrieveDestroyAPIView") or
64+
this =
65+
API::moduleImport("rest_framework").getMember("generics").getMember("RetrieveUpdateAPIView") or
66+
this =
67+
API::moduleImport("rest_framework")
68+
.getMember("generics")
69+
.getMember("RetrieveUpdateDestroyAPIView") or
70+
this = API::moduleImport("rest_framework").getMember("generics").getMember("UpdateAPIView") or
71+
this = API::moduleImport("rest_framework").getMember("routers").getMember("APIRootView") or
72+
this = API::moduleImport("rest_framework").getMember("routers").getMember("SchemaView") or
73+
this =
74+
API::moduleImport("rest_framework")
75+
.getMember("schemas")
76+
.getMember("views")
77+
.getMember("APIView") or
78+
this =
79+
API::moduleImport("rest_framework")
80+
.getMember("schemas")
81+
.getMember("views")
82+
.getMember("SchemaView") or
83+
this = API::moduleImport("rest_framework").getMember("viewsets").getMember("GenericViewSet") or
84+
this = API::moduleImport("rest_framework").getMember("viewsets").getMember("ModelViewSet") or
85+
this =
86+
API::moduleImport("rest_framework").getMember("viewsets").getMember("ReadOnlyModelViewSet") or
87+
this = API::moduleImport("rest_framework").getMember("viewsets").getMember("ViewSet")
4288
}
4389
}
4490

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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

Comments
 (0)