|
| 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 |
| 4 | + */ |
| 5 | + |
| 6 | +import javascript |
| 7 | + |
| 8 | +/** |
| 9 | + * Provides classes and predicates for working with serverless handlers. |
| 10 | + * In particular a `RemoteFlowSource` is added for AWS and Alibaba serverless. |
| 11 | + */ |
| 12 | +private module ServerLess { |
| 13 | + /** |
| 14 | + * Holds if the `.yml` file `ymlFile` contains a serverless configuration with `handler` and `codeURI` properties. |
| 15 | + * `codeURI` defaults to the empty string if no explicit value is set in the configuration. |
| 16 | + */ |
| 17 | + private predicate hasServerlessHandler(File ymlFile, string handler, string codeURI) { |
| 18 | + exists(YAMLMapping resource | ymlFile = resource.getFile() | |
| 19 | + // There exists at least "AWS::Serverless::Function" and "Aliyun::Serverless::Function" |
| 20 | + resource.lookup("Type").(YAMLScalar).getValue().regexpMatch(".*::Serverless::Function") and |
| 21 | + exists(YAMLMapping properties | properties = resource.lookup("Properties") | |
| 22 | + handler = properties.lookup("Handler").(YAMLScalar).getValue() and |
| 23 | + if exists(properties.lookup("CodeUri")) |
| 24 | + then codeURI = properties.lookup("CodeUri").(YAMLScalar).getValue() |
| 25 | + else codeURI = "" |
| 26 | + ) |
| 27 | + ) |
| 28 | + } |
| 29 | + |
| 30 | + /** |
| 31 | + * Gets a string where an ending "/." is simplified to "/" (if it exists). |
| 32 | + */ |
| 33 | + bindingset[base] |
| 34 | + private string removeTrailingDot(string base) { |
| 35 | + if base.regexpMatch(".*/\\.") |
| 36 | + then result = base.substring(0, base.length() - 1) |
| 37 | + else result = base |
| 38 | + } |
| 39 | + |
| 40 | + /** |
| 41 | + * Gets a string where a leading "./" is simplified to "" (if it exists). |
| 42 | + */ |
| 43 | + bindingset[base] |
| 44 | + private string removeLeadingDotSlash(string base) { |
| 45 | + if base.regexpMatch("\\./.*") then result = base.substring(2, base.length()) else result = base |
| 46 | + } |
| 47 | + |
| 48 | + /** |
| 49 | + * Gets a path to a file from a `codeURI` property and a file name from a serverless configuration. |
| 50 | + * |
| 51 | + * For example if `codeURI` is "function/." and `file` is "index", then the result becomes "function/index.js". |
| 52 | + */ |
| 53 | + bindingset[codeURI, file] |
| 54 | + private string getPathFromHandlerProperties(string codeURI, string file) { |
| 55 | + exists(string folder | folder = removeLeadingDotSlash(removeTrailingDot(codeURI)) | |
| 56 | + result = folder + file + ".js" |
| 57 | + ) |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * Holds if `file` has a serverless handler function with name `func`. |
| 62 | + */ |
| 63 | + private predicate hasServerlessHandler(File file, string func) { |
| 64 | + exists(File ymlFile, string handler, string codeURI, string fileName | |
| 65 | + hasServerlessHandler(ymlFile, handler, codeURI) and |
| 66 | + // Splits a `handler` into two components. The `fileName` to the left of the dot, and the `func` to the right. |
| 67 | + // E.g. if `handler` is "index.foo", then `fileName` is "index" and `func` is "foo". |
| 68 | + exists(string pattern | pattern = "(.*)\\.(.*)" | |
| 69 | + fileName = handler.regexpCapture(pattern, 1) and |
| 70 | + func = handler.regexpCapture(pattern, 2) |
| 71 | + ) |
| 72 | + | |
| 73 | + file.getAbsolutePath() = |
| 74 | + ymlFile.getParentContainer().getAbsolutePath() + "/" + |
| 75 | + getPathFromHandlerProperties(codeURI, fileName) |
| 76 | + ) |
| 77 | + } |
| 78 | + |
| 79 | + /** |
| 80 | + * Gets a function that is a serverless request handler. |
| 81 | + * |
| 82 | + * For example: if an AWS serverless resource contains the following properties (in the "template.yml" file): |
| 83 | + * ```yaml |
| 84 | + * Handler: mylibrary.handler |
| 85 | + * Runtime: nodejs12.x |
| 86 | + * CodeUri: backend/src/ |
| 87 | + * ``` |
| 88 | + * |
| 89 | + * And a file "mylibrary.js" exists in the folder "backend/src" (relative to the "template.yml" file). |
| 90 | + * Then the result of this predicate is a function exported as "handler" from "mylibrary.js". |
| 91 | + * The "mylibrary.js" file could for example look like: |
| 92 | + * |
| 93 | + * ```JavaScript |
| 94 | + * module.exports.handler = function (event) { ... } |
| 95 | + * ``` |
| 96 | + */ |
| 97 | + private DataFlow::FunctionNode getAServerlessHandler() { |
| 98 | + exists(File file, string handler, Module mod | hasServerlessHandler(file, handler) | |
| 99 | + mod.getFile() = file and |
| 100 | + result = mod.getAnExportedValue(handler).getAFunctionValue() |
| 101 | + ) |
| 102 | + } |
| 103 | + |
| 104 | + /** |
| 105 | + * A serverless request handler event, seen as a RemoteFlowSource. |
| 106 | + */ |
| 107 | + private class ServerlessHandlerEventAsRemoteFlow extends RemoteFlowSource { |
| 108 | + ServerlessHandlerEventAsRemoteFlow() { this = getAServerlessHandler().getParameter(0) } |
| 109 | + |
| 110 | + override string getSourceType() { result = "Serverless event" } |
| 111 | + } |
| 112 | +} |
0 commit comments