Skip to content

Commit 4d2ce6b

Browse files
committed
python: create shared serverless module and use it
Modelled on the javascript serverless module, but - The predicate that reports YAML files is now public so languages can implement their own file conventions. - It also reports framework and runtime. - The conveninece predicates with files still exist, but they only report the path. - Handler mapping conventions are now documented. - Use parameterised serverless module in Python, tests now pass.
1 parent a892e83 commit 4d2ce6b

File tree

6 files changed

+304
-3
lines changed

6 files changed

+304
-3
lines changed

python/ql/lib/semmle/python/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ private import semmle.python.frameworks.Requests
4949
private import semmle.python.frameworks.RestFramework
5050
private import semmle.python.frameworks.Rsa
5151
private import semmle.python.frameworks.RuamelYaml
52+
private import semmle.python.frameworks.ServerLess
5253
private import semmle.python.frameworks.Simplejson
5354
private import semmle.python.frameworks.SqlAlchemy
5455
private import semmle.python.frameworks.Starlette
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import python
2+
import codeql.serverless.ServerLess
3+
import semmle.python.dataflow.new.DataFlow
4+
import semmle.python.dataflow.new.RemoteFlowSources
5+
6+
private module YamlImpl implements Input {
7+
import semmle.python.Files
8+
import semmle.python.Yaml
9+
}
10+
11+
module SL = ServerLess<YamlImpl>;
12+
13+
/**
14+
* Gets a function that is a serverless request handler.
15+
*
16+
* For example: if an AWS serverless resource contains the following properties (in the "template.yml" file):
17+
* ```yaml
18+
* Handler: mylibrary.handler
19+
* Runtime: pythonXXX
20+
* CodeUri: backend/src/
21+
* ```
22+
*
23+
* And a file "mylibrary.py" exists in the folder "backend/src" (relative to the "template.yml" file).
24+
* Then the result of this predicate is a function exported as "handler" from "mylibrary.py".
25+
* The "mylibrary.py" file could for example look like:
26+
*
27+
* ```python
28+
* def handler(event):
29+
* ...
30+
* ```
31+
*/
32+
private Function getAServerlessHandler() {
33+
exists(File file, string stem, string handler, string runtime, Module mod |
34+
SL::hasServerlessHandler(stem, handler, _, runtime) and
35+
file.getAbsolutePath() = stem + ".py" and
36+
// if runtime is specified, it should be python
37+
(runtime = "" or runtime.matches("python%"))
38+
|
39+
mod.getFile() = file and
40+
mod.getAnExport() = handler and
41+
result.getEnclosingModule() = mod and
42+
result.getName() = handler
43+
)
44+
}
45+
46+
private DataFlow::ParameterNode getAHandlerEventParameter() {
47+
exists(Function func | func = getAServerlessHandler() |
48+
result.getParameter() in [func.getArg(0), func.getArgByName("event")]
49+
)
50+
}
51+
52+
/**
53+
* A serverless request handler event, seen as a RemoteFlowSource.
54+
*/
55+
private class ServerlessHandlerEventAsRemoteFlow extends RemoteFlowSource::Range {
56+
ServerlessHandlerEventAsRemoteFlow() { this = getAHandlerEventParameter() }
57+
58+
override string getSourceType() { result = "Serverless event" }
59+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
def handler1(event, context): # $ MISSING: remoteFlow=event, remoteFlow=context
1+
def handler1(event, context): # $ remoteFlow=event
22
return "Hello World!"
33

4-
def handler2(event, context): # $ MISSING: remoteFlow=event, remoteFlow=context
4+
def handler2(event, context): # $ remoteFlow=event
55
return "Hello World!"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
def lambda_handler(event, context): # $ MISSING: remoteFlow=event, remoteFlow=context
1+
def lambda_handler(event, context): # $ remoteFlow=event
22
return "OK"
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* Provides classes and predicates for working with serverless handlers.
3+
* E.g. [AWS](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html) or [serverless](https://npmjs.com/package/serverless)
4+
*/
5+
6+
/**
7+
* Provides the input for the `ServerLess` module.
8+
* Most of these should be provided by the `yaml` library.
9+
*/
10+
signature module Input {
11+
// --------------------------------------------------
12+
// The below should be provided by the `yaml` library.
13+
// --------------------------------------------------
14+
class Container {
15+
string getAbsolutePath();
16+
17+
Container getParentContainer();
18+
}
19+
20+
class File extends Container;
21+
22+
class YamlNode {
23+
File getFile();
24+
25+
YamlCollection getParentNode();
26+
}
27+
28+
class YamlValue extends YamlNode;
29+
30+
class YamlCollection extends YamlValue;
31+
32+
class YamlScalar extends YamlValue {
33+
string getValue();
34+
}
35+
36+
class YamlMapping extends YamlCollection {
37+
YamlValue lookup(string key);
38+
39+
YamlValue getValue(int i);
40+
}
41+
}
42+
43+
/**
44+
* Provides classes and predicates for working with serverless handlers.
45+
* Supports AWS, Alibaba, and serverless.
46+
*
47+
* Common usage is to interpret the handlers as functions and add the
48+
* first argument of these as remote flow sources.
49+
*/
50+
module ServerLess<Input I> {
51+
import I
52+
53+
/**
54+
* Gets the looked up value as a convenience.
55+
*/
56+
pragma[inline]
57+
private string lookupValue(YamlMapping mapping, string property) {
58+
result = mapping.lookup(property).(YamlScalar).getValue()
59+
}
60+
61+
/**
62+
* Gets a string where an ending "/." is simplified to "/" (if it exists).
63+
*/
64+
bindingset[base]
65+
private string removeTrailingDot(string base) {
66+
if base.regexpMatch(".*/\\.")
67+
then result = base.substring(0, base.length() - 1)
68+
else result = base
69+
}
70+
71+
/**
72+
* Gets a string where a leading "./" is simplified to "" (if it exists).
73+
*/
74+
bindingset[base]
75+
private string removeLeadingDotSlash(string base) {
76+
if base.regexpMatch("\\./.*") then result = base.substring(2, base.length()) else result = base
77+
}
78+
79+
/**
80+
* Gets a string suitable as part of a file path.
81+
*/
82+
bindingset[base]
83+
private string normalise(string base) { result = removeLeadingDotSlash(removeTrailingDot(base)) }
84+
85+
/**
86+
* Holds if the `.yml` file `ymlFile` contains a serverless configuration fro `framework` with
87+
* `handler`, `codeURI`, and `runtime` properties.
88+
* `codeURI` and `runtime` default to the empty string if no explicit value is set in the configuration.
89+
*
90+
* `handler` should be interpreted in a language specific way, see `mapping.md`.
91+
*/
92+
predicate hasServerlessHandler(
93+
File ymlFile, string framework, string handler, string codeUri, string runtime
94+
) {
95+
exists(YamlMapping resource | ymlFile = resource.getFile() |
96+
// There exists at least "AWS::Serverless::Function" and "Aliyun::Serverless::Function"
97+
resource.lookup("Type").(YamlScalar).getValue().regexpMatch(".*::Serverless::Function") and
98+
framework = lookupValue(resource, "Type") and
99+
exists(YamlMapping properties | properties = resource.lookup("Properties") |
100+
(
101+
handler = lookupValue(properties, "Handler") and
102+
(
103+
if exists(properties.lookup("CodeUri"))
104+
then codeUri = normalise(lookupValue(properties, "CodeUri"))
105+
else codeUri = ""
106+
) and
107+
(
108+
if exists(properties.lookup("Runtime"))
109+
then runtime = lookupValue(properties, "Runtime")
110+
else runtime = ""
111+
)
112+
)
113+
)
114+
or
115+
// The `serverless` library, which specifies a top-level `functions` property
116+
framework = "Serverless" and
117+
exists(YamlMapping functions |
118+
functions = resource.lookup("functions") and
119+
not exists(resource.getParentNode()) and
120+
handler = lookupValue(functions.getValue(_), "handler") and
121+
codeUri = "" and
122+
(
123+
if exists(functions.lookup("Runtime"))
124+
then runtime = lookupValue(functions, "Runtime")
125+
else runtime = ""
126+
)
127+
)
128+
)
129+
}
130+
131+
/**
132+
* Holds if `handler` = `filePart . astPart` and `filePart` does not contain a `.`.
133+
* This is a convenience predicate, as in many cases the first part of the handler property
134+
* should be interpreted as (the stem of) a file name.
135+
*/
136+
bindingset[handler]
137+
predicate splitHandler(string handler, string filePart, string astPart) {
138+
exists(string pattern | pattern = "(.*?)\\.(.*)" |
139+
filePart = handler.regexpCapture(pattern, 1) and
140+
astPart = handler.regexpCapture(pattern, 2)
141+
)
142+
}
143+
144+
/**
145+
* Holds if a file with stem `fileStem` has a serverless handler denoted by `func`.
146+
*
147+
* This is a convenience predicate for the common case where the first part of the
148+
* handler property is the file name.
149+
*
150+
* `func` should be interpreted in a language specific way, see `mapping.md`.
151+
*/
152+
predicate hasServerlessHandler(string fileStem, string func, string framework, string runtime) {
153+
exists(File ymlFile, string handler, string codeUri, string filePart |
154+
hasServerlessHandler(ymlFile, framework, handler, codeUri, runtime)
155+
|
156+
splitHandler(handler, filePart, func) and
157+
fileStem = ymlFile.getParentContainer().getAbsolutePath() + "/" + codeUri + filePart
158+
)
159+
}
160+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Mapping the `handler` property to a function definition
2+
3+
## AWS
4+
5+
[Documentation](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html)
6+
7+
### Node.js or Typescript
8+
See [documentaion](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html)
9+
10+
Setting `handler` to `index.handler` means that `handler` is exported from `index.js`.
11+
12+
For Typescript, code is first transpiled to JavaScript, see [documentation](https://docs.aws.amazon.com/lambda/latest/dg/lambda-typescript.html).
13+
14+
15+
### Python
16+
See [documentation](https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html)
17+
18+
Setting `handler` to `lambda_function.lambda_handler` means that `def lambda_handler` is found in `lambda_function.py`.
19+
20+
### Ruby
21+
See [documentation](https://docs.aws.amazon.com/lambda/latest/dg/ruby-handler.html)
22+
23+
Setting `handler` to `function.handler` means that `def handler` is found in `function.rb`.
24+
Setting `handler` to `source.LambdaFunctions::Handler.process` means that `def self.process` is found inside `class Handler` inside `module LambdaFunctions` in `source.rb`.
25+
26+
### Java
27+
See [documentation](https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html)
28+
29+
You can express the hander in the following formats:
30+
31+
- `package.Class::method` – Full format. For example: example.Handler::handleRequest.
32+
33+
- `package.Class` – Abbreviated format for functions that implement a handler interface. For example: example.Handler.
34+
35+
### Go
36+
See [documentation](https://docs.aws.amazon.com/lambda/latest/dg/golang-handler.html)
37+
38+
When you configure a function in Go, the value of the handler setting is the executable file name. For example, if you set the value of the handler to Handler, Lambda will call the main() function in the Handler executable file.
39+
40+
### C#
41+
See [documentation](https://docs.aws.amazon.com/lambda/latest/dg/csharp-handler.html)
42+
43+
`handler` is of this format: `Assembly::Namespace.ClassName::MethodName`.
44+
For example, `HelloWorldApp::Example.Hello::MyHandler` if `public Stream MyHandler` is found inside `public class Hello` inside `namespace Example` in the `HelloWorldApp` assembly.
45+
46+
47+
## Aliyun (Alibaba Cloud)
48+
[Properties](https://www.alibabacloud.com/help/en/resource-orchestration-service/latest/aliyun-serverless-function)
49+
[Languages](https://www.alibabacloud.com/help/en/function-compute/latest/programming-languages)
50+
51+
### Node.js
52+
See [documentation](https://www.alibabacloud.com/help/en/function-compute/latest/node-request-handler)
53+
54+
The handler must be in the `File name.Method name` format. For example, if your file name is `main.js` and your method name is `handler`, the handler is `main.handler`.
55+
56+
### Python
57+
See [documentation](https://www.alibabacloud.com/help/en/function-compute/latest/programming-languages-python)
58+
59+
In Python, your request handler must be in the `File name.Method name` format. For example, if your file name is `main.py` and your method name is `handler`, the handler is `main.handler`.
60+
61+
### Java
62+
See [documentation](https://www.alibabacloud.com/help/en/function-compute/latest/programming-languages-java)
63+
64+
The handler must be in the `[Package name].[Class name]::[Method name]` format. For example, if the name of your package is `example`, the class type is `HelloFC`, and method is `handleRequest`, the handler can be configured as `example.HelloFC::handleRequest`.
65+
66+
### C#
67+
See [documentation](https://www.alibabacloud.com/help/en/function-compute/latest/programming-languages-csharp)
68+
69+
The handler is in the format of `Assembly::Namespace.ClassName::MethodName`.
70+
71+
### Go
72+
See [documentation](https://www.alibabacloud.com/help/en/function-compute/latest/go-323505)
73+
74+
The handler for FC functions in the Go language is compiled into an executable binary file. You only need to set the Request Handler parameter of the FC function to the name of the executable file.
75+
76+
## Serverless
77+
[Documentation](https://www.serverless.com/framework/docs/providers/aws/guide/functions)
78+
79+
The handler property points to the file and module containing the code you want to run in your function.
80+
81+
There seems to be nothing language specific written down about the handler property.

0 commit comments

Comments
 (0)