mirror of
https://github.com/github/codeql.git
synced 2026-07-07 12:35:33 +02:00
Compare commits
228 Commits
codeql-cli
...
nickrolfe/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea2814c03d | ||
|
|
2f1ba20165 | ||
|
|
8eddf62b22 | ||
|
|
81954c3a8d | ||
|
|
ced07b52a5 | ||
|
|
a61ec78f03 | ||
|
|
fe73601a4e | ||
|
|
681e58c8e0 | ||
|
|
cb3ebeedf9 | ||
|
|
db41ce5f76 | ||
|
|
6cfde70898 | ||
|
|
b4d762fb21 | ||
|
|
0c0ba925a7 | ||
|
|
f39ca1aad2 | ||
|
|
0a35f97074 | ||
|
|
c2710fb038 | ||
|
|
2c095cf166 | ||
|
|
23c19311fb | ||
|
|
4767d5a1ba | ||
|
|
43266b75a1 | ||
|
|
5f9a03f103 | ||
|
|
77401ded4e | ||
|
|
d44bf326f0 | ||
|
|
7e67338fb5 | ||
|
|
871b6515d5 | ||
|
|
1842bde879 | ||
|
|
c1a6ca5f94 | ||
|
|
486a394a7f | ||
|
|
8fabc06d37 | ||
|
|
cc958dc171 | ||
|
|
1399610bd4 | ||
|
|
5f96c92fac | ||
|
|
ed0325f162 | ||
|
|
7be106d7bb | ||
|
|
27be3dff54 | ||
|
|
8dae85e1b1 | ||
|
|
0a8ecd3cf7 | ||
|
|
388c9ffb74 | ||
|
|
ad8335d6f3 | ||
|
|
466eb4a845 | ||
|
|
a437fcbbcc | ||
|
|
4d0f6a0b96 | ||
|
|
a10370f813 | ||
|
|
b3f2159a7e | ||
|
|
17c80336f5 | ||
|
|
8d80e0332e | ||
|
|
a1d9228a66 | ||
|
|
694d6395d5 | ||
|
|
8c55a15fa6 | ||
|
|
6f74a2609c | ||
|
|
f54fc1a88d | ||
|
|
c2b7300709 | ||
|
|
e9e5d948b3 | ||
|
|
ca819573f5 | ||
|
|
3527897eff | ||
|
|
7620a6f653 | ||
|
|
aa53841466 | ||
|
|
018a76bb17 | ||
|
|
dcc76ddf36 | ||
|
|
0828474192 | ||
|
|
43a9b8960e | ||
|
|
cf23d338f3 | ||
|
|
6bc2fe513d | ||
|
|
b9bdee6651 | ||
|
|
03bf9eb166 | ||
|
|
bfe90413e2 | ||
|
|
dcbd82907f | ||
|
|
ec1d1eb547 | ||
|
|
962155fd61 | ||
|
|
9586259706 | ||
|
|
304203ad2f | ||
|
|
7b8603c89b | ||
|
|
f9b6ca76e5 | ||
|
|
fc00e56058 | ||
|
|
c9e5206396 | ||
|
|
541df9b550 | ||
|
|
336548f746 | ||
|
|
9474e63faf | ||
|
|
b136790efd | ||
|
|
0bd94a6307 | ||
|
|
4854679a40 | ||
|
|
39fb714ad1 | ||
|
|
e1bd4a78ff | ||
|
|
410167671f | ||
|
|
eebba36b18 | ||
|
|
98fc8812fc | ||
|
|
c779936ee8 | ||
|
|
c08c3955d6 | ||
|
|
78fc356feb | ||
|
|
dbd6607875 | ||
|
|
cc5f59f313 | ||
|
|
d4443592eb | ||
|
|
6b17890e4f | ||
|
|
eefa659503 | ||
|
|
fe789c8aa9 | ||
|
|
2f50549184 | ||
|
|
a1df1d1119 | ||
|
|
ee1c09329f | ||
|
|
d50816a284 | ||
|
|
0ee476129a | ||
|
|
d13f9d5d71 | ||
|
|
7fbe4f8547 | ||
|
|
592ce3ec58 | ||
|
|
22ff8c2c7e | ||
|
|
3e06455ac1 | ||
|
|
21066d277f | ||
|
|
5ba4f6dae8 | ||
|
|
a7a9428dc1 | ||
|
|
47c9b446f0 | ||
|
|
380070f2e4 | ||
|
|
33fdcf1e4f | ||
|
|
855d4c2ea1 | ||
|
|
43a82004b2 | ||
|
|
18c5a8c8da | ||
|
|
da8123072d | ||
|
|
f1144b9672 | ||
|
|
d748cb483d | ||
|
|
4c53c341f6 | ||
|
|
62a10e20b2 | ||
|
|
8ca7d7d775 | ||
|
|
9d277027a3 | ||
|
|
3dd61cadf4 | ||
|
|
9a186ba5d2 | ||
|
|
ee79834cc8 | ||
|
|
ae634367c9 | ||
|
|
2cc703387b | ||
|
|
f5301aa478 | ||
|
|
f7c47b6c75 | ||
|
|
f7dca4d70f | ||
|
|
1fa2144716 | ||
|
|
fd10947ca0 | ||
|
|
49aab51893 | ||
|
|
ea95e2e1d0 | ||
|
|
b9fc82a741 | ||
|
|
4cfaa86d5d | ||
|
|
5f17d8370c | ||
|
|
63dcce9a31 | ||
|
|
b5a3d3c488 | ||
|
|
301914d80c | ||
|
|
706d1d2eee | ||
|
|
0dbb03f732 | ||
|
|
7df7b92d86 | ||
|
|
7129002573 | ||
|
|
b3f1a513d1 | ||
|
|
9a0a9491da | ||
|
|
2566ae9889 | ||
|
|
db5f63b208 | ||
|
|
7facc63699 | ||
|
|
74d6061082 | ||
|
|
a4adf06713 | ||
|
|
d929b1338b | ||
|
|
64343e00f4 | ||
|
|
8a48708014 | ||
|
|
2bac181094 | ||
|
|
a4e35a97ea | ||
|
|
a51d713925 | ||
|
|
48c71c9407 | ||
|
|
033b239b22 | ||
|
|
47a4cac8ee | ||
|
|
d5791e2d56 | ||
|
|
02e11b7ee9 | ||
|
|
ac05577966 | ||
|
|
e5702d0e15 | ||
|
|
7fc9ae6c49 | ||
|
|
5d89a5d164 | ||
|
|
156bc34cda | ||
|
|
ad7c3e7217 | ||
|
|
539fbbc126 | ||
|
|
39406436bf | ||
|
|
9ed7aa9fae | ||
|
|
74641ccfee | ||
|
|
7d5dd384c3 | ||
|
|
7c3cadc9b6 | ||
|
|
e8e8da1b31 | ||
|
|
5d3232c614 | ||
|
|
96e66c4a50 | ||
|
|
0435105d16 | ||
|
|
6aab970a9e | ||
|
|
bd50fd7f1e | ||
|
|
11e39aa030 | ||
|
|
940254d251 | ||
|
|
b4869158f2 | ||
|
|
2f1cfa816f | ||
|
|
f8994d04d6 | ||
|
|
01da877d0e | ||
|
|
dd1a9a22e3 | ||
|
|
f5c6b45014 | ||
|
|
56060e0610 | ||
|
|
e43e5810cf | ||
|
|
02dd933e5f | ||
|
|
5db2f9a768 | ||
|
|
c1302a90e0 | ||
|
|
a7956ad422 | ||
|
|
82c9b8b494 | ||
|
|
c33690381e | ||
|
|
c1a2e2abe0 | ||
|
|
fd28397056 | ||
|
|
9cf48fc804 | ||
|
|
b1251f0c63 | ||
|
|
3c9e743495 | ||
|
|
17d139c87d | ||
|
|
280c959dc8 | ||
|
|
d92430b0e7 | ||
|
|
9e4116618a | ||
|
|
c1515db09c | ||
|
|
03d0f66247 | ||
|
|
6ea1aad5fc | ||
|
|
ce2edd4b28 | ||
|
|
ca074e2275 | ||
|
|
cf36333082 | ||
|
|
45dd38df6e | ||
|
|
e838b83f5f | ||
|
|
995f365568 | ||
|
|
c767f241ad | ||
|
|
f6c4b5c44b | ||
|
|
990747cd22 | ||
|
|
53729f99c5 | ||
|
|
bbe17b3667 | ||
|
|
83b720d730 | ||
|
|
3478e7e910 | ||
|
|
0456870136 | ||
|
|
ecb2114b7b | ||
|
|
8b36191023 | ||
|
|
059c4d38ad | ||
|
|
8aa2602d9e | ||
|
|
6bef71ea2c | ||
|
|
7bdec98e6f | ||
|
|
c012c235c6 |
77
.github/workflows/ql-for-ql-build.yml
vendored
77
.github/workflows/ql-for-ql-build.yml
vendored
@@ -10,9 +10,10 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
queries:
|
||||
runs-on: ubuntu-latest
|
||||
analyze:
|
||||
runs-on: ubuntu-latest-xl
|
||||
steps:
|
||||
### Build the queries ###
|
||||
- uses: actions/checkout@v3
|
||||
- name: Find codeql
|
||||
id: find-codeql
|
||||
@@ -48,11 +49,7 @@ jobs:
|
||||
name: query-pack-zip
|
||||
path: ${{ runner.temp }}/query-pack.zip
|
||||
|
||||
extractors:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
### Build the extractor ###
|
||||
- name: Cache entire extractor
|
||||
id: cache-extractor
|
||||
uses: actions/cache@v3
|
||||
@@ -96,15 +93,8 @@ jobs:
|
||||
ql/target/release/ql-extractor
|
||||
ql/target/release/ql-extractor.exe
|
||||
retention-days: 1
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
needs:
|
||||
- extractors
|
||||
- queries
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
### Package the queries and extractor ###
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: query-pack-zip
|
||||
@@ -132,16 +122,8 @@ jobs:
|
||||
name: codeql-ql-pack
|
||||
path: codeql-ql.zip
|
||||
retention-days: 1
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
folder: [cpp, csharp, java, javascript, python, ql, ruby, swift, go]
|
||||
|
||||
needs:
|
||||
- package
|
||||
|
||||
steps:
|
||||
### Run the analysis ###
|
||||
- name: Download pack
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@@ -161,14 +143,11 @@ jobs:
|
||||
env:
|
||||
PACK: ${{ runner.temp }}/pack
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Create CodeQL config file
|
||||
run: |
|
||||
echo "paths:" > ${CONF}
|
||||
echo " - ${FOLDER}" >> ${CONF}
|
||||
echo "paths-ignore:" >> ${CONF}
|
||||
echo " - ql/ql/test" >> ${CONF}
|
||||
echo " - \"*/ql/lib/upgrades/\"" >> ${CONF}
|
||||
echo "disable-default-queries: true" >> ${CONF}
|
||||
echo "packs:" >> ${CONF}
|
||||
echo " - codeql/ql" >> ${CONF}
|
||||
@@ -176,7 +155,6 @@ jobs:
|
||||
cat ${CONF}
|
||||
env:
|
||||
CONF: ./ql-for-ql-config.yml
|
||||
FOLDER: ${{ matrix.folder }}
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@aa93aea877e5fb8841bcb1193f672abf6e9f2980
|
||||
with:
|
||||
@@ -187,39 +165,24 @@ jobs:
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@aa93aea877e5fb8841bcb1193f672abf6e9f2980
|
||||
with:
|
||||
category: "ql-for-ql-${{ matrix.folder }}"
|
||||
category: "ql-for-ql"
|
||||
- name: Copy sarif file to CWD
|
||||
run: cp ../results/ql.sarif ./${{ matrix.folder }}.sarif
|
||||
run: cp ../results/ql.sarif ./ql-for-ql.sarif
|
||||
- name: Fixup the $scema in sarif # Until https://github.com/microsoft/sarif-vscode-extension/pull/436/ is part in a stable release
|
||||
run: |
|
||||
sed -i 's/\$schema.*/\$schema": "https:\/\/raw.githubusercontent.com\/oasis-tcs\/sarif-spec\/master\/Schemata\/sarif-schema-2.1.0",/' ${{ matrix.folder }}.sarif
|
||||
sed -i 's/\$schema.*/\$schema": "https:\/\/raw.githubusercontent.com\/oasis-tcs\/sarif-spec\/master\/Schemata\/sarif-schema-2.1.0",/' ql-for-ql.sarif
|
||||
- name: Sarif as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.folder }}.sarif
|
||||
path: ${{ matrix.folder }}.sarif
|
||||
|
||||
combine:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- analyze
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Make a folder for artifacts.
|
||||
run: mkdir -p results
|
||||
- name: Download all sarif files
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: results
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Combine all sarif files
|
||||
run: |
|
||||
node ./ql/scripts/merge-sarif.js results/**/*.sarif combined.sarif
|
||||
- name: Upload combined sarif file
|
||||
name: ql-for-ql.sarif
|
||||
path: ql-for-ql.sarif
|
||||
- name: Split out the sarif file into langs
|
||||
run: |
|
||||
mkdir split-sarif
|
||||
node ./ql/scripts/split-sarif.js ql-for-ql.sarif split-sarif
|
||||
- name: Upload langs as artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: combined.sarif
|
||||
path: combined.sarif
|
||||
name: ql-for-ql-langs
|
||||
path: split-sarif
|
||||
retention-days: 1
|
||||
4
cpp/ql/lib/change-notes/2022-06-24-unique-variable.md
Normal file
4
cpp/ql/lib/change-notes/2022-06-24-unique-variable.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: fix
|
||||
---
|
||||
* Under certain circumstances a variable declaration that is not also a definition could be associated with a `Variable` that did not have the definition as a `VariableDeclarationEntry`. This is now fixed, and a unique `Variable` will exist that has both the declaration and the definition as a `VariableDeclarationEntry`.
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/cpp-all
|
||||
version: 0.3.1
|
||||
version: 0.3.2-dev
|
||||
groups: cpp
|
||||
dbscheme: semmlecode.cpp.dbscheme
|
||||
extractor: cpp
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import semmle.code.cpp.Location
|
||||
private import semmle.code.cpp.Enclosing
|
||||
private import semmle.code.cpp.internal.ResolveClass
|
||||
private import semmle.code.cpp.internal.ResolveGlobalVariable
|
||||
|
||||
/**
|
||||
* Get the `Element` that represents this `@element`.
|
||||
@@ -28,9 +29,12 @@ Element mkElement(@element e) { unresolveElement(result) = e }
|
||||
pragma[inline]
|
||||
@element unresolveElement(Element e) {
|
||||
not result instanceof @usertype and
|
||||
not result instanceof @variable and
|
||||
result = e
|
||||
or
|
||||
e = resolveClass(result)
|
||||
or
|
||||
e = resolveGlobalVariable(result)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import semmle.code.cpp.Element
|
||||
import semmle.code.cpp.exprs.Access
|
||||
import semmle.code.cpp.Initializer
|
||||
private import semmle.code.cpp.internal.ResolveClass
|
||||
private import semmle.code.cpp.internal.ResolveGlobalVariable
|
||||
|
||||
/**
|
||||
* A C/C++ variable. For example, in the following code there are four
|
||||
@@ -32,6 +33,8 @@ private import semmle.code.cpp.internal.ResolveClass
|
||||
* can have multiple declarations.
|
||||
*/
|
||||
class Variable extends Declaration, @variable {
|
||||
Variable() { isVariable(underlyingElement(this)) }
|
||||
|
||||
override string getAPrimaryQlClass() { result = "Variable" }
|
||||
|
||||
/** Gets the initializer of this variable, if any. */
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
private predicate hasDefinition(@globalvariable g) {
|
||||
exists(@var_decl vd | var_decls(vd, g, _, _, _) | var_def(vd))
|
||||
}
|
||||
|
||||
pragma[noinline]
|
||||
private predicate onlyOneCompleteGlobalVariableExistsWithMangledName(@mangledname name) {
|
||||
strictcount(@globalvariable g | hasDefinition(g) and mangled_name(g, name)) = 1
|
||||
}
|
||||
|
||||
/** Holds if `g` is a unique global variable with a definition named `name`. */
|
||||
pragma[noinline]
|
||||
private predicate isGlobalWithMangledNameAndWithDefinition(@mangledname name, @globalvariable g) {
|
||||
hasDefinition(g) and
|
||||
mangled_name(g, name) and
|
||||
onlyOneCompleteGlobalVariableExistsWithMangledName(name)
|
||||
}
|
||||
|
||||
/** Holds if `g` is a global variable without a definition named `name`. */
|
||||
pragma[noinline]
|
||||
private predicate isGlobalWithMangledNameAndWithoutDefinition(@mangledname name, @globalvariable g) {
|
||||
not hasDefinition(g) and
|
||||
mangled_name(g, name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `incomplete` is a global variable without a definition, and there exists
|
||||
* a unique global variable `complete` with the same name that does have a definition.
|
||||
*/
|
||||
private predicate hasTwinWithDefinition(@globalvariable incomplete, @globalvariable complete) {
|
||||
exists(@mangledname name |
|
||||
not variable_instantiation(incomplete, complete) and
|
||||
isGlobalWithMangledNameAndWithoutDefinition(name, incomplete) and
|
||||
isGlobalWithMangledNameAndWithDefinition(name, complete)
|
||||
)
|
||||
}
|
||||
|
||||
import Cached
|
||||
|
||||
cached
|
||||
private module Cached {
|
||||
/**
|
||||
* If `v` is a global variable without a definition, and there exists a unique
|
||||
* global variable with the same name that does have a definition, then the
|
||||
* result is that unique global variable. Otherwise, the result is `v`.
|
||||
*/
|
||||
cached
|
||||
@variable resolveGlobalVariable(@variable v) {
|
||||
hasTwinWithDefinition(v, result)
|
||||
or
|
||||
not hasTwinWithDefinition(v, _) and
|
||||
result = v
|
||||
}
|
||||
|
||||
cached
|
||||
predicate isVariable(@variable v) {
|
||||
not v instanceof @globalvariable
|
||||
or
|
||||
v = resolveGlobalVariable(_)
|
||||
}
|
||||
}
|
||||
@@ -74,13 +74,12 @@ class ReturnStackAllocatedMemoryConfig extends MustFlowConfiguration {
|
||||
|
||||
from
|
||||
MustFlowPathNode source, MustFlowPathNode sink, VariableAddressInstruction var,
|
||||
ReturnStackAllocatedMemoryConfig conf, Function f
|
||||
ReturnStackAllocatedMemoryConfig conf
|
||||
where
|
||||
conf.hasFlowPath(source, sink) and
|
||||
conf.hasFlowPath(pragma[only_bind_into](source), pragma[only_bind_into](sink)) and
|
||||
source.getNode().asInstruction() = var and
|
||||
// Only raise an alert if we're returning from the _same_ callable as the on that
|
||||
// declared the stack variable.
|
||||
var.getEnclosingFunction() = pragma[only_bind_into](f) and
|
||||
sink.getNode().getEnclosingCallable() = pragma[only_bind_into](f)
|
||||
var.getEnclosingFunction() = sink.getNode().getEnclosingCallable()
|
||||
select sink.getNode(), source, sink, "May return stack-allocated memory from $@.", var.getAst(),
|
||||
var.getAst().toString()
|
||||
|
||||
@@ -77,7 +77,7 @@ class ExecState extends DataFlow::FlowState {
|
||||
ExecState() {
|
||||
this =
|
||||
"ExecState (" + fst.getLocation() + " | " + fst + ", " + snd.getLocation() + " | " + snd + ")" and
|
||||
interestingConcatenation(fst, snd)
|
||||
interestingConcatenation(pragma[only_bind_into](fst), pragma[only_bind_into](snd))
|
||||
}
|
||||
|
||||
DataFlow::Node getFstNode() { result = fst }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/cpp-queries
|
||||
version: 0.3.0
|
||||
version: 0.3.1-dev
|
||||
groups:
|
||||
- cpp
|
||||
- queries
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
| c.c:6:5:6:6 | ls | array of 4 {int} | 1 |
|
||||
| c.c:8:5:8:7 | iss | array of 4 {array of 2 {int}} | 1 |
|
||||
| c.c:12:11:12:11 | i | typedef {int} as "int_alias" | 1 |
|
||||
| c.h:4:12:4:13 | ks | array of {int} | 1 |
|
||||
| c.h:8:12:8:14 | iss | array of {array of 2 {int}} | 1 |
|
||||
| c.h:10:12:10:12 | i | int | 1 |
|
||||
| d.cpp:3:7:3:8 | xs | array of {int} | 1 |
|
||||
| d.h:3:14:3:15 | xs | array of 2 {int} | 1 |
|
||||
| file://:0:0:0:0 | (unnamed parameter 0) | reference to {const {struct __va_list_tag}} | 1 |
|
||||
| file://:0:0:0:0 | (unnamed parameter 0) | rvalue reference to {struct __va_list_tag} | 1 |
|
||||
| file://:0:0:0:0 | fp_offset | unsigned int | 1 |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/csharp-solorigate-all
|
||||
version: 1.2.1
|
||||
version: 1.2.2-dev
|
||||
groups:
|
||||
- csharp
|
||||
- solorigate
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/csharp-solorigate-queries
|
||||
version: 1.2.1
|
||||
version: 1.2.2-dev
|
||||
groups:
|
||||
- csharp
|
||||
- solorigate
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/csharp-all
|
||||
version: 0.3.1
|
||||
version: 0.3.2-dev
|
||||
groups: csharp
|
||||
dbscheme: semmlecode.csharp.dbscheme
|
||||
extractor: csharp
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
|
||||
{
|
||||
SymmetricKey aesKey = new SymmetricKey(kid: "symencryptionkey");
|
||||
|
||||
// BAD: Using the outdated client side encryption version V1_0
|
||||
BlobEncryptionPolicy uploadPolicy = new BlobEncryptionPolicy(key: aesKey, keyResolver: null);
|
||||
BlobRequestOptions uploadOptions = new BlobRequestOptions() { EncryptionPolicy = uploadPolicy };
|
||||
|
||||
MemoryStream stream = new MemoryStream(buffer);
|
||||
blob.UploadFromStream(stream, length: size, accessCondition: null, options: uploadOptions);
|
||||
}
|
||||
|
||||
var client = new BlobClient(myConnectionString, new SpecializedBlobClientOptions()
|
||||
{
|
||||
// BAD: Using an outdated SDK that does not support client side encryption version V2_0
|
||||
ClientSideEncryption = new ClientSideEncryptionOptions()
|
||||
{
|
||||
KeyEncryptionKey = myKey,
|
||||
KeyResolver = myKeyResolver,
|
||||
KeyWrapAlgorihm = myKeyWrapAlgorithm
|
||||
}
|
||||
});
|
||||
|
||||
var client = new BlobClient(myConnectionString, new SpecializedBlobClientOptions()
|
||||
{
|
||||
// BAD: Using the outdated client side encryption version V1_0
|
||||
ClientSideEncryption = new ClientSideEncryptionOptions(ClientSideEncryptionVersion.V1_0)
|
||||
{
|
||||
KeyEncryptionKey = myKey,
|
||||
KeyResolver = myKeyResolver,
|
||||
KeyWrapAlgorihm = myKeyWrapAlgorithm
|
||||
}
|
||||
});
|
||||
|
||||
var client = new BlobClient(myConnectionString, new SpecializedBlobClientOptions()
|
||||
{
|
||||
// GOOD: Using client side encryption version V2_0
|
||||
ClientSideEncryption = new ClientSideEncryptionOptions(ClientSideEncryptionVersion.V2_0)
|
||||
{
|
||||
KeyEncryptionKey = myKey,
|
||||
KeyResolver = myKeyResolver,
|
||||
KeyWrapAlgorihm = myKeyWrapAlgorithm
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
|
||||
<overview>
|
||||
<p>Azure Storage .NET, Java, and Python SDKs support encryption on the client with a customer-managed key that is maintained in Azure Key Vault or another key store.</p>
|
||||
<p>Current release versions of the Azure Storage SDKs use cipher block chaining (CBC mode) for client-side encryption (referred to as <code>v1</code>).</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>Consider switching to <code>v2</code> client-side encryption.</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<sample src="UnsafeUsageOfClientSideEncryptionVersion.cs" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
<li>
|
||||
<a href="http://aka.ms/azstorageclientencryptionblog">Azure Storage Client Encryption Blog.</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-30187">CVE-2022-30187</a>
|
||||
</li>
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @name Unsafe usage of v1 version of Azure Storage client-side encryption (CVE-2022-30187).
|
||||
* @description Unsafe usage of v1 version of Azure Storage client-side encryption, please refer to http://aka.ms/azstorageclientencryptionblog
|
||||
* @kind problem
|
||||
* @tags security
|
||||
* cryptography
|
||||
* external/cwe/cwe-327
|
||||
* @id cs/azure-storage/unsafe-usage-of-client-side-encryption-version
|
||||
* @problem.severity error
|
||||
* @precision high
|
||||
*/
|
||||
|
||||
import csharp
|
||||
|
||||
/**
|
||||
* Holds if `oc` is creating an object of type `c` = `Azure.Storage.ClientSideEncryptionOptions`
|
||||
* and `e` is the `version` argument to the constructor
|
||||
*/
|
||||
predicate isCreatingAzureClientSideEncryptionObject(ObjectCreation oc, Class c, Expr e) {
|
||||
exists(Parameter p | p.hasName("version") |
|
||||
c.hasQualifiedName("Azure.Storage.ClientSideEncryptionOptions") and
|
||||
oc.getTarget() = c.getAConstructor() and
|
||||
e = oc.getArgumentForParameter(p)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `oc` is an object creation of the outdated type `c` = `Microsoft.Azure.Storage.Blob.BlobEncryptionPolicy`
|
||||
*/
|
||||
predicate isCreatingOutdatedAzureClientSideEncryptionObject(ObjectCreation oc, Class c) {
|
||||
c.hasQualifiedName("Microsoft.Azure.Storage.Blob.BlobEncryptionPolicy") and
|
||||
oc.getTarget() = c.getAConstructor()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the Azure.Storage assembly for `c` is a version known to support
|
||||
* version 2+ for client-side encryption
|
||||
*/
|
||||
predicate doesAzureStorageAssemblySupportSafeClientSideEncryption(Assembly asm) {
|
||||
exists(int versionCompare |
|
||||
versionCompare = asm.getVersion().compareTo("12.12.0.0") and
|
||||
versionCompare >= 0
|
||||
) and
|
||||
asm.getName() = "Azure.Storage.Common"
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the Azure.Storage assembly for `c` is a version known to support
|
||||
* version 2+ for client-side encryption and if the argument for the constructor `version`
|
||||
* is set to a secure value.
|
||||
*/
|
||||
predicate isObjectCreationArgumentSafeAndUsingSafeVersionOfAssembly(Expr versionExpr, Assembly asm) {
|
||||
// Check if the Azure.Storage assembly version has the fix
|
||||
doesAzureStorageAssemblySupportSafeClientSideEncryption(asm) and
|
||||
// and that the version argument for the constructor is guaranteed to be Version2
|
||||
isExprAnAccessToSafeClientSideEncryptionVersionValue(versionExpr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the expression `e` is an access to a safe version of the enum `ClientSideEncryptionVersion`
|
||||
* or an equivalent numeric value
|
||||
*/
|
||||
predicate isExprAnAccessToSafeClientSideEncryptionVersionValue(Expr e) {
|
||||
exists(EnumConstant ec |
|
||||
ec.hasQualifiedName("Azure.Storage.ClientSideEncryptionVersion.V2_0") and
|
||||
ec.getAnAccess() = e
|
||||
)
|
||||
}
|
||||
|
||||
from Expr e, Class c, Assembly asm
|
||||
where
|
||||
asm = c.getLocation() and
|
||||
(
|
||||
exists(Expr e2 |
|
||||
isCreatingAzureClientSideEncryptionObject(e, c, e2) and
|
||||
not isObjectCreationArgumentSafeAndUsingSafeVersionOfAssembly(e2, asm)
|
||||
)
|
||||
or
|
||||
isCreatingOutdatedAzureClientSideEncryptionObject(e, c)
|
||||
)
|
||||
select e, "Unsafe usage of v1 version of Azure Storage client-side encryption."
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/csharp-queries
|
||||
version: 0.3.0
|
||||
version: 0.3.1-dev
|
||||
groups:
|
||||
- csharp
|
||||
- queries
|
||||
|
||||
@@ -2,7 +2,54 @@ function RegisterExtractorPack(id)
|
||||
local extractor = GetPlatformToolsDirectory() ..
|
||||
'Semmle.Extraction.CSharp.Driver'
|
||||
if OperatingSystem == 'windows' then extractor = extractor .. '.exe' end
|
||||
|
||||
function DotnetMatcherBuild(compilerName, compilerPath, compilerArguments,
|
||||
_languageId)
|
||||
if compilerName ~= 'dotnet' and compilerName ~= 'dotnet.exe' then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- The dotnet CLI has the following usage instructions:
|
||||
-- dotnet [sdk-options] [command] [command-options] [arguments]
|
||||
-- we are interested in dotnet build, which has the following usage instructions:
|
||||
-- dotnet [options] build [<PROJECT | SOLUTION>...]
|
||||
-- For now, parse the command line as follows:
|
||||
-- Everything that starts with `-` (or `/`) will be ignored.
|
||||
-- The first non-option argument is treated as the command.
|
||||
-- if that's `build`, we append `/p:UseSharedCompilation=false` to the command line,
|
||||
-- otherwise we do nothing.
|
||||
local match = false
|
||||
local argv = compilerArguments.argv
|
||||
if OperatingSystem == 'windows' then
|
||||
-- let's hope that this split matches the escaping rules `dotnet` applies to command line arguments
|
||||
-- or, at least, that it is close enough
|
||||
argv =
|
||||
NativeArgumentsToArgv(compilerArguments.nativeArgumentPointer)
|
||||
end
|
||||
for i, arg in ipairs(argv) do
|
||||
-- dotnet options start with either - or / (both are legal)
|
||||
local firstCharacter = string.sub(arg, 1, 1)
|
||||
if not (firstCharacter == '-') and not (firstCharacter == '/') then
|
||||
Log(1, 'Dotnet subcommand detected: %s', arg)
|
||||
if arg == 'build' then match = true end
|
||||
break
|
||||
end
|
||||
end
|
||||
if match then
|
||||
return {
|
||||
order = ORDER_REPLACE,
|
||||
invocation = BuildExtractorInvocation(id, compilerPath,
|
||||
compilerPath,
|
||||
compilerArguments, nil, {
|
||||
'/p:UseSharedCompilation=false'
|
||||
})
|
||||
}
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local windowsMatchers = {
|
||||
DotnetMatcherBuild,
|
||||
CreatePatternMatcher({'^dotnet%.exe$'}, MatchCompilerName, extractor, {
|
||||
prepend = {'--dotnetexec', '--cil'},
|
||||
order = ORDER_BEFORE
|
||||
@@ -10,22 +57,21 @@ function RegisterExtractorPack(id)
|
||||
CreatePatternMatcher({'^csc.*%.exe$'}, MatchCompilerName, extractor, {
|
||||
prepend = {'--compiler', '"${compiler}"', '--cil'},
|
||||
order = ORDER_BEFORE
|
||||
|
||||
}),
|
||||
CreatePatternMatcher({'^fakes.*%.exe$', 'moles.*%.exe'},
|
||||
MatchCompilerName, nil, {trace = false})
|
||||
}
|
||||
local posixMatchers = {
|
||||
CreatePatternMatcher({'^mcs%.exe$', '^csc%.exe$'}, MatchCompilerName,
|
||||
extractor, {
|
||||
prepend = {'--compiler', '"${compiler}"', '--cil'},
|
||||
order = ORDER_BEFORE
|
||||
|
||||
}),
|
||||
DotnetMatcherBuild,
|
||||
CreatePatternMatcher({'^mono', '^dotnet$'}, MatchCompilerName,
|
||||
extractor, {
|
||||
prepend = {'--dotnetexec', '--cil'},
|
||||
order = ORDER_BEFORE
|
||||
}),
|
||||
CreatePatternMatcher({'^mcs%.exe$', '^csc%.exe$'}, MatchCompilerName,
|
||||
extractor, {
|
||||
prepend = {'--compiler', '"${compiler}"', '--cil'},
|
||||
order = ORDER_BEFORE
|
||||
}), function(compilerName, compilerPath, compilerArguments, _languageId)
|
||||
if MatchCompilerName('^msbuild$', compilerName, compilerPath,
|
||||
compilerArguments) or
|
||||
@@ -49,7 +95,6 @@ function RegisterExtractorPack(id)
|
||||
else
|
||||
return posixMatchers
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- Return a list of minimum supported versions of the configuration file format
|
||||
|
||||
@@ -11,14 +11,17 @@ CodeQL.
|
||||
Languages and compilers
|
||||
#######################
|
||||
|
||||
CodeQL supports the following languages and compilers.
|
||||
The current versions of the CodeQL CLI (`changelog <https://github.com/github/codeql-cli-binaries/blob/main/CHANGELOG.md>`__, `releases <https://github.com/github/codeql-cli-binaries/releases>`__),
|
||||
CodeQL library packs (`source <https://github.com/github/codeql/tree/codeql-cli/latest>`__),
|
||||
and CodeQL bundle (`releases <https://github.com/github/codeql-action/releases>`__)
|
||||
support the following languages and compilers.
|
||||
|
||||
.. include:: ../support/reusables/versions-compilers.rst
|
||||
|
||||
Frameworks and libraries
|
||||
########################
|
||||
|
||||
The libraries and queries in the current version of CodeQL have been explicitly checked against the libraries and frameworks listed below.
|
||||
The current versions of the CodeQL library and query packs (`source <https://github.com/github/codeql/tree/codeql-cli/latest>`__) have been explicitly checked against the libraries and frameworks listed below.
|
||||
|
||||
.. pull-quote::
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ CodeQL query help for C and C++
|
||||
|
||||
.. include:: ../reusables/query-help-overview.rst
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/main/cpp/ql/examples>`__.
|
||||
These queries are published in the CodeQL query pack ``codeql/cpp-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/cpp/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/cpp/ql/src>`__).
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/codeql-cli/latest/cpp/ql/examples>`__.
|
||||
|
||||
.. include:: toc-cpp.rst
|
||||
|
||||
@@ -3,6 +3,8 @@ CodeQL query help for C#
|
||||
|
||||
.. include:: ../reusables/query-help-overview.rst
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/main/csharp/ql/examples>`__.
|
||||
These queries are published in the CodeQL query pack ``codeql/csharp-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/csharp/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/csharp/ql/src>`__).
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/codeql-cli/latest/csharp/ql/examples>`__.
|
||||
|
||||
.. include:: toc-csharp.rst
|
||||
@@ -3,6 +3,8 @@ CodeQL query help for Go
|
||||
|
||||
.. include:: ../reusables/query-help-overview.rst
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/main/go/ql/examples>`__.
|
||||
These queries are published in the CodeQL query pack ``codeql/go-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/go/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/go/ql/src>`__).
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/codeql-cli/latest/go/ql/examples>`__.
|
||||
|
||||
.. include:: toc-go.rst
|
||||
|
||||
@@ -3,6 +3,8 @@ CodeQL query help for Java
|
||||
|
||||
.. include:: ../reusables/query-help-overview.rst
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/main/java/ql/examples>`__.
|
||||
These queries are published in the CodeQL query pack ``codeql/java-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/java/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/java/ql/src>`__).
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/codeql-cli/latest/java/ql/examples>`__.
|
||||
|
||||
.. include:: toc-java.rst
|
||||
|
||||
@@ -3,6 +3,8 @@ CodeQL query help for JavaScript
|
||||
|
||||
.. include:: ../reusables/query-help-overview.rst
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/main/javascript/ql/examples>`__.
|
||||
These queries are published in the CodeQL query pack ``codeql/javascript-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/javascript/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/javascript/ql/src>`__).
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/codeql-cli/latest/javascript/ql/examples>`__.
|
||||
|
||||
.. include:: toc-javascript.rst
|
||||
@@ -3,6 +3,8 @@ CodeQL query help for Python
|
||||
|
||||
.. include:: ../reusables/query-help-overview.rst
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/main/python/ql/examples>`__.
|
||||
These queries are published in the CodeQL query pack ``codeql/python-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/python/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/python/ql/src>`__).
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/codeql-cli/latest/python/ql/examples>`__.
|
||||
|
||||
.. include:: toc-python.rst
|
||||
@@ -3,6 +3,8 @@ CodeQL query help for Ruby
|
||||
|
||||
.. include:: ../reusables/query-help-overview.rst
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/main/ruby/ql/examples>`__.
|
||||
These queries are published in the CodeQL query pack ``codeql/ruby-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/ruby/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/ruby/ql/src>`__).
|
||||
|
||||
For shorter queries that you can use as building blocks when writing your own queries, see the `example queries in the CodeQL repository <https://github.com/github/codeql/tree/codeql-cli/latest/ruby/ql/examples>`__.
|
||||
|
||||
.. include:: toc-ruby.rst
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
C and C++ built-in support
|
||||
================================
|
||||
|
||||
Provided by the current versions of the
|
||||
CodeQL query pack ``codeql/cpp-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/cpp/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/cpp/ql/src>`__)
|
||||
and the CodeQL library pack ``codeql/cpp-all`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/cpp/ql/lib/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/cpp/ql/lib>`__).
|
||||
|
||||
.. csv-table::
|
||||
:header-rows: 1
|
||||
:class: fullWidthTable
|
||||
@@ -14,6 +18,10 @@ C and C++ built-in support
|
||||
C# built-in support
|
||||
================================
|
||||
|
||||
Provided by the current versions of the
|
||||
CodeQL query pack ``codeql/csharp-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/csharp/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/csharp/ql/src>`__)
|
||||
and the CodeQL library pack ``codeql/csharp-all`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/csharp/ql/lib/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/csharp/ql/lib>`__).
|
||||
|
||||
.. csv-table::
|
||||
:header-rows: 1
|
||||
:class: fullWidthTable
|
||||
@@ -33,6 +41,10 @@ C# built-in support
|
||||
Go built-in support
|
||||
================================
|
||||
|
||||
Provided by the current versions of the
|
||||
CodeQL query pack ``codeql/go-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/go/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/go/ql/src>`__)
|
||||
and the CodeQL library pack ``codeql/go-all`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/go/ql/lib/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/go/ql/lib>`__).
|
||||
|
||||
.. csv-table::
|
||||
:header-rows: 1
|
||||
:class: fullWidthTable
|
||||
@@ -84,6 +96,10 @@ Go built-in support
|
||||
Java built-in support
|
||||
==================================
|
||||
|
||||
Provided by the current versions of the
|
||||
CodeQL query pack ``codeql/java-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/java/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/java/ql/src>`__)
|
||||
and the CodeQL library pack ``codeql/java-all`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/java/ql/lib/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/java/ql/lib>`__).
|
||||
|
||||
.. csv-table::
|
||||
:header-rows: 1
|
||||
:class: fullWidthTable
|
||||
@@ -113,6 +129,10 @@ Java built-in support
|
||||
JavaScript and TypeScript built-in support
|
||||
=======================================================
|
||||
|
||||
Provided by the current versions of the
|
||||
CodeQL query pack ``codeql/javascript-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/javascript/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/javascript/ql/src>`__)
|
||||
and the CodeQL library pack ``codeql/javascript-all`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/javascript/ql/lib/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/javascript/ql/lib>`__).
|
||||
|
||||
.. csv-table::
|
||||
:header-rows: 1
|
||||
:class: fullWidthTable
|
||||
@@ -156,6 +176,10 @@ JavaScript and TypeScript built-in support
|
||||
Python built-in support
|
||||
====================================
|
||||
|
||||
Provided by the current versions of the
|
||||
CodeQL query pack ``codeql/python-queries`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/python/ql/src/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/python/ql/src>`__)
|
||||
and the CodeQL library pack ``codeql/python-all`` (`changelog <https://github.com/github/codeql/tree/codeql-cli/latest/python/ql/lib/CHANGELOG.md>`__, `source <https://github.com/github/codeql/tree/codeql-cli/latest/python/ql/lib>`__).
|
||||
|
||||
.. csv-table::
|
||||
:header-rows: 1
|
||||
:class: fullWidthTable
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/go-all
|
||||
version: 0.2.1
|
||||
version: 0.2.2-dev
|
||||
groups: go
|
||||
dbscheme: go.dbscheme
|
||||
extractor: go
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/go-queries
|
||||
version: 0.2.1
|
||||
version: 0.2.2-dev
|
||||
groups:
|
||||
- go
|
||||
- queries
|
||||
|
||||
@@ -36,7 +36,7 @@ java.lang,13,,58,,,,,,,,,,,8,,,,,4,,,1,,,,,,,,,,,,,,,46,12
|
||||
java.net,10,3,7,,,,,,,,,,,,,,10,,,,,,,,,,,,,,,,,,,3,7,
|
||||
java.nio,15,,6,,13,,,,,,,,,,,,,,,,,,,,,,,,2,,,,,,,,6,
|
||||
java.sql,11,,,,,,,,,4,,,,,,,,,,,,,,,,7,,,,,,,,,,,,
|
||||
java.util,44,,438,,,,,,,,,,,34,,,,,,5,2,,1,2,,,,,,,,,,,,,24,414
|
||||
java.util,44,,441,,,,,,,,,,,34,,,,,,5,2,,1,2,,,,,,,,,,,,,24,417
|
||||
javax.faces.context,2,7,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,2,,,,7,,
|
||||
javax.jms,,9,57,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,9,57,
|
||||
javax.json,,,123,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,100,23
|
||||
|
||||
|
@@ -15,9 +15,9 @@ Java framework & library support
|
||||
`Apache HttpComponents <https://hc.apache.org/>`_,"``org.apache.hc.core5.*``, ``org.apache.http``",5,136,28,,,3,,,,25
|
||||
`Google Guava <https://guava.dev/>`_,``com.google.common.*``,,728,39,,6,,,,,
|
||||
`JSON-java <https://github.com/stleary/JSON-java>`_,``org.json``,,236,,,,,,,,
|
||||
Java Standard Library,``java.*``,3,549,130,28,,,7,,,10
|
||||
Java Standard Library,``java.*``,3,552,130,28,,,7,,,10
|
||||
Java extensions,"``javax.*``, ``jakarta.*``",63,609,32,,,4,,1,1,2
|
||||
`Spring <https://spring.io/>`_,``org.springframework.*``,29,476,101,,,,19,14,,29
|
||||
Others,"``androidx.slice``, ``cn.hutool.core.codec``, ``com.esotericsoftware.kryo.io``, ``com.esotericsoftware.kryo5.io``, ``com.fasterxml.jackson.core``, ``com.fasterxml.jackson.databind``, ``com.opensymphony.xwork2.ognl``, ``com.rabbitmq.client``, ``com.unboundid.ldap.sdk``, ``com.zaxxer.hikari``, ``flexjson``, ``groovy.lang``, ``groovy.util``, ``jodd.json``, ``kotlin.jvm.internal``, ``net.sf.saxon.s9api``, ``ognl``, ``okhttp3``, ``org.apache.commons.codec``, ``org.apache.commons.jexl2``, ``org.apache.commons.jexl3``, ``org.apache.commons.logging``, ``org.apache.commons.ognl``, ``org.apache.directory.ldap.client.api``, ``org.apache.ibatis.jdbc``, ``org.apache.log4j``, ``org.apache.logging.log4j``, ``org.apache.shiro.codec``, ``org.apache.shiro.jndi``, ``org.codehaus.groovy.control``, ``org.dom4j``, ``org.hibernate``, ``org.jboss.logging``, ``org.jdbi.v3.core``, ``org.jooq``, ``org.mvel2``, ``org.scijava.log``, ``org.slf4j``, ``org.xml.sax``, ``org.xmlpull.v1``, ``play.mvc``, ``ratpack.core.form``, ``ratpack.core.handling``, ``ratpack.core.http``, ``ratpack.exec``, ``ratpack.form``, ``ratpack.func``, ``ratpack.handling``, ``ratpack.http``, ``ratpack.util``, ``retrofit2``",65,395,932,,,,14,18,,3
|
||||
Totals,,217,6410,1474,117,6,10,107,33,1,84
|
||||
Totals,,217,6413,1474,117,6,10,107,33,1,84
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/java-all
|
||||
version: 0.3.1
|
||||
version: 0.3.2-dev
|
||||
groups: java
|
||||
dbscheme: config/semmlecode.dbscheme
|
||||
extractor: java
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
// BAD: Using an outdated SDK that does not support client side encryption version V2_0
|
||||
new EncryptedBlobClientBuilder()
|
||||
.blobClient(blobClient)
|
||||
.key(resolver.buildAsyncKeyEncryptionKey(keyid).block(), keyWrapAlgorithm)
|
||||
.buildEncryptedBlobClient()
|
||||
.uploadWithResponse(new BlobParallelUploadOptions(data)
|
||||
.setMetadata(metadata)
|
||||
.setHeaders(headers)
|
||||
.setTags(tags)
|
||||
.setTier(tier)
|
||||
.setRequestConditions(requestConditions)
|
||||
.setComputeMd5(computeMd5)
|
||||
.setParallelTransferOptions(parallelTransferOptions),
|
||||
timeout, context);
|
||||
|
||||
// BAD: Using the deprecatedd client side encryption version V1_0
|
||||
new EncryptedBlobClientBuilder(EncryptionVersion.V1)
|
||||
.blobClient(blobClient)
|
||||
.key(resolver.buildAsyncKeyEncryptionKey(keyid).block(), keyWrapAlgorithm)
|
||||
.buildEncryptedBlobClient()
|
||||
.uploadWithResponse(new BlobParallelUploadOptions(data)
|
||||
.setMetadata(metadata)
|
||||
.setHeaders(headers)
|
||||
.setTags(tags)
|
||||
.setTier(tier)
|
||||
.setRequestConditions(requestConditions)
|
||||
.setComputeMd5(computeMd5)
|
||||
.setParallelTransferOptions(parallelTransferOptions),
|
||||
timeout, context);
|
||||
|
||||
|
||||
// GOOD: Using client side encryption version V2_0
|
||||
new EncryptedBlobClientBuilder(EncryptionVersion.V2)
|
||||
.blobClient(blobClient)
|
||||
.key(resolver.buildAsyncKeyEncryptionKey(keyid).block(), keyWrapAlgorithm)
|
||||
.buildEncryptedBlobClient()
|
||||
.uploadWithResponse(new BlobParallelUploadOptions(data)
|
||||
.setMetadata(metadata)
|
||||
.setHeaders(headers)
|
||||
.setTags(tags)
|
||||
.setTier(tier)
|
||||
.setRequestConditions(requestConditions)
|
||||
.setComputeMd5(computeMd5)
|
||||
.setParallelTransferOptions(parallelTransferOptions),
|
||||
timeout, context);
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
|
||||
<overview>
|
||||
<p>Azure Storage .NET, Java, and Python SDKs support encryption on the client with a customer-managed key that is maintained in Azure Key Vault or another key store.</p>
|
||||
<p>The Azure Storage SDK version 12.18.0 or later supports version <code>V2</code> for client-side encryption. All previous versions of Azure Storage SDK only support client-side encryption <code>V1</code> which is unsafe.</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>Consider switching to <code>V2</code> client-side encryption.</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<sample src="UnsafeUsageOfClientSideEncryptionVersion.java" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
<li>
|
||||
<a href="http://aka.ms/azstorageclientencryptionblog">Azure Storage Client Encryption Blog.</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-30187">CVE-2022-30187</a>
|
||||
</li>
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @name Unsafe usage of v1 version of Azure Storage client-side encryption (CVE-2022-30187).
|
||||
* @description Unsafe usage of v1 version of Azure Storage client-side encryption, please refer to http://aka.ms/azstorageclientencryptionblog
|
||||
* @kind problem
|
||||
* @tags security
|
||||
* cryptography
|
||||
* external/cwe/cwe-327
|
||||
* @id java/azure-storage/unsafe-client-side-encryption-in-use
|
||||
* @problem.severity error
|
||||
* @precision high
|
||||
*/
|
||||
|
||||
import java
|
||||
import semmle.code.java.dataflow.DataFlow
|
||||
|
||||
/**
|
||||
* Holds if `call` is an object creation for a class `EncryptedBlobClientBuilder`
|
||||
* that takes no arguments, which means that it is using V1 encryption.
|
||||
*/
|
||||
predicate isCreatingOutdatedAzureClientSideEncryptionObject(Call call, Class c) {
|
||||
exists(string package, string type, Constructor constructor |
|
||||
c.hasQualifiedName(package, type) and
|
||||
c.getAConstructor() = constructor and
|
||||
call.getCallee() = constructor and
|
||||
(
|
||||
type = "EncryptedBlobClientBuilder" and
|
||||
package = "com.azure.storage.blob.specialized.cryptography" and
|
||||
constructor.hasNoParameters()
|
||||
or
|
||||
type = "BlobEncryptionPolicy" and package = "com.microsoft.azure.storage.blob"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `call` is an object creation for a class `EncryptedBlobClientBuilder`
|
||||
* that takes `versionArg` as the argument specifying the encryption version.
|
||||
*/
|
||||
predicate isCreatingAzureClientSideEncryptionObjectNewVersion(Call call, Class c, Expr versionArg) {
|
||||
exists(string package, string type, Constructor constructor |
|
||||
c.hasQualifiedName(package, type) and
|
||||
c.getAConstructor() = constructor and
|
||||
call.getCallee() = constructor and
|
||||
type = "EncryptedBlobClientBuilder" and
|
||||
package = "com.azure.storage.blob.specialized.cryptography" and
|
||||
versionArg = call.getArgument(0)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A dataflow config that tracks `EncryptedBlobClientBuilder.version` argument initialization.
|
||||
*/
|
||||
private class EncryptedBlobClientBuilderSafeEncryptionVersionConfig extends DataFlow::Configuration {
|
||||
EncryptedBlobClientBuilderSafeEncryptionVersionConfig() {
|
||||
this = "EncryptedBlobClientBuilderSafeEncryptionVersionConfig"
|
||||
}
|
||||
|
||||
override predicate isSource(DataFlow::Node source) {
|
||||
exists(FieldRead fr, Field f | fr = source.asExpr() |
|
||||
f.getAnAccess() = fr and
|
||||
f.hasQualifiedName("com.azure.storage.blob.specialized.cryptography", "EncryptionVersion",
|
||||
"V2")
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
isCreatingAzureClientSideEncryptionObjectNewVersion(_, _, sink.asExpr())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `call` is an object creation for a class `EncryptedBlobClientBuilder`
|
||||
* that takes `versionArg` as the argument specifying the encryption version, and that version is safe.
|
||||
*/
|
||||
predicate isCreatingSafeAzureClientSideEncryptionObject(Call call, Class c, Expr versionArg) {
|
||||
isCreatingAzureClientSideEncryptionObjectNewVersion(call, c, versionArg) and
|
||||
exists(EncryptedBlobClientBuilderSafeEncryptionVersionConfig config, DataFlow::Node sink |
|
||||
sink.asExpr() = versionArg
|
||||
|
|
||||
config.hasFlow(_, sink)
|
||||
)
|
||||
}
|
||||
|
||||
from Expr e, Class c
|
||||
where
|
||||
exists(Expr argVersion |
|
||||
isCreatingAzureClientSideEncryptionObjectNewVersion(e, c, argVersion) and
|
||||
not isCreatingSafeAzureClientSideEncryptionObject(e, c, argVersion)
|
||||
)
|
||||
or
|
||||
isCreatingOutdatedAzureClientSideEncryptionObject(e, c)
|
||||
select e, "Unsafe usage of v1 version of Azure Storage client-side encryption."
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/java-queries
|
||||
version: 0.3.0
|
||||
version: 0.3.1-dev
|
||||
groups:
|
||||
- java
|
||||
- queries
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/javascript-all
|
||||
version: 0.2.1
|
||||
version: 0.2.2-dev
|
||||
groups: javascript
|
||||
dbscheme: semmlecode.javascript.dbscheme
|
||||
extractor: javascript
|
||||
|
||||
@@ -28,6 +28,9 @@ module Actions {
|
||||
/** Gets the `jobs` mapping from job IDs to job definitions in this workflow. */
|
||||
YAMLMapping getJobs() { result = this.lookup("jobs") }
|
||||
|
||||
/** Gets the name of the workflow. */
|
||||
string getName() { result = this.lookup("name").(YAMLString).getValue() }
|
||||
|
||||
/** Gets the name of the workflow file. */
|
||||
string getFileName() { result = this.getFile().getBaseName() }
|
||||
|
||||
@@ -129,6 +132,9 @@ module Actions {
|
||||
|
||||
/** Gets the value of the `if` field in this step, if any. */
|
||||
StepIf getIf() { result.getStep() = this }
|
||||
|
||||
/** Gets the ID of this step, if any. */
|
||||
string getId() { result = this.lookup("id").(YAMLString).getValue() }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -148,6 +148,18 @@ module Routing {
|
||||
this instanceof MkRouter
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `mayResumeDispatch` but without the assumption that functions with an unknown
|
||||
* implementation invoke their continuation.
|
||||
*/
|
||||
predicate definitelyResumesDispatch() {
|
||||
this.getLastChild().definitelyResumesDispatch()
|
||||
or
|
||||
exists(this.(RouteHandler).getAContinuationInvocation())
|
||||
or
|
||||
this instanceof MkRouter
|
||||
}
|
||||
|
||||
/** Gets the parent of this node, provided that this node may invoke its continuation. */
|
||||
private Node getContinuationParent() {
|
||||
result = this.getParent() and
|
||||
|
||||
@@ -312,7 +312,7 @@ class TypeBackTracker extends TTypeBackTracker {
|
||||
* result = < some API call >.getArgument(< n >)
|
||||
* or
|
||||
* exists (DataFlow::TypeBackTracker t2 |
|
||||
* t = t2.smallstep(result, myType(t2))
|
||||
* t2 = t.smallstep(result, myType(t2))
|
||||
* )
|
||||
* }
|
||||
*
|
||||
|
||||
@@ -33,6 +33,11 @@ module Express {
|
||||
or
|
||||
// `app = [new] express.Router()`
|
||||
result = DataFlow::moduleMember("express", "Router").getAnInvocation()
|
||||
or
|
||||
exists(DataFlow::SourceNode app |
|
||||
app.hasUnderlyingType("probot/lib/application", "Application") and
|
||||
result = app.getAMethodCall("route")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1043,4 +1048,22 @@ module Express {
|
||||
|
||||
override DataFlow::SourceNode getOutput() { result = this.getCallback(2).getParameter(1) }
|
||||
}
|
||||
|
||||
private class ResumeDispatchRefinement extends Routing::RouteHandler {
|
||||
ResumeDispatchRefinement() { this.getFunction() instanceof RouteHandler }
|
||||
|
||||
override predicate mayResumeDispatch() { this.getAParameter().getName() = "next" }
|
||||
|
||||
override predicate definitelyResumesDispatch() { this.getAParameter().getName() = "next" }
|
||||
}
|
||||
|
||||
private class ExpressStaticResumeDispatchRefinement extends Routing::Node {
|
||||
ExpressStaticResumeDispatchRefinement() {
|
||||
this = Routing::getNode(DataFlow::moduleMember("express", "static").getACall())
|
||||
}
|
||||
|
||||
override predicate mayResumeDispatch() { none() }
|
||||
|
||||
override predicate definitelyResumesDispatch() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ module UnsafeHtmlConstruction {
|
||||
t.start() and
|
||||
result = sink
|
||||
or
|
||||
exists(DataFlow::TypeBackTracker t2 | t = t2.smallstep(result, isUsedInXssSink(t2, sink)))
|
||||
exists(DataFlow::TypeBackTracker t2 | t2 = t.smallstep(result, isUsedInXssSink(t2, sink)))
|
||||
or
|
||||
exists(DataFlow::TypeBackTracker t2 |
|
||||
t.continue() = t2 and
|
||||
|
||||
@@ -144,6 +144,9 @@ predicate whitelisted(UnusedLocal v) {
|
||||
// exclude variables mentioned in JSDoc comments in externs
|
||||
mentionedInJSDocComment(v)
|
||||
or
|
||||
// the attributes in .vue files are not extracted, so we can get false positives in those.
|
||||
v.getADeclaration().getFile().getExtension() = "vue"
|
||||
or
|
||||
// exclude variables used to filter out unwanted properties
|
||||
isPropertyFilter(v)
|
||||
or
|
||||
|
||||
@@ -50,7 +50,7 @@ private DataFlow::Node endsInCodeInjectionSink(DataFlow::TypeBackTracker t) {
|
||||
not result instanceof StringOps::ConcatenationRoot // the heuristic CodeInjection sink looks for string-concats, we are not interrested in those here.
|
||||
)
|
||||
or
|
||||
exists(DataFlow::TypeBackTracker t2 | t = t2.smallstep(result, endsInCodeInjectionSink(t2)))
|
||||
exists(DataFlow::TypeBackTracker t2 | t2 = t.smallstep(result, endsInCodeInjectionSink(t2)))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Using a case-sensitive regular expression path in a middleware route enables an attacker to bypass that middleware
|
||||
when accessing an endpoint with a case-insensitive path.
|
||||
Paths specified using a string are case-insensitive, whereas regular expressions are case-sensitive by default.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
When using a regular expression as a middleware path, make sure the regular expression is
|
||||
case-insensitive by adding the <code>i</code> flag.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The following example restricts access to paths in the <code>/admin</code> path to users logged in as
|
||||
administrators:
|
||||
</p>
|
||||
<sample src="examples/CaseSensitiveMiddlewarePath.js" />
|
||||
<p>
|
||||
A path such as <code>/admin/users/45</code> can only be accessed by an administrator. However, the path
|
||||
<code>/ADMIN/USERS/45</code> can be accessed by anyone because the upper-case path doesn't match the case-sensitive regular expression, whereas
|
||||
Express considers it to match the path string <code>/admin/users</code>.
|
||||
</p>
|
||||
<p>
|
||||
The issue can be fixed by adding the <code>i</code> flag to the regular expression:
|
||||
</p>
|
||||
<sample src="examples/CaseSensitiveMiddlewarePathGood.js" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>
|
||||
MDN
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#advanced_searching_with_flags">Regular Expression Flags</a>.
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @name Case-sensitive middleware path
|
||||
* @description Middleware with case-sensitive paths do not protect endpoints with case-insensitive paths.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @security-severity 7.3
|
||||
* @precision high
|
||||
* @id js/case-sensitive-middleware-path
|
||||
* @tags security
|
||||
* external/cwe/cwe-178
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* Converts `s` to upper case, or to lower-case if it was already upper case.
|
||||
*/
|
||||
bindingset[s]
|
||||
string toOtherCase(string s) {
|
||||
if s.regexpMatch(".*[a-z].*") then result = s.toUpperCase() else result = s.toLowerCase()
|
||||
}
|
||||
|
||||
RegExpCharacterClass getEnclosingClass(RegExpTerm term) {
|
||||
term = result.getAChild()
|
||||
or
|
||||
term = result.getAChild().(RegExpRange).getAChild()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `term` seems to distinguish between upper and lower case letters, assuming the `i` flag is not present.
|
||||
*/
|
||||
pragma[inline]
|
||||
predicate isLikelyCaseSensitiveRegExp(RegExpTerm term) {
|
||||
exists(RegExpConstant const |
|
||||
const = term.getAChild*() and
|
||||
const.getValue().regexpMatch(".*[a-zA-Z].*") and
|
||||
not getEnclosingClass(const).getAChild().(RegExpConstant).getValue() =
|
||||
toOtherCase(const.getValue()) and
|
||||
not const.getParent*() instanceof RegExpNegativeLookahead and
|
||||
not const.getParent*() instanceof RegExpNegativeLookbehind
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a string matched by `term`, or part of such a string.
|
||||
*/
|
||||
string getExampleString(RegExpTerm term) {
|
||||
result = term.getAMatchedString()
|
||||
or
|
||||
// getAMatchedString does not recurse into sequences. Perform one step manually.
|
||||
exists(RegExpSequence seq | seq = term |
|
||||
result =
|
||||
strictconcat(RegExpTerm child, int i, string text |
|
||||
child = seq.getChild(i) and
|
||||
(
|
||||
text = child.getAMatchedString()
|
||||
or
|
||||
not exists(child.getAMatchedString()) and
|
||||
text = ""
|
||||
)
|
||||
|
|
||||
text order by i
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
string getCaseSensitiveBypassExample(RegExpTerm term) {
|
||||
exists(string example |
|
||||
example = getExampleString(term) and
|
||||
result = toOtherCase(example) and
|
||||
result != example // getting an example string is approximate; ensure we got a proper case-change example
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `setup` has a path-argument `arg` referring to the given case-sensitive `regexp`.
|
||||
*/
|
||||
predicate isCaseSensitiveMiddleware(
|
||||
Routing::RouteSetup setup, DataFlow::RegExpCreationNode regexp, DataFlow::Node arg
|
||||
) {
|
||||
exists(DataFlow::MethodCallNode call |
|
||||
setup = Routing::getRouteSetupNode(call) and
|
||||
(
|
||||
setup.definitelyResumesDispatch()
|
||||
or
|
||||
// If applied to all HTTP methods, be a bit more lenient in detecting middleware
|
||||
setup.mayResumeDispatch() and
|
||||
not exists(setup.getOwnHttpMethod())
|
||||
) and
|
||||
arg = call.getArgument(0) and
|
||||
regexp.getAReference().flowsTo(arg) and
|
||||
isLikelyCaseSensitiveRegExp(regexp.getRoot()) and
|
||||
exists(string flags |
|
||||
flags = regexp.getFlags() and
|
||||
not RegExp::isIgnoreCase(flags)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
predicate isGuardedCaseInsensitiveEndpoint(
|
||||
Routing::RouteSetup endpoint, Routing::RouteSetup middleware
|
||||
) {
|
||||
isCaseSensitiveMiddleware(middleware, _, _) and
|
||||
exists(DataFlow::MethodCallNode call |
|
||||
endpoint = Routing::getRouteSetupNode(call) and
|
||||
endpoint.isGuardedByNode(middleware) and
|
||||
call.getArgument(0).mayHaveStringValue(_)
|
||||
)
|
||||
}
|
||||
|
||||
from
|
||||
DataFlow::RegExpCreationNode regexp, Routing::RouteSetup middleware, Routing::RouteSetup endpoint,
|
||||
DataFlow::Node arg, string example
|
||||
where
|
||||
isCaseSensitiveMiddleware(middleware, regexp, arg) and
|
||||
example = getCaseSensitiveBypassExample(regexp.getRoot()) and
|
||||
isGuardedCaseInsensitiveEndpoint(endpoint, middleware) and
|
||||
exists(endpoint.getRelativePath().toLowerCase().indexOf(example.toLowerCase()))
|
||||
select arg,
|
||||
"This route uses a case-sensitive path $@, but is guarding a case-insensitive path $@. A path such as '"
|
||||
+ example + "' will bypass the middleware.", regexp, "pattern", endpoint, "here"
|
||||
@@ -0,0 +1,13 @@
|
||||
const app = require('express')();
|
||||
|
||||
app.use(/\/admin\/.*/, (req, res, next) => {
|
||||
if (!req.user.isAdmin) {
|
||||
res.status(401).send('Unauthorized');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/users/:id', (req, res) => {
|
||||
res.send(app.database.users[req.params.id]);
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
const app = require('express')();
|
||||
|
||||
app.use(/\/admin\/.*/i, (req, res, next) => {
|
||||
if (!req.user.isAdmin) {
|
||||
res.status(401).send('Unauthorized');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/admin/users/:id', (req, res) => {
|
||||
res.send(app.database.users[req.params.id]);
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: newQuery
|
||||
---
|
||||
|
||||
- A new query "Case-sensitive middleware path" (`js/case-sensitive-middleware-path`) has been added.
|
||||
It highlights middleware routes that can be bypassed due to having a case-sensitive regular expression path.
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/javascript-queries
|
||||
version: 0.3.0
|
||||
version: 0.3.1-dev
|
||||
groups:
|
||||
- javascript
|
||||
- queries
|
||||
|
||||
@@ -50,6 +50,12 @@ nodes
|
||||
| main.js:79:34:79:36 | val |
|
||||
| main.js:81:35:81:37 | val |
|
||||
| main.js:81:35:81:37 | val |
|
||||
| main.js:89:21:89:21 | x |
|
||||
| main.js:90:23:90:23 | x |
|
||||
| main.js:90:23:90:23 | x |
|
||||
| main.js:93:43:93:43 | x |
|
||||
| main.js:93:43:93:43 | x |
|
||||
| main.js:94:31:94:31 | x |
|
||||
| typed.ts:1:39:1:39 | s |
|
||||
| typed.ts:1:39:1:39 | s |
|
||||
| typed.ts:2:29:2:29 | s |
|
||||
@@ -115,6 +121,11 @@ edges
|
||||
| main.js:79:34:79:36 | val | main.js:81:35:81:37 | val |
|
||||
| main.js:79:34:79:36 | val | main.js:81:35:81:37 | val |
|
||||
| main.js:79:34:79:36 | val | main.js:81:35:81:37 | val |
|
||||
| main.js:89:21:89:21 | x | main.js:90:23:90:23 | x |
|
||||
| main.js:89:21:89:21 | x | main.js:90:23:90:23 | x |
|
||||
| main.js:93:43:93:43 | x | main.js:94:31:94:31 | x |
|
||||
| main.js:93:43:93:43 | x | main.js:94:31:94:31 | x |
|
||||
| main.js:94:31:94:31 | x | main.js:89:21:89:21 | x |
|
||||
| typed.ts:1:39:1:39 | s | typed.ts:2:29:2:29 | s |
|
||||
| typed.ts:1:39:1:39 | s | typed.ts:2:29:2:29 | s |
|
||||
| typed.ts:1:39:1:39 | s | typed.ts:2:29:2:29 | s |
|
||||
@@ -141,5 +152,6 @@ edges
|
||||
| main.js:62:19:62:31 | settings.name | main.js:56:28:56:34 | options | main.js:62:19:62:31 | settings.name | $@ based on $@ might later cause $@. | main.js:62:19:62:31 | settings.name | HTML construction | main.js:56:28:56:34 | options | library input | main.js:62:11:62:40 | "<b>" + ... "</b>" | cross-site scripting |
|
||||
| main.js:67:63:67:69 | attrVal | main.js:66:35:66:41 | attrVal | main.js:67:63:67:69 | attrVal | $@ based on $@ might later cause $@. | main.js:67:63:67:69 | attrVal | HTML construction | main.js:66:35:66:41 | attrVal | library input | main.js:67:47:67:78 | "<img a ... "\\"/>" | cross-site scripting |
|
||||
| main.js:81:35:81:37 | val | main.js:79:34:79:36 | val | main.js:81:35:81:37 | val | $@ based on $@ might later cause $@. | main.js:81:35:81:37 | val | HTML construction | main.js:79:34:79:36 | val | library input | main.js:81:24:81:49 | "<span> ... /span>" | cross-site scripting |
|
||||
| main.js:90:23:90:23 | x | main.js:93:43:93:43 | x | main.js:90:23:90:23 | x | $@ based on $@ might later cause $@. | main.js:90:23:90:23 | x | HTML construction | main.js:93:43:93:43 | x | library input | main.js:94:20:94:32 | createHTML(x) | cross-site scripting |
|
||||
| typed.ts:2:29:2:29 | s | typed.ts:1:39:1:39 | s | typed.ts:2:29:2:29 | s | $@ based on $@ might later cause $@. | typed.ts:2:29:2:29 | s | HTML construction | typed.ts:1:39:1:39 | s | library input | typed.ts:3:31:3:34 | html | cross-site scripting |
|
||||
| typed.ts:8:40:8:40 | s | typed.ts:6:43:6:43 | s | typed.ts:8:40:8:40 | s | $@ based on $@ might later cause $@. | typed.ts:8:40:8:40 | s | HTML construction | typed.ts:6:43:6:43 | s | library input | typed.ts:8:29:8:52 | "<span> ... /span>" | cross-site scripting |
|
||||
|
||||
@@ -85,3 +85,11 @@ module.exports.types = function (val) {
|
||||
$("#foo").html("<span>" + val + "</span>"); // OK
|
||||
}
|
||||
}
|
||||
|
||||
function createHTML(x) {
|
||||
return "<span>" + x + "</span>"; // NOT OK
|
||||
}
|
||||
|
||||
module.exports.usesCreateHTML = function (x) {
|
||||
$("#foo").html(createHTML(x));
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
| tst.js:8:9:8:19 | /\\/foo\\/.*/ | This route uses a case-sensitive path $@, but is guarding a case-insensitive path $@. A path such as '/FOO/' will bypass the middleware. | tst.js:8:9:8:19 | /\\/foo\\/.*/ | pattern | tst.js:60:1:61:2 | app.get ... ware\\n}) | here |
|
||||
| tst.js:14:5:14:28 | new Reg ... (.*)?') | This route uses a case-sensitive path $@, but is guarding a case-insensitive path $@. A path such as '/FOO' will bypass the middleware. | tst.js:14:5:14:28 | new Reg ... (.*)?') | pattern | tst.js:60:1:61:2 | app.get ... ware\\n}) | here |
|
||||
| tst.js:41:9:41:25 | /\\/foo\\/([0-9]+)/ | This route uses a case-sensitive path $@, but is guarding a case-insensitive path $@. A path such as '/FOO/' will bypass the middleware. | tst.js:41:9:41:25 | /\\/foo\\/([0-9]+)/ | pattern | tst.js:60:1:61:2 | app.get ... ware\\n}) | here |
|
||||
@@ -0,0 +1 @@
|
||||
Security/CWE-178/CaseSensitiveMiddlewarePath.ql
|
||||
@@ -0,0 +1,9 @@
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
app.get(/\/[a-zA-Z]+/, (req, res, next) => { // OK - regexp term is case insensitive
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/foo', (req, res) => {
|
||||
});
|
||||
61
javascript/ql/test/query-tests/Security/CWE-178/tst.js
Normal file
61
javascript/ql/test/query-tests/Security/CWE-178/tst.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const unknown = require('~something/blah');
|
||||
|
||||
app.all(/\/.*/, unknown()); // OK - does not contain letters
|
||||
app.all(/\/.*/i, unknown()); // OK
|
||||
|
||||
app.all(/\/foo\/.*/, unknown()); // NOT OK
|
||||
app.all(/\/foo\/.*/i, unknown()); // OK - case insensitive
|
||||
|
||||
app.use(/\/x\/#\d{6}/, express.static('images/')); // OK - not a middleware
|
||||
|
||||
app.get(
|
||||
new RegExp('^/foo(.*)?'), // NOT OK - case sensitive
|
||||
unknown(),
|
||||
function(req, res, next) {
|
||||
if (req.params.blah) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
new RegExp('^/foo(.*)?', 'i'), // OK - case insensitive
|
||||
unknown(),
|
||||
function(req, res, next) {
|
||||
if (req.params.blah) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
new RegExp('^/foo(.*)?'), // OK - not a middleware
|
||||
unknown(),
|
||||
function(req,res) {
|
||||
res.send('Hello World!');
|
||||
}
|
||||
);
|
||||
|
||||
app.use(/\/foo\/([0-9]+)/, (req, res, next) => { // NOT OK - case sensitive
|
||||
unknown(req);
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(/\/foo\/([0-9]+)/i, (req, res, next) => { // OK - case insensitive
|
||||
unknown(req);
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
app.use(/\/foo\/([0-9]+)/, (req, res) => { // OK - not middleware
|
||||
unknown(req, res);
|
||||
});
|
||||
|
||||
app.use(/\/foo\/([0-9]+)/i, (req, res) => { // OK - not middleware (also case insensitive)
|
||||
unknown(req, res);
|
||||
});
|
||||
|
||||
app.get('/foo/:param', (req, res) => { // OK - not a middleware
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/python-all
|
||||
version: 0.5.1
|
||||
version: 0.5.2-dev
|
||||
groups: python
|
||||
dbscheme: semmlecode.python.dbscheme
|
||||
extractor: python
|
||||
|
||||
@@ -74,9 +74,7 @@ private module NotExposed {
|
||||
}
|
||||
|
||||
/** DEPRECATED: Alias for fullyQualifiedToApiGraphPath */
|
||||
deprecated string fullyQualifiedToAPIGraphPath(string fullyQaulified) {
|
||||
result = fullyQualifiedToApiGraphPath(fullyQaulified)
|
||||
}
|
||||
deprecated predicate fullyQualifiedToAPIGraphPath = fullyQualifiedToApiGraphPath/1;
|
||||
|
||||
bindingset[this]
|
||||
abstract class FindSubclassesSpec extends string {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_name)
|
||||
blob_client.require_encryption = True
|
||||
blob_client.key_encryption_key = kek
|
||||
# GOOD: Must use `encryption_version` set to `2.0`
|
||||
blob_client.encryption_version = '2.0' # Use Version 2.0!
|
||||
with open("decryptedcontentfile.txt", "rb") as stream:
|
||||
blob_client.upload_blob(stream, overwrite=OVERWRITE_EXISTING)
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
|
||||
<overview>
|
||||
<p>Azure Storage .NET, Java, and Python SDKs support encryption on the client with a customer-managed key that is maintained in Azure Key Vault or another key store.</p>
|
||||
<p>Current release versions of the Azure Storage SDKs use cipher block chaining (CBC mode) for client-side encryption (referred to as <code>v1</code>).</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>Consider switching to <code>v2</code> client-side encryption.</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<sample src="UnsafeUsageOfClientSideEncryptionVersion.py" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
<li>
|
||||
<a href="http://aka.ms/azstorageclientencryptionblog">Azure Storage Client Encryption Blog.</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-30187">CVE-2022-30187</a>
|
||||
</li>
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @name Unsafe usage of v1 version of Azure Storage client-side encryption.
|
||||
* @description Using version v1 of Azure Storage client-side encryption is insecure, and may enable an attacker to decrypt encrypted data
|
||||
* @kind problem
|
||||
* @tags security
|
||||
* cryptography
|
||||
* external/cwe/cwe-327
|
||||
* @id py/azure-storage/unsafe-client-side-encryption-in-use
|
||||
* @problem.severity error
|
||||
* @precision medium
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.ApiGraphs
|
||||
|
||||
predicate isUnsafeClientSideAzureStorageEncryptionViaAttributes(Call call, AttrNode node) {
|
||||
exists(
|
||||
API::Node n, API::Node n2, Attribute a, AssignStmt astmt, API::Node uploadBlob,
|
||||
ControlFlowNode ctrlFlowNode, string s
|
||||
|
|
||||
s in ["key_encryption_key", "key_resolver_function"] and
|
||||
n =
|
||||
API::moduleImport("azure")
|
||||
.getMember("storage")
|
||||
.getMember("blob")
|
||||
.getMember("BlobClient")
|
||||
.getReturn()
|
||||
.getMember(s) and
|
||||
n2 =
|
||||
API::moduleImport("azure")
|
||||
.getMember("storage")
|
||||
.getMember("blob")
|
||||
.getMember("BlobClient")
|
||||
.getReturn()
|
||||
.getMember("upload_blob") and
|
||||
n.getAValueReachableFromSource().asExpr() = a and
|
||||
astmt.getATarget() = a and
|
||||
a.getAFlowNode() = node and
|
||||
uploadBlob =
|
||||
API::moduleImport("azure")
|
||||
.getMember("storage")
|
||||
.getMember("blob")
|
||||
.getMember("BlobClient")
|
||||
.getReturn()
|
||||
.getMember("upload_blob") and
|
||||
uploadBlob.getACall().asExpr() = call and
|
||||
ctrlFlowNode = call.getAFlowNode() and
|
||||
node.strictlyReaches(ctrlFlowNode) and
|
||||
node != ctrlFlowNode and
|
||||
not exists(
|
||||
AssignStmt astmt2, Attribute a2, AttrNode encryptionVersionSet, StrConst uc,
|
||||
API::Node encryptionVersion
|
||||
|
|
||||
uc = astmt2.getValue() and
|
||||
uc.getText() in ["'2.0'", "2.0"] and
|
||||
encryptionVersion =
|
||||
API::moduleImport("azure")
|
||||
.getMember("storage")
|
||||
.getMember("blob")
|
||||
.getMember("BlobClient")
|
||||
.getReturn()
|
||||
.getMember("encryption_version") and
|
||||
encryptionVersion.getAValueReachableFromSource().asExpr() = a2 and
|
||||
astmt2.getATarget() = a2 and
|
||||
a2.getAFlowNode() = encryptionVersionSet and
|
||||
encryptionVersionSet.strictlyReaches(ctrlFlowNode)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
predicate isUnsafeClientSideAzureStorageEncryptionViaObjectCreation(Call call, ControlFlowNode node) {
|
||||
exists(API::Node c, string s, Keyword k | k.getAFlowNode() = node |
|
||||
c.getACall().asExpr() = call and
|
||||
c = API::moduleImport("azure").getMember("storage").getMember("blob").getMember(s) and
|
||||
s in ["ContainerClient", "BlobClient", "BlobServiceClient"] and
|
||||
k.getArg() = "key_encryption_key" and
|
||||
k = call.getANamedArg() and
|
||||
not k.getValue() instanceof None and
|
||||
not exists(Keyword k2 | k2 = call.getANamedArg() |
|
||||
k2.getArg() = "encryption_version" and
|
||||
k2.getValue().(StrConst).getText() in ["'2.0'", "2.0"]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
from Call call, ControlFlowNode node
|
||||
where
|
||||
isUnsafeClientSideAzureStorageEncryptionViaAttributes(call, node) or
|
||||
isUnsafeClientSideAzureStorageEncryptionViaObjectCreation(call, node)
|
||||
select node, "Unsafe usage of v1 version of Azure Storage client-side encryption."
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/python-queries
|
||||
version: 0.3.0
|
||||
version: 0.3.1-dev
|
||||
groups:
|
||||
- python
|
||||
- queries
|
||||
|
||||
@@ -1,161 +1,112 @@
|
||||
use crate::trap;
|
||||
use node_types::{EntryKind, Field, NodeTypeMap, Storage, TypeName};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap as Map;
|
||||
use std::collections::BTreeSet as Set;
|
||||
use std::fmt;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use tracing::{error, info, span, Level};
|
||||
use tree_sitter::{Language, Node, Parser, Range, Tree};
|
||||
|
||||
pub struct TrapWriter {
|
||||
/// The accumulated trap entries
|
||||
trap_output: Vec<TrapEntry>,
|
||||
/// A counter for generating fresh labels
|
||||
counter: u32,
|
||||
/// cache of global keys
|
||||
global_keys: std::collections::HashMap<String, Label>,
|
||||
pub fn populate_file(writer: &mut trap::Writer, absolute_path: &Path) -> trap::Label {
|
||||
let (file_label, fresh) =
|
||||
writer.global_id(&trap::full_id_for_file(&normalize_path(absolute_path)));
|
||||
if fresh {
|
||||
writer.add_tuple(
|
||||
"files",
|
||||
vec![
|
||||
trap::Arg::Label(file_label),
|
||||
trap::Arg::String(normalize_path(absolute_path)),
|
||||
],
|
||||
);
|
||||
populate_parent_folders(writer, file_label, absolute_path.parent());
|
||||
}
|
||||
file_label
|
||||
}
|
||||
|
||||
pub fn new_trap_writer() -> TrapWriter {
|
||||
TrapWriter {
|
||||
counter: 0,
|
||||
trap_output: Vec::new(),
|
||||
global_keys: std::collections::HashMap::new(),
|
||||
fn populate_empty_file(writer: &mut trap::Writer) -> trap::Label {
|
||||
let (file_label, fresh) = writer.global_id("empty;sourcefile");
|
||||
if fresh {
|
||||
writer.add_tuple(
|
||||
"files",
|
||||
vec![
|
||||
trap::Arg::Label(file_label),
|
||||
trap::Arg::String("".to_string()),
|
||||
],
|
||||
);
|
||||
}
|
||||
file_label
|
||||
}
|
||||
|
||||
impl TrapWriter {
|
||||
/// Gets a label that will hold the unique ID of the passed string at import time.
|
||||
/// This can be used for incrementally importable TRAP files -- use globally unique
|
||||
/// strings to compute a unique ID for table tuples.
|
||||
///
|
||||
/// Note: You probably want to make sure that the key strings that you use are disjoint
|
||||
/// for disjoint column types; the standard way of doing this is to prefix (or append)
|
||||
/// the column type name to the ID. Thus, you might identify methods in Java by the
|
||||
/// full ID "methods_com.method.package.DeclaringClass.method(argumentList)".
|
||||
pub fn populate_empty_location(writer: &mut trap::Writer) {
|
||||
let file_label = populate_empty_file(writer);
|
||||
location(writer, file_label, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
fn fresh_id(&mut self) -> Label {
|
||||
let label = Label(self.counter);
|
||||
self.counter += 1;
|
||||
self.trap_output.push(TrapEntry::FreshId(label));
|
||||
label
|
||||
}
|
||||
|
||||
fn global_id(&mut self, key: &str) -> (Label, bool) {
|
||||
if let Some(label) = self.global_keys.get(key) {
|
||||
return (*label, false);
|
||||
}
|
||||
let label = Label(self.counter);
|
||||
self.counter += 1;
|
||||
self.global_keys.insert(key.to_owned(), label);
|
||||
self.trap_output
|
||||
.push(TrapEntry::MapLabelToKey(label, key.to_owned()));
|
||||
(label, true)
|
||||
}
|
||||
|
||||
fn add_tuple(&mut self, table_name: &str, args: Vec<Arg>) {
|
||||
self.trap_output
|
||||
.push(TrapEntry::GenericTuple(table_name.to_owned(), args))
|
||||
}
|
||||
|
||||
fn populate_file(&mut self, absolute_path: &Path) -> Label {
|
||||
let (file_label, fresh) = self.global_id(&full_id_for_file(absolute_path));
|
||||
if fresh {
|
||||
self.add_tuple(
|
||||
"files",
|
||||
vec![
|
||||
Arg::Label(file_label),
|
||||
Arg::String(normalize_path(absolute_path)),
|
||||
],
|
||||
);
|
||||
self.populate_parent_folders(file_label, absolute_path.parent());
|
||||
}
|
||||
file_label
|
||||
}
|
||||
|
||||
fn populate_empty_file(&mut self) -> Label {
|
||||
let (file_label, fresh) = self.global_id("empty;sourcefile");
|
||||
if fresh {
|
||||
self.add_tuple(
|
||||
"files",
|
||||
vec![Arg::Label(file_label), Arg::String("".to_string())],
|
||||
);
|
||||
}
|
||||
file_label
|
||||
}
|
||||
|
||||
pub fn populate_empty_location(&mut self) {
|
||||
let file_label = self.populate_empty_file();
|
||||
self.location(file_label, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
fn populate_parent_folders(&mut self, child_label: Label, path: Option<&Path>) {
|
||||
let mut path = path;
|
||||
let mut child_label = child_label;
|
||||
loop {
|
||||
match path {
|
||||
None => break,
|
||||
Some(folder) => {
|
||||
let (folder_label, fresh) = self.global_id(&full_id_for_folder(folder));
|
||||
self.add_tuple(
|
||||
"containerparent",
|
||||
vec![Arg::Label(folder_label), Arg::Label(child_label)],
|
||||
pub fn populate_parent_folders(
|
||||
writer: &mut trap::Writer,
|
||||
child_label: trap::Label,
|
||||
path: Option<&Path>,
|
||||
) {
|
||||
let mut path = path;
|
||||
let mut child_label = child_label;
|
||||
loop {
|
||||
match path {
|
||||
None => break,
|
||||
Some(folder) => {
|
||||
let (folder_label, fresh) =
|
||||
writer.global_id(&trap::full_id_for_folder(&normalize_path(folder)));
|
||||
writer.add_tuple(
|
||||
"containerparent",
|
||||
vec![
|
||||
trap::Arg::Label(folder_label),
|
||||
trap::Arg::Label(child_label),
|
||||
],
|
||||
);
|
||||
if fresh {
|
||||
writer.add_tuple(
|
||||
"folders",
|
||||
vec![
|
||||
trap::Arg::Label(folder_label),
|
||||
trap::Arg::String(normalize_path(folder)),
|
||||
],
|
||||
);
|
||||
if fresh {
|
||||
self.add_tuple(
|
||||
"folders",
|
||||
vec![
|
||||
Arg::Label(folder_label),
|
||||
Arg::String(normalize_path(folder)),
|
||||
],
|
||||
);
|
||||
path = folder.parent();
|
||||
child_label = folder_label;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
path = folder.parent();
|
||||
child_label = folder_label;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn location(
|
||||
&mut self,
|
||||
file_label: Label,
|
||||
start_line: usize,
|
||||
start_column: usize,
|
||||
end_line: usize,
|
||||
end_column: usize,
|
||||
) -> Label {
|
||||
let (loc_label, fresh) = self.global_id(&format!(
|
||||
"loc,{{{}}},{},{},{},{}",
|
||||
file_label, start_line, start_column, end_line, end_column
|
||||
));
|
||||
if fresh {
|
||||
self.add_tuple(
|
||||
"locations_default",
|
||||
vec![
|
||||
Arg::Label(loc_label),
|
||||
Arg::Label(file_label),
|
||||
Arg::Int(start_line),
|
||||
Arg::Int(start_column),
|
||||
Arg::Int(end_line),
|
||||
Arg::Int(end_column),
|
||||
],
|
||||
);
|
||||
}
|
||||
loc_label
|
||||
}
|
||||
|
||||
fn comment(&mut self, text: String) {
|
||||
self.trap_output.push(TrapEntry::Comment(text));
|
||||
}
|
||||
|
||||
pub fn output(self, writer: &mut dyn Write) -> std::io::Result<()> {
|
||||
write!(writer, "{}", Program(self.trap_output))
|
||||
fn location(
|
||||
writer: &mut trap::Writer,
|
||||
file_label: trap::Label,
|
||||
start_line: usize,
|
||||
start_column: usize,
|
||||
end_line: usize,
|
||||
end_column: usize,
|
||||
) -> trap::Label {
|
||||
let (loc_label, fresh) = writer.global_id(&format!(
|
||||
"loc,{{{}}},{},{},{},{}",
|
||||
file_label, start_line, start_column, end_line, end_column
|
||||
));
|
||||
if fresh {
|
||||
writer.add_tuple(
|
||||
"locations_default",
|
||||
vec![
|
||||
trap::Arg::Label(loc_label),
|
||||
trap::Arg::Label(file_label),
|
||||
trap::Arg::Int(start_line),
|
||||
trap::Arg::Int(start_column),
|
||||
trap::Arg::Int(end_line),
|
||||
trap::Arg::Int(end_column),
|
||||
],
|
||||
);
|
||||
}
|
||||
loc_label
|
||||
}
|
||||
|
||||
/// Extracts the source file at `path`, which is assumed to be canonicalized.
|
||||
@@ -163,71 +114,43 @@ pub fn extract(
|
||||
language: Language,
|
||||
language_prefix: &str,
|
||||
schema: &NodeTypeMap,
|
||||
trap_writer: &mut TrapWriter,
|
||||
trap_writer: &mut trap::Writer,
|
||||
path: &Path,
|
||||
source: &[u8],
|
||||
ranges: &[Range],
|
||||
) -> std::io::Result<()> {
|
||||
let path_str = format!("{}", path.display());
|
||||
let span = span!(
|
||||
Level::TRACE,
|
||||
"extract",
|
||||
file = %path.display()
|
||||
file = %path_str
|
||||
);
|
||||
|
||||
let _enter = span.enter();
|
||||
|
||||
info!("extracting: {}", path.display());
|
||||
info!("extracting: {}", path_str);
|
||||
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(language).unwrap();
|
||||
parser.set_included_ranges(ranges).unwrap();
|
||||
let tree = parser.parse(&source, None).expect("Failed to parse file");
|
||||
trap_writer.comment(format!("Auto-generated TRAP file for {}", path.display()));
|
||||
let file_label = &trap_writer.populate_file(path);
|
||||
let mut visitor = Visitor {
|
||||
trap_writer.comment(format!("Auto-generated TRAP file for {}", path_str));
|
||||
let file_label = populate_file(trap_writer, path);
|
||||
let mut visitor = Visitor::new(
|
||||
source,
|
||||
trap_writer,
|
||||
// TODO: should we handle path strings that are not valid UTF8 better?
|
||||
path: format!("{}", path.display()),
|
||||
file_label: *file_label,
|
||||
toplevel_child_counter: 0,
|
||||
stack: Vec::new(),
|
||||
&path_str,
|
||||
file_label,
|
||||
language_prefix,
|
||||
schema,
|
||||
};
|
||||
);
|
||||
traverse(&tree, &mut visitor);
|
||||
|
||||
parser.reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Escapes a string for use in a TRAP key, by replacing special characters with
|
||||
/// HTML entities.
|
||||
fn escape_key<'a, S: Into<Cow<'a, str>>>(key: S) -> Cow<'a, str> {
|
||||
fn needs_escaping(c: char) -> bool {
|
||||
matches!(c, '&' | '{' | '}' | '"' | '@' | '#')
|
||||
}
|
||||
|
||||
let key = key.into();
|
||||
if key.contains(needs_escaping) {
|
||||
let mut escaped = String::with_capacity(2 * key.len());
|
||||
for c in key.chars() {
|
||||
match c {
|
||||
'&' => escaped.push_str("&"),
|
||||
'{' => escaped.push_str("{"),
|
||||
'}' => escaped.push_str("}"),
|
||||
'"' => escaped.push_str("""),
|
||||
'@' => escaped.push_str("@"),
|
||||
'#' => escaped.push_str("#"),
|
||||
_ => escaped.push(c),
|
||||
}
|
||||
}
|
||||
Cow::Owned(escaped)
|
||||
} else {
|
||||
key
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalizes the path according the common CodeQL specification. Assumes that
|
||||
/// `path` has already been canonicalized using `std::fs::canonicalize`.
|
||||
fn normalize_path(path: &Path) -> String {
|
||||
@@ -267,34 +190,28 @@ fn normalize_path(path: &Path) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn full_id_for_file(path: &Path) -> String {
|
||||
format!("{};sourcefile", escape_key(&normalize_path(path)))
|
||||
}
|
||||
|
||||
fn full_id_for_folder(path: &Path) -> String {
|
||||
format!("{};folder", escape_key(&normalize_path(path)))
|
||||
}
|
||||
|
||||
struct ChildNode {
|
||||
field_name: Option<&'static str>,
|
||||
label: Label,
|
||||
label: trap::Label,
|
||||
type_name: TypeName,
|
||||
}
|
||||
|
||||
struct Visitor<'a> {
|
||||
/// The file path of the source code (as string)
|
||||
path: String,
|
||||
path: &'a str,
|
||||
/// The label to use whenever we need to refer to the `@file` entity of this
|
||||
/// source file.
|
||||
file_label: Label,
|
||||
file_label: trap::Label,
|
||||
/// The source code as a UTF-8 byte array
|
||||
source: &'a [u8],
|
||||
/// A TrapWriter to accumulate trap entries
|
||||
trap_writer: &'a mut TrapWriter,
|
||||
/// A trap::Writer to accumulate trap entries
|
||||
trap_writer: &'a mut trap::Writer,
|
||||
/// A counter for top-level child nodes
|
||||
toplevel_child_counter: usize,
|
||||
/// Language prefix
|
||||
language_prefix: &'a str,
|
||||
/// Language-specific name of the AST info table
|
||||
ast_node_info_table_name: String,
|
||||
/// Language-specific name of the tokeninfo table
|
||||
tokeninfo_table_name: String,
|
||||
/// A lookup table from type name to node types
|
||||
schema: &'a NodeTypeMap,
|
||||
/// A stack for gathering information from child nodes. Whenever a node is
|
||||
@@ -303,27 +220,48 @@ struct Visitor<'a> {
|
||||
/// node the list containing the child data is popped from the stack and
|
||||
/// matched against the dbscheme for the node. If the expectations are met
|
||||
/// the corresponding row definitions are added to the trap_output.
|
||||
stack: Vec<(Label, usize, Vec<ChildNode>)>,
|
||||
stack: Vec<(trap::Label, usize, Vec<ChildNode>)>,
|
||||
}
|
||||
|
||||
impl Visitor<'_> {
|
||||
impl<'a> Visitor<'a> {
|
||||
fn new(
|
||||
source: &'a [u8],
|
||||
trap_writer: &'a mut trap::Writer,
|
||||
path: &'a str,
|
||||
file_label: trap::Label,
|
||||
language_prefix: &str,
|
||||
schema: &'a NodeTypeMap,
|
||||
) -> Visitor<'a> {
|
||||
Visitor {
|
||||
path,
|
||||
file_label,
|
||||
source,
|
||||
trap_writer,
|
||||
toplevel_child_counter: 0,
|
||||
ast_node_info_table_name: format!("{}_ast_node_info", language_prefix),
|
||||
tokeninfo_table_name: format!("{}_tokeninfo", language_prefix),
|
||||
schema,
|
||||
stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn record_parse_error(
|
||||
&mut self,
|
||||
error_message: String,
|
||||
full_error_message: String,
|
||||
loc: Label,
|
||||
loc: trap::Label,
|
||||
) {
|
||||
error!("{}", full_error_message);
|
||||
let id = self.trap_writer.fresh_id();
|
||||
self.trap_writer.add_tuple(
|
||||
"diagnostics",
|
||||
vec![
|
||||
Arg::Label(id),
|
||||
Arg::Int(40), // severity 40 = error
|
||||
Arg::String("parse_error".to_string()),
|
||||
Arg::String(error_message),
|
||||
Arg::String(full_error_message),
|
||||
Arg::Label(loc),
|
||||
trap::Arg::Label(id),
|
||||
trap::Arg::Int(40), // severity 40 = error
|
||||
trap::Arg::String("parse_error".to_string()),
|
||||
trap::Arg::String(error_message),
|
||||
trap::Arg::String(full_error_message),
|
||||
trap::Arg::Label(loc),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -335,7 +273,8 @@ impl Visitor<'_> {
|
||||
node: Node,
|
||||
) {
|
||||
let (start_line, start_column, end_line, end_column) = location_for(self.source, node);
|
||||
let loc = self.trap_writer.location(
|
||||
let loc = location(
|
||||
self.trap_writer,
|
||||
self.file_label,
|
||||
start_line,
|
||||
start_column,
|
||||
@@ -374,7 +313,8 @@ impl Visitor<'_> {
|
||||
}
|
||||
let (id, _, child_nodes) = self.stack.pop().expect("Vistor: empty stack");
|
||||
let (start_line, start_column, end_line, end_column) = location_for(self.source, node);
|
||||
let loc = self.trap_writer.location(
|
||||
let loc = location(
|
||||
self.trap_writer,
|
||||
self.file_label,
|
||||
start_line,
|
||||
start_column,
|
||||
@@ -402,19 +342,19 @@ impl Visitor<'_> {
|
||||
match &table.kind {
|
||||
EntryKind::Token { kind_id, .. } => {
|
||||
self.trap_writer.add_tuple(
|
||||
&format!("{}_ast_node_info", self.language_prefix),
|
||||
&self.ast_node_info_table_name,
|
||||
vec![
|
||||
Arg::Label(id),
|
||||
Arg::Label(parent_id),
|
||||
Arg::Int(parent_index),
|
||||
Arg::Label(loc),
|
||||
trap::Arg::Label(id),
|
||||
trap::Arg::Label(parent_id),
|
||||
trap::Arg::Int(parent_index),
|
||||
trap::Arg::Label(loc),
|
||||
],
|
||||
);
|
||||
self.trap_writer.add_tuple(
|
||||
&format!("{}_tokeninfo", self.language_prefix),
|
||||
&self.tokeninfo_table_name,
|
||||
vec![
|
||||
Arg::Label(id),
|
||||
Arg::Int(*kind_id),
|
||||
trap::Arg::Label(id),
|
||||
trap::Arg::Int(*kind_id),
|
||||
sliced_source_arg(self.source, node),
|
||||
],
|
||||
);
|
||||
@@ -425,15 +365,15 @@ impl Visitor<'_> {
|
||||
} => {
|
||||
if let Some(args) = self.complex_node(&node, fields, &child_nodes, id) {
|
||||
self.trap_writer.add_tuple(
|
||||
&format!("{}_ast_node_info", self.language_prefix),
|
||||
&self.ast_node_info_table_name,
|
||||
vec![
|
||||
Arg::Label(id),
|
||||
Arg::Label(parent_id),
|
||||
Arg::Int(parent_index),
|
||||
Arg::Label(loc),
|
||||
trap::Arg::Label(id),
|
||||
trap::Arg::Label(parent_id),
|
||||
trap::Arg::Int(parent_index),
|
||||
trap::Arg::Label(loc),
|
||||
],
|
||||
);
|
||||
let mut all_args = vec![Arg::Label(id)];
|
||||
let mut all_args = vec![trap::Arg::Label(id)];
|
||||
all_args.extend(args);
|
||||
self.trap_writer.add_tuple(table_name, all_args);
|
||||
}
|
||||
@@ -472,9 +412,9 @@ impl Visitor<'_> {
|
||||
node: &Node,
|
||||
fields: &[Field],
|
||||
child_nodes: &[ChildNode],
|
||||
parent_id: Label,
|
||||
) -> Option<Vec<Arg>> {
|
||||
let mut map: Map<&Option<String>, (&Field, Vec<Arg>)> = Map::new();
|
||||
parent_id: trap::Label,
|
||||
) -> Option<Vec<trap::Arg>> {
|
||||
let mut map: Map<&Option<String>, (&Field, Vec<trap::Arg>)> = Map::new();
|
||||
for field in fields {
|
||||
map.insert(&field.name, (field, Vec::new()));
|
||||
}
|
||||
@@ -488,9 +428,9 @@ impl Visitor<'_> {
|
||||
{
|
||||
// We can safely unwrap because type_matches checks the key is in the map.
|
||||
let (int_value, _) = int_mapping.get(&child_node.type_name.kind).unwrap();
|
||||
values.push(Arg::Int(*int_value));
|
||||
values.push(trap::Arg::Int(*int_value));
|
||||
} else {
|
||||
values.push(Arg::Label(child_node.label));
|
||||
values.push(trap::Arg::Label(child_node.label));
|
||||
}
|
||||
} else if field.name.is_some() {
|
||||
let error_message = format!(
|
||||
@@ -569,9 +509,9 @@ impl Visitor<'_> {
|
||||
);
|
||||
break;
|
||||
}
|
||||
let mut args = vec![Arg::Label(parent_id)];
|
||||
let mut args = vec![trap::Arg::Label(parent_id)];
|
||||
if *has_index {
|
||||
args.push(Arg::Int(index))
|
||||
args.push(trap::Arg::Int(index))
|
||||
}
|
||||
args.push(child_value.clone());
|
||||
self.trap_writer.add_tuple(table_name, args);
|
||||
@@ -625,9 +565,9 @@ impl Visitor<'_> {
|
||||
}
|
||||
|
||||
// Emit a slice of a source file as an Arg.
|
||||
fn sliced_source_arg(source: &[u8], n: Node) -> Arg {
|
||||
fn sliced_source_arg(source: &[u8], n: Node) -> trap::Arg {
|
||||
let range = n.byte_range();
|
||||
Arg::String(String::from_utf8_lossy(&source[range.start..range.end]).into_owned())
|
||||
trap::Arg::String(String::from_utf8_lossy(&source[range.start..range.end]).into_owned())
|
||||
}
|
||||
|
||||
// Emit a pair of `TrapEntry`s for the provided node, appropriately calibrated.
|
||||
@@ -699,59 +639,6 @@ fn traverse(tree: &Tree, visitor: &mut Visitor) {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Program(Vec<TrapEntry>);
|
||||
|
||||
impl fmt::Display for Program {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut text = String::new();
|
||||
for trap_entry in &self.0 {
|
||||
text.push_str(&format!("{}\n", trap_entry));
|
||||
}
|
||||
write!(f, "{}", text)
|
||||
}
|
||||
}
|
||||
|
||||
enum TrapEntry {
|
||||
/// Maps the label to a fresh id, e.g. `#123=*`.
|
||||
FreshId(Label),
|
||||
/// Maps the label to a key, e.g. `#7=@"foo"`.
|
||||
MapLabelToKey(Label, String),
|
||||
/// foo_bar(arg*)
|
||||
GenericTuple(String, Vec<Arg>),
|
||||
Comment(String),
|
||||
}
|
||||
impl fmt::Display for TrapEntry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
TrapEntry::FreshId(label) => write!(f, "{}=*", label),
|
||||
TrapEntry::MapLabelToKey(label, key) => {
|
||||
write!(f, "{}=@\"{}\"", label, key.replace("\"", "\"\""))
|
||||
}
|
||||
TrapEntry::GenericTuple(name, args) => {
|
||||
write!(f, "{}(", name)?;
|
||||
for (index, arg) in args.iter().enumerate() {
|
||||
if index > 0 {
|
||||
write!(f, ",")?;
|
||||
}
|
||||
write!(f, "{}", arg)?;
|
||||
}
|
||||
write!(f, ")")
|
||||
}
|
||||
TrapEntry::Comment(line) => write!(f, "// {}", line),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
// Identifiers of the form #0, #1...
|
||||
struct Label(u32);
|
||||
|
||||
impl fmt::Display for Label {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "#{:x}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
// Numeric indices.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct Index(usize);
|
||||
@@ -761,69 +648,3 @@ impl fmt::Display for Index {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
// Some untyped argument to a TrapEntry.
|
||||
#[derive(Debug, Clone)]
|
||||
enum Arg {
|
||||
Label(Label),
|
||||
Int(usize),
|
||||
String(String),
|
||||
}
|
||||
|
||||
const MAX_STRLEN: usize = 1048576;
|
||||
|
||||
impl fmt::Display for Arg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Arg::Label(x) => write!(f, "{}", x),
|
||||
Arg::Int(x) => write!(f, "{}", x),
|
||||
Arg::String(x) => write!(
|
||||
f,
|
||||
"\"{}\"",
|
||||
limit_string(x, MAX_STRLEN).replace("\"", "\"\"")
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Limit the length (in bytes) of a string. If the string's length in bytes is
|
||||
/// less than or equal to the limit then the entire string is returned. Otherwise
|
||||
/// the string is sliced at the provided limit. If there is a multi-byte character
|
||||
/// at the limit then the returned slice will be slightly shorter than the limit to
|
||||
/// avoid splitting that multi-byte character.
|
||||
fn limit_string(string: &str, max_size: usize) -> &str {
|
||||
if string.len() <= max_size {
|
||||
return string;
|
||||
}
|
||||
let p = string.as_bytes();
|
||||
let mut index = max_size;
|
||||
// We want to clip the string at [max_size]; however, the character at that position
|
||||
// may span several bytes. We need to find the first byte of the character. In UTF-8
|
||||
// encoded data any byte that matches the bit pattern 10XXXXXX is not a start byte.
|
||||
// Therefore we decrement the index as long as there are bytes matching this pattern.
|
||||
// This ensures we cut the string at the border between one character and another.
|
||||
while index > 0 && (p[index] & 0b11000000) == 0b10000000 {
|
||||
index -= 1;
|
||||
}
|
||||
&string[0..index]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limit_string_test() {
|
||||
assert_eq!("hello", limit_string(&"hello world".to_owned(), 5));
|
||||
assert_eq!("hi ☹", limit_string(&"hi ☹☹".to_owned(), 6));
|
||||
assert_eq!("hi ", limit_string(&"hi ☹☹".to_owned(), 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_key_test() {
|
||||
assert_eq!("foo!", escape_key("foo!"));
|
||||
assert_eq!("foo{}", escape_key("foo{}"));
|
||||
assert_eq!("{}", escape_key("{}"));
|
||||
assert_eq!("", escape_key(""));
|
||||
assert_eq!("/path/to/foo.rb", escape_key("/path/to/foo.rb"));
|
||||
assert_eq!(
|
||||
"/path/to/foo&{}"@#.rb",
|
||||
escape_key("/path/to/foo&{}\"@#.rb")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,49 +1,13 @@
|
||||
mod extractor;
|
||||
mod trap;
|
||||
|
||||
extern crate num_cpus;
|
||||
|
||||
use flate2::write::GzEncoder;
|
||||
use rayon::prelude::*;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufWriter};
|
||||
use std::io::BufRead;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
enum TrapCompression {
|
||||
None,
|
||||
Gzip,
|
||||
}
|
||||
|
||||
impl TrapCompression {
|
||||
fn from_env() -> TrapCompression {
|
||||
match std::env::var("CODEQL_QL_TRAP_COMPRESSION") {
|
||||
Ok(method) => match TrapCompression::from_string(&method) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
tracing::error!("Unknown compression method '{}'; using gzip.", &method);
|
||||
TrapCompression::Gzip
|
||||
}
|
||||
},
|
||||
// Default compression method if the env var isn't set:
|
||||
Err(_) => TrapCompression::Gzip,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_string(s: &str) -> Option<TrapCompression> {
|
||||
match s.to_lowercase().as_ref() {
|
||||
"none" => Some(TrapCompression::None),
|
||||
"gzip" => Some(TrapCompression::Gzip),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extension(&self) -> &str {
|
||||
match self {
|
||||
TrapCompression::None => "trap",
|
||||
TrapCompression::Gzip => "trap.gz",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of threads the extractor should use, by reading the
|
||||
* CODEQL_THREADS environment variable and using it as described in the
|
||||
@@ -115,7 +79,7 @@ fn main() -> std::io::Result<()> {
|
||||
.value_of("output-dir")
|
||||
.expect("missing --output-dir");
|
||||
let trap_dir = PathBuf::from(trap_dir);
|
||||
let trap_compression = TrapCompression::from_env();
|
||||
let trap_compression = trap::Compression::from_env("CODEQL_QL_TRAP_COMPRESSION");
|
||||
|
||||
let file_list = matches.value_of("file-list").expect("missing --file-list");
|
||||
let file_list = fs::File::open(file_list)?;
|
||||
@@ -140,7 +104,7 @@ fn main() -> std::io::Result<()> {
|
||||
let src_archive_file = path_for(&src_archive_dir, &path, "");
|
||||
let source = std::fs::read(&path)?;
|
||||
let code_ranges = vec![];
|
||||
let mut trap_writer = extractor::new_trap_writer();
|
||||
let mut trap_writer = trap::Writer::new();
|
||||
extractor::extract(
|
||||
language,
|
||||
"ql",
|
||||
@@ -152,33 +116,25 @@ fn main() -> std::io::Result<()> {
|
||||
)?;
|
||||
std::fs::create_dir_all(&src_archive_file.parent().unwrap())?;
|
||||
std::fs::copy(&path, &src_archive_file)?;
|
||||
write_trap(&trap_dir, path, trap_writer, &trap_compression)
|
||||
write_trap(&trap_dir, path, &trap_writer, trap_compression)
|
||||
})
|
||||
.expect("failed to extract files");
|
||||
|
||||
let path = PathBuf::from("extras");
|
||||
let mut trap_writer = extractor::new_trap_writer();
|
||||
trap_writer.populate_empty_location();
|
||||
write_trap(&trap_dir, path, trap_writer, &trap_compression)
|
||||
let mut trap_writer = trap::Writer::new();
|
||||
extractor::populate_empty_location(&mut trap_writer);
|
||||
write_trap(&trap_dir, path, &trap_writer, trap_compression)
|
||||
}
|
||||
|
||||
fn write_trap(
|
||||
trap_dir: &Path,
|
||||
path: PathBuf,
|
||||
trap_writer: extractor::TrapWriter,
|
||||
trap_compression: &TrapCompression,
|
||||
trap_writer: &trap::Writer,
|
||||
trap_compression: trap::Compression,
|
||||
) -> std::io::Result<()> {
|
||||
let trap_file = path_for(trap_dir, &path, trap_compression.extension());
|
||||
std::fs::create_dir_all(&trap_file.parent().unwrap())?;
|
||||
let trap_file = std::fs::File::create(&trap_file)?;
|
||||
let mut trap_file = BufWriter::new(trap_file);
|
||||
match trap_compression {
|
||||
TrapCompression::None => trap_writer.output(&mut trap_file),
|
||||
TrapCompression::Gzip => {
|
||||
let mut compressed_writer = GzEncoder::new(trap_file, flate2::Compression::fast());
|
||||
trap_writer.output(&mut compressed_writer)
|
||||
}
|
||||
}
|
||||
trap_writer.write_to_file(&trap_file, trap_compression)
|
||||
}
|
||||
|
||||
fn path_for(dir: &Path, path: &Path, ext: &str) -> PathBuf {
|
||||
|
||||
272
ql/extractor/src/trap.rs
Normal file
272
ql/extractor/src/trap.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use flate2::write::GzEncoder;
|
||||
|
||||
pub struct Writer {
|
||||
/// The accumulated trap entries
|
||||
trap_output: Vec<Entry>,
|
||||
/// A counter for generating fresh labels
|
||||
counter: u32,
|
||||
/// cache of global keys
|
||||
global_keys: std::collections::HashMap<String, Label>,
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
pub fn new() -> Writer {
|
||||
Writer {
|
||||
counter: 0,
|
||||
trap_output: Vec::new(),
|
||||
global_keys: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fresh_id(&mut self) -> Label {
|
||||
let label = Label(self.counter);
|
||||
self.counter += 1;
|
||||
self.trap_output.push(Entry::FreshId(label));
|
||||
label
|
||||
}
|
||||
|
||||
/// Gets a label that will hold the unique ID of the passed string at import time.
|
||||
/// This can be used for incrementally importable TRAP files -- use globally unique
|
||||
/// strings to compute a unique ID for table tuples.
|
||||
///
|
||||
/// Note: You probably want to make sure that the key strings that you use are disjoint
|
||||
/// for disjoint column types; the standard way of doing this is to prefix (or append)
|
||||
/// the column type name to the ID. Thus, you might identify methods in Java by the
|
||||
/// full ID "methods_com.method.package.DeclaringClass.method(argumentList)".
|
||||
pub fn global_id(&mut self, key: &str) -> (Label, bool) {
|
||||
if let Some(label) = self.global_keys.get(key) {
|
||||
return (*label, false);
|
||||
}
|
||||
let label = Label(self.counter);
|
||||
self.counter += 1;
|
||||
self.global_keys.insert(key.to_owned(), label);
|
||||
self.trap_output
|
||||
.push(Entry::MapLabelToKey(label, key.to_owned()));
|
||||
(label, true)
|
||||
}
|
||||
|
||||
pub fn add_tuple(&mut self, table_name: &str, args: Vec<Arg>) {
|
||||
self.trap_output
|
||||
.push(Entry::GenericTuple(table_name.to_owned(), args))
|
||||
}
|
||||
|
||||
pub fn comment(&mut self, text: String) {
|
||||
self.trap_output.push(Entry::Comment(text));
|
||||
}
|
||||
|
||||
pub fn write_to_file(&self, path: &Path, compression: Compression) -> std::io::Result<()> {
|
||||
let trap_file = std::fs::File::create(path)?;
|
||||
let mut trap_file = BufWriter::new(trap_file);
|
||||
match compression {
|
||||
Compression::None => self.write_trap_entries(&mut trap_file),
|
||||
Compression::Gzip => {
|
||||
let mut compressed_writer = GzEncoder::new(trap_file, flate2::Compression::fast());
|
||||
self.write_trap_entries(&mut compressed_writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_trap_entries<W: Write>(&self, file: &mut W) -> std::io::Result<()> {
|
||||
for trap_entry in &self.trap_output {
|
||||
writeln!(file, "{}", trap_entry)?;
|
||||
}
|
||||
std::io::Result::Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Entry {
|
||||
/// Maps the label to a fresh id, e.g. `#123=*`.
|
||||
FreshId(Label),
|
||||
/// Maps the label to a key, e.g. `#7=@"foo"`.
|
||||
MapLabelToKey(Label, String),
|
||||
/// foo_bar(arg*)
|
||||
GenericTuple(String, Vec<Arg>),
|
||||
Comment(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Entry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Entry::FreshId(label) => write!(f, "{}=*", label),
|
||||
Entry::MapLabelToKey(label, key) => {
|
||||
write!(f, "{}=@\"{}\"", label, key.replace("\"", "\"\""))
|
||||
}
|
||||
Entry::GenericTuple(name, args) => {
|
||||
write!(f, "{}(", name)?;
|
||||
for (index, arg) in args.iter().enumerate() {
|
||||
if index > 0 {
|
||||
write!(f, ",")?;
|
||||
}
|
||||
write!(f, "{}", arg)?;
|
||||
}
|
||||
write!(f, ")")
|
||||
}
|
||||
Entry::Comment(line) => write!(f, "// {}", line),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
// Identifiers of the form #0, #1...
|
||||
pub struct Label(u32);
|
||||
|
||||
impl fmt::Display for Label {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "#{:x}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
// Some untyped argument to a TrapEntry.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Arg {
|
||||
Label(Label),
|
||||
Int(usize),
|
||||
String(String),
|
||||
}
|
||||
|
||||
const MAX_STRLEN: usize = 1048576;
|
||||
|
||||
impl fmt::Display for Arg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Arg::Label(x) => write!(f, "{}", x),
|
||||
Arg::Int(x) => write!(f, "{}", x),
|
||||
Arg::String(x) => write!(
|
||||
f,
|
||||
"\"{}\"",
|
||||
limit_string(x, MAX_STRLEN).replace("\"", "\"\"")
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Program(Vec<Entry>);
|
||||
|
||||
impl fmt::Display for Program {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut text = String::new();
|
||||
for trap_entry in &self.0 {
|
||||
text.push_str(&format!("{}\n", trap_entry));
|
||||
}
|
||||
write!(f, "{}", text)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn full_id_for_file(normalized_path: &str) -> String {
|
||||
format!("{};sourcefile", escape_key(normalized_path))
|
||||
}
|
||||
|
||||
pub fn full_id_for_folder(normalized_path: &str) -> String {
|
||||
format!("{};folder", escape_key(normalized_path))
|
||||
}
|
||||
|
||||
/// Escapes a string for use in a TRAP key, by replacing special characters with
|
||||
/// HTML entities.
|
||||
fn escape_key<'a, S: Into<Cow<'a, str>>>(key: S) -> Cow<'a, str> {
|
||||
fn needs_escaping(c: char) -> bool {
|
||||
matches!(c, '&' | '{' | '}' | '"' | '@' | '#')
|
||||
}
|
||||
|
||||
let key = key.into();
|
||||
if key.contains(needs_escaping) {
|
||||
let mut escaped = String::with_capacity(2 * key.len());
|
||||
for c in key.chars() {
|
||||
match c {
|
||||
'&' => escaped.push_str("&"),
|
||||
'{' => escaped.push_str("{"),
|
||||
'}' => escaped.push_str("}"),
|
||||
'"' => escaped.push_str("""),
|
||||
'@' => escaped.push_str("@"),
|
||||
'#' => escaped.push_str("#"),
|
||||
_ => escaped.push(c),
|
||||
}
|
||||
}
|
||||
Cow::Owned(escaped)
|
||||
} else {
|
||||
key
|
||||
}
|
||||
}
|
||||
|
||||
/// Limit the length (in bytes) of a string. If the string's length in bytes is
|
||||
/// less than or equal to the limit then the entire string is returned. Otherwise
|
||||
/// the string is sliced at the provided limit. If there is a multi-byte character
|
||||
/// at the limit then the returned slice will be slightly shorter than the limit to
|
||||
/// avoid splitting that multi-byte character.
|
||||
fn limit_string(string: &str, max_size: usize) -> &str {
|
||||
if string.len() <= max_size {
|
||||
return string;
|
||||
}
|
||||
let p = string.as_bytes();
|
||||
let mut index = max_size;
|
||||
// We want to clip the string at [max_size]; however, the character at that position
|
||||
// may span several bytes. We need to find the first byte of the character. In UTF-8
|
||||
// encoded data any byte that matches the bit pattern 10XXXXXX is not a start byte.
|
||||
// Therefore we decrement the index as long as there are bytes matching this pattern.
|
||||
// This ensures we cut the string at the border between one character and another.
|
||||
while index > 0 && (p[index] & 0b11000000) == 0b10000000 {
|
||||
index -= 1;
|
||||
}
|
||||
&string[0..index]
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Compression {
|
||||
None,
|
||||
Gzip,
|
||||
}
|
||||
|
||||
impl Compression {
|
||||
pub fn from_env(var_name: &str) -> Compression {
|
||||
match std::env::var(var_name) {
|
||||
Ok(method) => match Compression::from_string(&method) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
tracing::error!("Unknown compression method '{}'; using gzip.", &method);
|
||||
Compression::Gzip
|
||||
}
|
||||
},
|
||||
// Default compression method if the env var isn't set:
|
||||
Err(_) => Compression::Gzip,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(s: &str) -> Option<Compression> {
|
||||
match s.to_lowercase().as_ref() {
|
||||
"none" => Some(Compression::None),
|
||||
"gzip" => Some(Compression::Gzip),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extension(&self) -> &str {
|
||||
match self {
|
||||
Compression::None => "trap",
|
||||
Compression::Gzip => "trap.gz",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limit_string_test() {
|
||||
assert_eq!("hello", limit_string(&"hello world".to_owned(), 5));
|
||||
assert_eq!("hi ☹", limit_string(&"hi ☹☹".to_owned(), 6));
|
||||
assert_eq!("hi ", limit_string(&"hi ☹☹".to_owned(), 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_key_test() {
|
||||
assert_eq!("foo!", escape_key("foo!"));
|
||||
assert_eq!("foo{}", escape_key("foo{}"));
|
||||
assert_eq!("{}", escape_key("{}"));
|
||||
assert_eq!("", escape_key(""));
|
||||
assert_eq!("/path/to/foo.rb", escape_key("/path/to/foo.rb"));
|
||||
assert_eq!(
|
||||
"/path/to/foo&{}"@#.rb",
|
||||
escape_key("/path/to/foo&{}\"@#.rb")
|
||||
);
|
||||
}
|
||||
30
ql/scripts/split-sarif.js
Normal file
30
ql/scripts/split-sarif.js
Normal file
@@ -0,0 +1,30 @@
|
||||
var fs = require("fs");
|
||||
|
||||
// the .sarif file to split, and then the directory to put the split files in.
|
||||
async function main(inputs) {
|
||||
const sarifFile = JSON.parse(fs.readFileSync(inputs[0]));
|
||||
const outFolder = inputs[1];
|
||||
|
||||
const out = {};
|
||||
|
||||
for (const result of sarifFile.runs[0].results) {
|
||||
const lang = getLanguage(result);
|
||||
if (!out[lang]) {
|
||||
out[lang] = [];
|
||||
}
|
||||
out[lang].push(result);
|
||||
}
|
||||
|
||||
for (const lang in out) {
|
||||
const outSarif = JSON.parse(JSON.stringify(sarifFile));
|
||||
outSarif.runs[0].results = out[lang];
|
||||
fs.writeFileSync(`${outFolder}/${lang}.sarif`, JSON.stringify(outSarif, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function getLanguage(result) {
|
||||
return result.locations[0].physicalLocation.artifactLocation.uri.split(
|
||||
"/"
|
||||
)[0];
|
||||
}
|
||||
main(process.argv.splice(2));
|
||||
BIN
ruby/Cargo.lock
generated
BIN
ruby/Cargo.lock
generated
Binary file not shown.
@@ -19,6 +19,7 @@ fn main() -> std::io::Result<()> {
|
||||
.arg("--include-extension=.erb")
|
||||
.arg("--include-extension=.gemspec")
|
||||
.arg("--include=**/Gemfile")
|
||||
.arg("--exclude=**/.git")
|
||||
.arg("--size-limit=5m")
|
||||
.arg("--language=ruby")
|
||||
.arg("--working-dir=.")
|
||||
|
||||
@@ -11,10 +11,12 @@ flate2 = "1.0"
|
||||
node-types = { path = "../node-types" }
|
||||
tree-sitter = "0.19"
|
||||
tree-sitter-embedded-template = { git = "https://github.com/tree-sitter/tree-sitter-embedded-template.git", rev = "1a538da253d73f896b9f6c0c7d79cda58791ac5c" }
|
||||
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "5b305c3cd32db10494cedd2743de6bbe32f1a573" }
|
||||
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "e75d04404c9dd71ad68850d5c672b226d5e694f3" }
|
||||
clap = "3.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3.3", features = ["env-filter"] }
|
||||
rayon = "1.5.0"
|
||||
num_cpus = "1.13.0"
|
||||
regex = "1.5.5"
|
||||
encoding = "0.2"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
mod extractor;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
extern crate num_cpus;
|
||||
|
||||
use clap::arg;
|
||||
use flate2::write::GzEncoder;
|
||||
use rayon::prelude::*;
|
||||
use std::borrow::Cow;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufWriter};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -75,6 +78,21 @@ fn num_codeql_threads() -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CP_NUMBER: regex::Regex = regex::Regex::new("cp([0-9]+)").unwrap();
|
||||
}
|
||||
|
||||
fn encoding_from_name(encoding_name: &str) -> Option<&(dyn encoding::Encoding + Send + Sync)> {
|
||||
match encoding::label::encoding_from_whatwg_label(encoding_name) {
|
||||
s @ Some(_) => s,
|
||||
None => CP_NUMBER.captures(encoding_name).and_then(|cap| {
|
||||
encoding::label::encoding_from_windows_code_page(
|
||||
str::parse(cap.get(1).unwrap().as_str()).unwrap(),
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(false)
|
||||
@@ -140,6 +158,7 @@ fn main() -> std::io::Result<()> {
|
||||
let path = PathBuf::from(line).canonicalize()?;
|
||||
let src_archive_file = path_for(&src_archive_dir, &path, "");
|
||||
let mut source = std::fs::read(&path)?;
|
||||
let mut needs_conversion = false;
|
||||
let code_ranges;
|
||||
let mut trap_writer = extractor::new_trap_writer();
|
||||
if path.extension().map_or(false, |x| x == "erb") {
|
||||
@@ -168,6 +187,43 @@ fn main() -> std::io::Result<()> {
|
||||
}
|
||||
code_ranges = ranges;
|
||||
} else {
|
||||
if let Some(encoding_name) = scan_coding_comment(&source) {
|
||||
// If the input is already UTF-8 then there is no need to recode the source
|
||||
// If the declared encoding is 'binary' or 'ascii-8bit' then it is not clear how
|
||||
// to interpret characters. In this case it is probably best to leave the input
|
||||
// unchanged.
|
||||
if !encoding_name.eq_ignore_ascii_case("utf-8")
|
||||
&& !encoding_name.eq_ignore_ascii_case("ascii-8bit")
|
||||
&& !encoding_name.eq_ignore_ascii_case("binary")
|
||||
{
|
||||
if let Some(encoding) = encoding_from_name(&encoding_name) {
|
||||
needs_conversion =
|
||||
encoding.whatwg_name().unwrap_or_default() != "utf-8";
|
||||
if needs_conversion {
|
||||
match encoding
|
||||
.decode(&source, encoding::types::DecoderTrap::Replace)
|
||||
{
|
||||
Ok(str) => source = str.as_bytes().to_owned(),
|
||||
Err(msg) => {
|
||||
needs_conversion = false;
|
||||
tracing::warn!(
|
||||
"{}: character decoding failure: {} ({})",
|
||||
&path.to_string_lossy(),
|
||||
msg,
|
||||
&encoding_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"{}: unknown character encoding: '{}'",
|
||||
&path.to_string_lossy(),
|
||||
&encoding_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
code_ranges = vec![];
|
||||
}
|
||||
extractor::extract(
|
||||
@@ -180,7 +236,11 @@ fn main() -> std::io::Result<()> {
|
||||
&code_ranges,
|
||||
)?;
|
||||
std::fs::create_dir_all(&src_archive_file.parent().unwrap())?;
|
||||
std::fs::copy(&path, &src_archive_file)?;
|
||||
if needs_conversion {
|
||||
std::fs::write(&src_archive_file, &source)?;
|
||||
} else {
|
||||
std::fs::copy(&path, &src_archive_file)?;
|
||||
}
|
||||
write_trap(&trap_dir, path, trap_writer, &trap_compression)
|
||||
})
|
||||
.expect("failed to extract files");
|
||||
@@ -299,3 +359,143 @@ fn path_for(dir: &Path, path: &Path, ext: &str) -> PathBuf {
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn skip_space(content: &[u8], index: usize) -> usize {
|
||||
let mut index = index;
|
||||
while index < content.len() {
|
||||
let c = content[index] as char;
|
||||
// white space except \n
|
||||
let is_space = c == ' ' || ('\t'..='\r').contains(&c) && c != '\n';
|
||||
if !is_space {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
fn scan_coding_comment(content: &[u8]) -> std::option::Option<Cow<str>> {
|
||||
let mut index = 0;
|
||||
// skip UTF-8 BOM marker if there is one
|
||||
if content.len() >= 3 && content[0] == 0xef && content[1] == 0xbb && content[2] == 0xbf {
|
||||
index += 3;
|
||||
}
|
||||
// skip #! line if there is one
|
||||
if index + 1 < content.len()
|
||||
&& content[index] as char == '#'
|
||||
&& content[index + 1] as char == '!'
|
||||
{
|
||||
index += 2;
|
||||
while index < content.len() && content[index] as char != '\n' {
|
||||
index += 1
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
index = skip_space(content, index);
|
||||
|
||||
if index >= content.len() || content[index] as char != '#' {
|
||||
return None;
|
||||
}
|
||||
index += 1;
|
||||
|
||||
const CODING: [char; 12] = ['C', 'c', 'O', 'o', 'D', 'd', 'I', 'i', 'N', 'n', 'G', 'g'];
|
||||
let mut word_index = 0;
|
||||
while index < content.len() && word_index < CODING.len() && content[index] as char != '\n' {
|
||||
if content[index] as char == CODING[word_index]
|
||||
|| content[index] as char == CODING[word_index + 1]
|
||||
{
|
||||
word_index += 2
|
||||
} else {
|
||||
word_index = 0;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
if word_index < CODING.len() {
|
||||
return None;
|
||||
}
|
||||
index = skip_space(content, index);
|
||||
|
||||
if index < content.len() && content[index] as char != ':' && content[index] as char != '=' {
|
||||
return None;
|
||||
}
|
||||
index += 1;
|
||||
index = skip_space(content, index);
|
||||
|
||||
let start = index;
|
||||
while index < content.len() {
|
||||
let c = content[index] as char;
|
||||
if c == '-' || c == '_' || c.is_ascii_alphanumeric() {
|
||||
index += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if index > start {
|
||||
return Some(String::from_utf8_lossy(&content[start..index]));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scan_coding_comment() {
|
||||
let text = "# encoding: utf-8";
|
||||
let result = scan_coding_comment(text.as_bytes());
|
||||
assert_eq!(result, Some("utf-8".into()));
|
||||
|
||||
let text = "#coding:utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("utf-8".into()));
|
||||
|
||||
let text = "# foo\n# encoding: utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, None);
|
||||
|
||||
let text = "# encoding: latin1 encoding: utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("latin1".into()));
|
||||
|
||||
let text = "# encoding: nonsense";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("nonsense".into()));
|
||||
|
||||
let text = "# coding = utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("utf-8".into()));
|
||||
|
||||
let text = "# CODING = utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("utf-8".into()));
|
||||
|
||||
let text = "# CoDiNg = utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("utf-8".into()));
|
||||
|
||||
let text = "# blah blahblahcoding = utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("utf-8".into()));
|
||||
|
||||
// unicode BOM is ignored
|
||||
let text = "\u{FEFF}# encoding: utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("utf-8".into()));
|
||||
|
||||
let text = "\u{FEFF} # encoding: utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("utf-8".into()));
|
||||
|
||||
let text = "#! /usr/bin/env ruby\n # encoding: utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("utf-8".into()));
|
||||
|
||||
let text = "\u{FEFF}#! /usr/bin/env ruby\n # encoding: utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("utf-8".into()));
|
||||
|
||||
// A #! must be the first thing on a line, otherwise it's a normal comment
|
||||
let text = " #! /usr/bin/env ruby encoding = utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, Some("utf-8".into()));
|
||||
let text = " #! /usr/bin/env ruby \n # encoding = utf-8";
|
||||
let result = scan_coding_comment(&text.as_bytes());
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ node-types = { path = "../node-types" }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3.3", features = ["env-filter"] }
|
||||
tree-sitter-embedded-template = { git = "https://github.com/tree-sitter/tree-sitter-embedded-template.git", rev = "1a538da253d73f896b9f6c0c7d79cda58791ac5c" }
|
||||
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "5b305c3cd32db10494cedd2743de6bbe32f1a573" }
|
||||
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "e75d04404c9dd71ad68850d5c672b226d5e694f3" }
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
|
||||
- Calls to `ActiveRecord::Relation#annotate` are now recognized as`SqlExecution`s so that it will be considered as a sink for queries like rb/sql-injection.
|
||||
4
ruby/ql/lib/change-notes/2022-07-19-arel.md
Normal file
4
ruby/ql/lib/change-notes/2022-07-19-arel.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Calls to `Arel.sql` are now recognised as propagating taint from their argument.
|
||||
@@ -10,6 +10,7 @@ private import codeql.ruby.frameworks.ActiveStorage
|
||||
private import codeql.ruby.frameworks.ActionView
|
||||
private import codeql.ruby.frameworks.ActiveSupport
|
||||
private import codeql.ruby.frameworks.Archive
|
||||
private import codeql.ruby.frameworks.Arel
|
||||
private import codeql.ruby.frameworks.GraphQL
|
||||
private import codeql.ruby.frameworks.Rails
|
||||
private import codeql.ruby.frameworks.Railties
|
||||
|
||||
@@ -36,7 +36,7 @@ private import ExprNodes
|
||||
* constant value in some cases.
|
||||
*/
|
||||
private module Propagation {
|
||||
private ExprCfgNode getSource(VariableReadAccessCfgNode read) {
|
||||
ExprCfgNode getSource(VariableReadAccessCfgNode read) {
|
||||
exists(Ssa::WriteDefinition def |
|
||||
def.assigns(result) and
|
||||
read = def.getARead()
|
||||
@@ -509,3 +509,53 @@ private module Cached {
|
||||
}
|
||||
|
||||
import Cached
|
||||
|
||||
/**
|
||||
* Holds if the control flow node `e` refers to an array constructed from the
|
||||
* array literal `arr`.
|
||||
* Example:
|
||||
* ```rb
|
||||
* [1, 2, 3]
|
||||
* C = [1, 2, 3]; C
|
||||
* x = [1, 2, 3]; x
|
||||
* ```
|
||||
*/
|
||||
predicate isArrayConstant(ExprCfgNode e, ArrayLiteralCfgNode arr) {
|
||||
// [...]
|
||||
e = arr
|
||||
or
|
||||
// e = [...]; e
|
||||
isArrayConstant(getSource(e), arr)
|
||||
or
|
||||
isArrayExpr(e.getExpr(), arr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the expression `e` refers to an array constructed from the array literal `arr`.
|
||||
*/
|
||||
private predicate isArrayExpr(Expr e, ArrayLiteralCfgNode arr) {
|
||||
// e = [...]
|
||||
e = arr.getExpr()
|
||||
or
|
||||
// Like above, but handles the desugaring of array literals to Array.[] calls.
|
||||
e.getDesugared() = arr.getExpr()
|
||||
or
|
||||
// A = [...]; A
|
||||
// A = a; A
|
||||
isArrayExpr(e.(ConstantReadAccess).getValue(), arr)
|
||||
or
|
||||
// Recurse via CFG nodes. Necessary for example in:
|
||||
// a = [...]
|
||||
// A = a
|
||||
// A
|
||||
//
|
||||
// We map from A to a via ConstantReadAccess::getValue, yielding the Expr a.
|
||||
// To get to [...] we need to go via getSource(ExprCfgNode e), so we find a
|
||||
// CFG node for a and call `isArrayConstant`.
|
||||
//
|
||||
// The use of `forex` is intended to ensure that a is an array constant in all
|
||||
// control flow paths.
|
||||
// Note(hmac): I don't think this is necessary, as `getSource` will not return
|
||||
// results if the source is a phi node.
|
||||
forex(ExprCfgNode n | n = e.getAControlFlowNode() | isArrayConstant(n, arr))
|
||||
}
|
||||
|
||||
@@ -374,6 +374,9 @@ module ExprNodes {
|
||||
MethodCallCfgNode() { super.getExpr() instanceof MethodCall }
|
||||
|
||||
override MethodCall getExpr() { result = super.getExpr() }
|
||||
|
||||
/** Gets the name of this method call. */
|
||||
string getMethodName() { result = this.getExpr().getMethodName() }
|
||||
}
|
||||
|
||||
private class CaseExprChildMapping extends ExprChildMapping, CaseExpr {
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
private import ruby
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.CFG
|
||||
private import codeql.ruby.controlflow.CfgNodes
|
||||
private import codeql.ruby.dataflow.SSA
|
||||
private import codeql.ruby.ast.internal.Constant
|
||||
private import codeql.ruby.InclusionTests
|
||||
|
||||
private predicate stringConstCompare(CfgNodes::ExprCfgNode g, CfgNode e, boolean branch) {
|
||||
exists(CfgNodes::ExprNodes::ComparisonOperationCfgNode c |
|
||||
@@ -61,19 +65,7 @@ deprecated class StringConstCompare extends DataFlow::BarrierGuard,
|
||||
// The value of the condition that results in the node being validated.
|
||||
private boolean checkedBranch;
|
||||
|
||||
StringConstCompare() {
|
||||
exists(CfgNodes::ExprNodes::StringLiteralCfgNode strLitNode |
|
||||
this.getExpr() instanceof EqExpr and checkedBranch = true
|
||||
or
|
||||
this.getExpr() instanceof CaseEqExpr and checkedBranch = true
|
||||
or
|
||||
this.getExpr() instanceof NEExpr and checkedBranch = false
|
||||
|
|
||||
this.getLeftOperand() = strLitNode and this.getRightOperand() = checkedNode
|
||||
or
|
||||
this.getLeftOperand() = checkedNode and this.getRightOperand() = strLitNode
|
||||
)
|
||||
}
|
||||
StringConstCompare() { stringConstCompare(this, checkedNode, checkedBranch) }
|
||||
|
||||
override predicate checks(CfgNode expr, boolean branch) {
|
||||
expr = checkedNode and branch = checkedBranch
|
||||
@@ -81,15 +73,19 @@ deprecated class StringConstCompare extends DataFlow::BarrierGuard,
|
||||
}
|
||||
|
||||
private predicate stringConstArrayInclusionCall(CfgNodes::ExprCfgNode g, CfgNode e, boolean branch) {
|
||||
exists(CfgNodes::ExprNodes::MethodCallCfgNode mc, ArrayLiteral aLit |
|
||||
mc = g and
|
||||
mc.getExpr().getMethodName() = "include?" and
|
||||
[mc.getExpr().getReceiver(), mc.getExpr().getReceiver().(ConstantReadAccess).getValue()] = aLit
|
||||
exists(InclusionTest t |
|
||||
t.asExpr() = g and
|
||||
e = t.getContainedNode().asExpr() and
|
||||
branch = t.getPolarity()
|
||||
|
|
||||
forall(Expr elem | elem = aLit.getAnElement() | elem instanceof StringLiteral) and
|
||||
mc.getArgument(0) = e
|
||||
) and
|
||||
branch = true
|
||||
exists(ExprNodes::ArrayLiteralCfgNode arr |
|
||||
isArrayConstant(t.getContainerNode().asExpr(), arr)
|
||||
|
|
||||
forall(ExprCfgNode elem | elem = arr.getAnArgument() |
|
||||
elem instanceof ExprNodes::StringLiteralCfgNode
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,16 +128,7 @@ deprecated class StringConstArrayInclusionCall extends DataFlow::BarrierGuard,
|
||||
CfgNodes::ExprNodes::MethodCallCfgNode {
|
||||
private CfgNode checkedNode;
|
||||
|
||||
StringConstArrayInclusionCall() {
|
||||
exists(ArrayLiteral aLit |
|
||||
this.getExpr().getMethodName() = "include?" and
|
||||
[this.getExpr().getReceiver(), this.getExpr().getReceiver().(ConstantReadAccess).getValue()] =
|
||||
aLit
|
||||
|
|
||||
forall(Expr elem | elem = aLit.getAnElement() | elem instanceof StringLiteral) and
|
||||
this.getArgument(0) = checkedNode
|
||||
)
|
||||
}
|
||||
StringConstArrayInclusionCall() { stringConstArrayInclusionCall(this, checkedNode, true) }
|
||||
|
||||
override predicate checks(CfgNode expr, boolean branch) { expr = checkedNode and branch = true }
|
||||
}
|
||||
|
||||
@@ -133,6 +133,11 @@ private Expr sqlFragmentArgument(MethodCall call) {
|
||||
or
|
||||
methodName = "reload" and
|
||||
result = call.getKeywordArgument("lock")
|
||||
or
|
||||
// Calls to `annotate` can be used to add block comments to SQL queries. These are potentially vulnerable to
|
||||
// SQLi if user supplied input is passed in as an argument.
|
||||
methodName = "annotate" and
|
||||
result = call.getArgument(_)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
31
ruby/ql/lib/codeql/ruby/frameworks/Arel.qll
Normal file
31
ruby/ql/lib/codeql/ruby/frameworks/Arel.qll
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Provides modeling for Arel, a low level SQL library that powers ActiveRecord.
|
||||
* Version: 7.0.3
|
||||
* https://api.rubyonrails.org/classes/Arel.html
|
||||
*/
|
||||
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import codeql.ruby.dataflow.FlowSummary
|
||||
|
||||
/**
|
||||
* Provides modeling for Arel, a low level SQL library that powers ActiveRecord.
|
||||
* Version: 7.0.3
|
||||
* https://api.rubyonrails.org/classes/Arel.html
|
||||
*/
|
||||
module Arel {
|
||||
/**
|
||||
* Flow summary for `Arel.sql`. This method wraps a SQL string, marking it as
|
||||
* safe.
|
||||
*/
|
||||
private class SqlSummary extends SummarizedCallable {
|
||||
SqlSummary() { this = "Arel.sql" }
|
||||
|
||||
override MethodCall getACall() {
|
||||
result = API::getTopLevelMember("Arel").getAMethodCall("sql").asExpr().getExpr()
|
||||
}
|
||||
|
||||
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
|
||||
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,4 @@
|
||||
|
||||
import stdlib.Open3
|
||||
import stdlib.Logger
|
||||
import stdlib.Pathname
|
||||
|
||||
188
ruby/ql/lib/codeql/ruby/frameworks/stdlib/Pathname.qll
Normal file
188
ruby/ql/lib/codeql/ruby/frameworks/stdlib/Pathname.qll
Normal file
@@ -0,0 +1,188 @@
|
||||
/** Modeling of the `Pathname` class from the Ruby standard library. */
|
||||
|
||||
private import codeql.ruby.AST
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.dataflow.FlowSummary
|
||||
private import codeql.ruby.dataflow.internal.DataFlowDispatch
|
||||
private import codeql.ruby.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Modeling of the `Pathname` class from the Ruby standard library.
|
||||
*
|
||||
* https://docs.ruby-lang.org/en/3.1/Pathname.html
|
||||
*/
|
||||
module Pathname {
|
||||
/**
|
||||
* An instance of the `Pathname` class. For example, in
|
||||
*
|
||||
* ```rb
|
||||
* pn = Pathname.new "foo.txt'"
|
||||
* puts pn.read
|
||||
* ```
|
||||
*
|
||||
* there are three `PathnameInstance`s - the call to `Pathname.new`, the
|
||||
* assignment `pn = ...`, and the read access to `pn` on the second line.
|
||||
*
|
||||
* Every `PathnameInstance` is considered to be a `FileNameSource`.
|
||||
*/
|
||||
class PathnameInstance extends FileNameSource, DataFlow::Node {
|
||||
PathnameInstance() { this = pathnameInstance() }
|
||||
}
|
||||
|
||||
private DataFlow::Node pathnameInstance() {
|
||||
// A call to `Pathname.new`.
|
||||
result = API::getTopLevelMember("Pathname").getAnInstantiation()
|
||||
or
|
||||
// Class methods on `Pathname` that return a new `Pathname`.
|
||||
result = API::getTopLevelMember("Pathname").getAMethodCall(["getwd", "pwd",])
|
||||
or
|
||||
// Instance methods on `Pathname` that return a new `Pathname`.
|
||||
exists(DataFlow::CallNode c | result = c |
|
||||
c.getReceiver() = pathnameInstance() and
|
||||
c.getMethodName() =
|
||||
[
|
||||
"+", "/", "basename", "cleanpath", "expand_path", "join", "realpath",
|
||||
"relative_path_from", "sub", "sub_ext", "to_path"
|
||||
]
|
||||
)
|
||||
or
|
||||
exists(DataFlow::Node inst |
|
||||
inst = pathnameInstance() and
|
||||
inst.(DataFlow::LocalSourceNode).flowsTo(result)
|
||||
)
|
||||
}
|
||||
|
||||
/** A call where the receiver is a `Pathname`. */
|
||||
class PathnameCall extends DataFlow::CallNode {
|
||||
PathnameCall() { this.getReceiver() instanceof PathnameInstance }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `Pathname#open` or `Pathname#opendir`, considered as a
|
||||
* `FileSystemAccess`.
|
||||
*/
|
||||
class PathnameOpen extends FileSystemAccess::Range, PathnameCall {
|
||||
PathnameOpen() { this.getMethodName() = ["open", "opendir"] }
|
||||
|
||||
override DataFlow::Node getAPathArgument() { result = this.getReceiver() }
|
||||
}
|
||||
|
||||
/** A call to `Pathname#read`, considered as a `FileSystemReadAccess`. */
|
||||
class PathnameRead extends FileSystemReadAccess::Range, PathnameCall {
|
||||
PathnameRead() { this.getMethodName() = "read" }
|
||||
|
||||
// The path is the receiver (the `Pathname` object).
|
||||
override DataFlow::Node getAPathArgument() { result = this.getReceiver() }
|
||||
|
||||
// The read data is the return value of the call.
|
||||
override DataFlow::Node getADataNode() { result = this }
|
||||
}
|
||||
|
||||
/** A call to `Pathname#write`, considered as a `FileSystemWriteAccess`. */
|
||||
class PathnameWrite extends FileSystemWriteAccess::Range, PathnameCall {
|
||||
PathnameWrite() { this.getMethodName() = "write" }
|
||||
|
||||
// The path is the receiver (the `Pathname` object).
|
||||
override DataFlow::Node getAPathArgument() { result = this.getReceiver() }
|
||||
|
||||
// The data to write is the 0th argument.
|
||||
override DataFlow::Node getADataNode() { result = this.getArgument(0) }
|
||||
}
|
||||
|
||||
/** A call to `Pathname#to_s`, considered as a `FileNameSource`. */
|
||||
class PathnameToSFilenameSource extends FileNameSource, PathnameCall {
|
||||
PathnameToSFilenameSource() { this.getMethodName() = "to_s" }
|
||||
}
|
||||
|
||||
private class PathnamePermissionModification extends FileSystemPermissionModification::Range,
|
||||
PathnameCall {
|
||||
private DataFlow::Node permissionArg;
|
||||
|
||||
PathnamePermissionModification() {
|
||||
exists(string methodName | this.getMethodName() = methodName |
|
||||
methodName = ["chmod", "mkdir"] and permissionArg = this.getArgument(0)
|
||||
or
|
||||
methodName = "mkpath" and permissionArg = this.getKeywordArgument("mode")
|
||||
or
|
||||
methodName = "open" and permissionArg = this.getArgument(1)
|
||||
// TODO: defaults for optional args? This may depend on the umask
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getAPermissionNode() { result = permissionArg }
|
||||
}
|
||||
|
||||
/**
|
||||
* Type summaries for the `Pathname` class, i.e. method calls that produce new
|
||||
* `Pathname` instances.
|
||||
*/
|
||||
private class PathnameTypeSummary extends ModelInput::TypeModelCsv {
|
||||
override predicate row(string row) {
|
||||
// package1;type1;package2;type2;path
|
||||
row =
|
||||
[
|
||||
// Pathname.new : Pathname
|
||||
";Pathname;;;Member[Pathname].Instance",
|
||||
// Pathname#+(path) : Pathname
|
||||
";Pathname;;Pathname;Method[+].ReturnValue",
|
||||
// Pathname#/(path) : Pathname
|
||||
";Pathname;;Pathname;Method[/].ReturnValue",
|
||||
// Pathname#basename(path) : Pathname
|
||||
";Pathname;;Pathname;Method[basename].ReturnValue",
|
||||
// Pathname#cleanpath(path) : Pathname
|
||||
";Pathname;;Pathname;Method[cleanpath].ReturnValue",
|
||||
// Pathname#expand_path(path) : Pathname
|
||||
";Pathname;;Pathname;Method[expand_path].ReturnValue",
|
||||
// Pathname#join(path) : Pathname
|
||||
";Pathname;;Pathname;Method[join].ReturnValue",
|
||||
// Pathname#realpath(path) : Pathname
|
||||
";Pathname;;Pathname;Method[realpath].ReturnValue",
|
||||
// Pathname#relative_path_from(path) : Pathname
|
||||
";Pathname;;Pathname;Method[relative_path_from].ReturnValue",
|
||||
// Pathname#sub(path) : Pathname
|
||||
";Pathname;;Pathname;Method[sub].ReturnValue",
|
||||
// Pathname#sub_ext(path) : Pathname
|
||||
";Pathname;;Pathname;Method[sub_ext].ReturnValue",
|
||||
// Pathname#to_path(path) : Pathname
|
||||
";Pathname;;Pathname;Method[to_path].ReturnValue",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/** Taint flow summaries for the `Pathname` class. */
|
||||
private class PathnameTaintSummary extends ModelInput::SummaryModelCsv {
|
||||
override predicate row(string row) {
|
||||
row =
|
||||
[
|
||||
// Pathname.new(path)
|
||||
";;Member[Pathname].Method[new];Argument[0];ReturnValue;taint",
|
||||
// Pathname#dirname
|
||||
";Pathname;Method[dirname];Argument[self];ReturnValue;taint",
|
||||
// Pathname#each_filename
|
||||
";Pathname;Method[each_filename];Argument[self];Argument[block].Parameter[0];taint",
|
||||
// Pathname#expand_path
|
||||
";Pathname;Method[expand_path];Argument[self];ReturnValue;taint",
|
||||
// Pathname#join
|
||||
";Pathname;Method[join];Argument[self,any];ReturnValue;taint",
|
||||
// Pathname#parent
|
||||
";Pathname;Method[parent];Argument[self];ReturnValue;taint",
|
||||
// Pathname#realpath
|
||||
";Pathname;Method[realpath];Argument[self];ReturnValue;taint",
|
||||
// Pathname#relative_path_from
|
||||
";Pathname;Method[relative_path_from];Argument[self];ReturnValue;taint",
|
||||
// Pathname#to_path
|
||||
";Pathname;Method[to_path];Argument[self];ReturnValue;taint",
|
||||
// Pathname#basename
|
||||
";Pathname;Method[basename];Argument[self];ReturnValue;taint",
|
||||
// Pathname#cleanpath
|
||||
";Pathname;Method[cleanpath];Argument[self];ReturnValue;taint",
|
||||
// Pathname#sub
|
||||
";Pathname;Method[sub];Argument[self];ReturnValue;taint",
|
||||
// Pathname#sub_ext
|
||||
";Pathname;Method[sub_ext];Argument[self];ReturnValue;taint",
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/ruby-all
|
||||
version: 0.3.1
|
||||
version: 0.3.2-dev
|
||||
groups: ruby
|
||||
extractor: ruby
|
||||
dbscheme: ruby.dbscheme
|
||||
|
||||
4
ruby/ql/src/change-notes/2022-07-21-check-http-verb.md
Normal file
4
ruby/ql/src/change-notes/2022-07-21-check-http-verb.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: newQuery
|
||||
---
|
||||
* Added a new experimental query, `rb/manually-checking-http-verb`, to detect cases when the HTTP verb for an incoming request is checked and then used as part of control flow.
|
||||
4
ruby/ql/src/change-notes/2022-07-21-weak-params.md
Normal file
4
ruby/ql/src/change-notes/2022-07-21-weak-params.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: newQuery
|
||||
---
|
||||
* Added a new experimental query, `rb/weak-params`, to detect cases when the rails strong parameters pattern isn't followed and values flow into persistent store writes.
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Manually checking the HTTP request verb inside of a controller method can lead to
|
||||
CSRF bypass if GET or HEAD requests are handled improperly.
|
||||
</p>
|
||||
</overview>
|
||||
<recommendation>
|
||||
<p>
|
||||
It is better to use different controller methods for each resource/http verb combination
|
||||
and configure the Rails routes in your application to call them accordingly.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<references>
|
||||
<li>
|
||||
See https://guides.rubyonrails.org/routing.html for more information.
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @name Manually checking http verb instead of using built in rails routes and protections
|
||||
* @description Manually checking HTTP verbs is an indication that multiple requests are routed to the same controller action. This could lead to bypassing necessary authorization methods and other protections, like CSRF protection. Prefer using different controller actions for each HTTP method and relying Rails routing to handle mappting resources and verbs to specific methods.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 5.0
|
||||
* @precision low
|
||||
* @id rb/manually-checking-http-verb
|
||||
* @tags security
|
||||
*/
|
||||
|
||||
import ruby
|
||||
import codeql.ruby.DataFlow
|
||||
import codeql.ruby.controlflow.CfgNodes
|
||||
import codeql.ruby.frameworks.ActionController
|
||||
import codeql.ruby.TaintTracking
|
||||
import DataFlow::PathGraph
|
||||
|
||||
// any `request` calls in an action method
|
||||
class Request extends DataFlow::CallNode {
|
||||
Request() {
|
||||
this.getMethodName() = "request" and
|
||||
this.asExpr().getExpr().getEnclosingMethod() instanceof ActionControllerActionMethod
|
||||
}
|
||||
}
|
||||
|
||||
// `request.env`
|
||||
class RequestEnvMethod extends DataFlow::CallNode {
|
||||
RequestEnvMethod() {
|
||||
this.getMethodName() = "env" and
|
||||
any(Request r).flowsTo(this.getReceiver())
|
||||
}
|
||||
}
|
||||
|
||||
// `request.request_method`
|
||||
class RequestRequestMethod extends DataFlow::CallNode {
|
||||
RequestRequestMethod() {
|
||||
this.getMethodName() = "request_method" and
|
||||
any(Request r).flowsTo(this.getReceiver())
|
||||
}
|
||||
}
|
||||
|
||||
// `request.method`
|
||||
class RequestMethod extends DataFlow::CallNode {
|
||||
RequestMethod() {
|
||||
this.getMethodName() = "method" and
|
||||
any(Request r).flowsTo(this.getReceiver())
|
||||
}
|
||||
}
|
||||
|
||||
// `request.raw_request_method`
|
||||
class RequestRawRequestMethod extends DataFlow::CallNode {
|
||||
RequestRawRequestMethod() {
|
||||
this.getMethodName() = "raw_request_method" and
|
||||
any(Request r).flowsTo(this.getReceiver())
|
||||
}
|
||||
}
|
||||
|
||||
// `request.request_method_symbol`
|
||||
class RequestRequestMethodSymbol extends DataFlow::CallNode {
|
||||
RequestRequestMethodSymbol() {
|
||||
this.getMethodName() = "request_method_symbol" and
|
||||
any(Request r).flowsTo(this.getReceiver())
|
||||
}
|
||||
}
|
||||
|
||||
// `request.get?`
|
||||
class RequestGet extends DataFlow::CallNode {
|
||||
RequestGet() {
|
||||
this.getMethodName() = "get?" and
|
||||
any(Request r).flowsTo(this.getReceiver())
|
||||
}
|
||||
}
|
||||
|
||||
class HttpVerbConfig extends TaintTracking::Configuration {
|
||||
HttpVerbConfig() { this = "HttpVerbConfig" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) {
|
||||
source instanceof RequestMethod or
|
||||
source instanceof RequestRequestMethod or
|
||||
source instanceof RequestEnvMethod or
|
||||
source instanceof RequestRawRequestMethod or
|
||||
source instanceof RequestRequestMethodSymbol or
|
||||
source instanceof RequestGet
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
exists(ExprNodes::ConditionalExprCfgNode c | c.getCondition() = sink.asExpr()) or
|
||||
exists(ExprNodes::CaseExprCfgNode c | c.getValue() = sink.asExpr())
|
||||
}
|
||||
}
|
||||
|
||||
from HttpVerbConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where config.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink,
|
||||
"Manually checking HTTP verbs is an indication that multiple requests are routed to the same controller action. This could lead to bypassing necessary authorization methods and other protections, like CSRF protection. Prefer using different controller actions for each HTTP method and relying Rails routing to handle mappting resources and verbs to specific methods."
|
||||
28
ruby/ql/src/experimental/weak-params/WeakParams.qhelp
Normal file
28
ruby/ql/src/experimental/weak-params/WeakParams.qhelp
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Directly checking request parameters without following a strong params
|
||||
pattern can lead to unintentional avenues for injection attacks.
|
||||
</p>
|
||||
</overview>
|
||||
<recommendation>
|
||||
<p>
|
||||
Instead of manually checking parameters from the `param` object, it is
|
||||
recommended that you follow the strong parameters pattern established in
|
||||
Rails: https://api.rubyonrails.org/classes/ActionController/StrongParameters.html
|
||||
</p>
|
||||
<p>
|
||||
In the strong parameters pattern, you are able to specify required and allowed
|
||||
parameters for each action called by your controller methods. This acts as an
|
||||
additional layer of data validation before being passed along to other areas
|
||||
of your application, such as the model.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<references>
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
61
ruby/ql/src/experimental/weak-params/WeakParams.ql
Normal file
61
ruby/ql/src/experimental/weak-params/WeakParams.ql
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @name Weak or direct parameter references are used
|
||||
* @description Directly checking request parameters without following a strong params pattern can lead to unintentional avenues for injection attacks.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 5.0
|
||||
* @precision medium
|
||||
* @id rb/weak-params
|
||||
* @tags security
|
||||
*/
|
||||
|
||||
import ruby
|
||||
import codeql.ruby.Concepts
|
||||
import codeql.ruby.DataFlow
|
||||
import codeql.ruby.TaintTracking
|
||||
import codeql.ruby.frameworks.ActionController
|
||||
import DataFlow::PathGraph
|
||||
|
||||
/**
|
||||
* A call to `request` in an ActionController controller class.
|
||||
* This probably refers to the incoming HTTP request object.
|
||||
*/
|
||||
class ActionControllerRequest extends DataFlow::Node {
|
||||
ActionControllerRequest() {
|
||||
exists(DataFlow::CallNode c |
|
||||
c.asExpr().getExpr().getEnclosingModule() instanceof ActionControllerControllerClass and
|
||||
c.getMethodName() = "request"
|
||||
|
|
||||
c.flowsTo(this)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A direct parameters reference that happens inside a controller class.
|
||||
*/
|
||||
class WeakParams extends DataFlow::CallNode {
|
||||
WeakParams() {
|
||||
this.getReceiver() instanceof ActionControllerRequest and
|
||||
this.getMethodName() =
|
||||
["path_parameters", "query_parameters", "request_parameters", "GET", "POST"]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Taint tracking config where the source is a weak params access in a controller and the sink
|
||||
* is a method call of a model class
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "WeakParamsConfiguration" }
|
||||
|
||||
override predicate isSource(DataFlow::Node node) { node instanceof WeakParams }
|
||||
|
||||
// the sink is an instance of a Model class that receives a method call
|
||||
override predicate isSink(DataFlow::Node node) { node = any(PersistentWriteAccess a).getValue() }
|
||||
}
|
||||
|
||||
from Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where config.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink,
|
||||
"By exposing all keys in request parameters or by blindy accessing them, unintended parameters could be used and lead to mass-assignment or have other unexpected side-effects. It is safer to follow the 'strong parameters' pattern in Rails, which is outlined here: https://api.rubyonrails.org/classes/ActionController/StrongParameters.html"
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/ruby-queries
|
||||
version: 0.3.0
|
||||
version: 0.3.1-dev
|
||||
groups:
|
||||
- ruby
|
||||
- queries
|
||||
|
||||
@@ -1556,6 +1556,35 @@ constants/constants.rb:
|
||||
# 73| getAnOperand/getLeftOperand: [ClassVariableAccess] @@fourty_six
|
||||
# 73| getAnOperand/getRightOperand: [ConstantReadAccess] FOURTY_SIX
|
||||
# 73| getScopeExpr: [ConstantReadAccess] Mod3
|
||||
# 78| getStmt: [AssignExpr] ... = ...
|
||||
# 78| getAnOperand/getLeftOperand: [LocalVariableAccess] a
|
||||
# 78| getAnOperand/getRightOperand: [ArrayLiteral] [...]
|
||||
# 78| getElement: [IntegerLiteral] 1
|
||||
# 78| getElement: [IntegerLiteral] 2
|
||||
# 78| getElement: [IntegerLiteral] 3
|
||||
# 79| getStmt: [AssignExpr] ... = ...
|
||||
# 79| getAnOperand/getLeftOperand: [ConstantAssignment] A
|
||||
# 79| getAnOperand/getRightOperand: [ArrayLiteral] [...]
|
||||
# 79| getElement: [IntegerLiteral] 1
|
||||
# 79| getElement: [IntegerLiteral] 2
|
||||
# 79| getElement: [IntegerLiteral] 3
|
||||
# 80| getStmt: [AssignExpr] ... = ...
|
||||
# 80| getAnOperand/getLeftOperand: [ConstantAssignment] B
|
||||
# 80| getAnOperand/getRightOperand: [LocalVariableAccess] a
|
||||
# 81| getStmt: [AssignExpr] ... = ...
|
||||
# 81| getAnOperand/getLeftOperand: [ConstantAssignment] C
|
||||
# 81| getAnOperand/getRightOperand: [ConstantReadAccess] A
|
||||
# 82| getStmt: [AssignExpr] ... = ...
|
||||
# 82| getAnOperand/getLeftOperand: [LocalVariableAccess] b
|
||||
# 82| getAnOperand/getRightOperand: [ConstantReadAccess] B
|
||||
# 84| getStmt: [IfExpr] if ...
|
||||
# 84| getCondition: [MethodCall] call to condition
|
||||
# 84| getReceiver: [SelfVariableAccess] self
|
||||
# 84| getBranch/getThen: [StmtSequence] then ...
|
||||
# 85| getStmt: [AssignExpr] ... = ...
|
||||
# 85| getAnOperand/getLeftOperand: [LocalVariableAccess] c
|
||||
# 85| getAnOperand/getRightOperand: [LocalVariableAccess] b
|
||||
# 87| getStmt: [LocalVariableAccess] c
|
||||
escape_sequences/escapes.rb:
|
||||
# 1| [Toplevel] escapes.rb
|
||||
# 6| getStmt: [StringLiteral] "\'"
|
||||
@@ -1733,6 +1762,12 @@ escape_sequences/escapes.rb:
|
||||
# 93| getStmt: [SymbolLiteral] :"\C-?"
|
||||
# 93| getComponent: [StringEscapeSequenceComponent] \C
|
||||
# 93| getComponent: [StringTextComponent] -?
|
||||
misc/iso-8859-15.rb:
|
||||
# 1| [Toplevel] iso-8859-15.rb
|
||||
# 4| getStmt: [MethodCall] call to print
|
||||
# 4| getReceiver: [SelfVariableAccess] self
|
||||
# 4| getArgument: [StringLiteral] "EUR = €"
|
||||
# 4| getComponent: [StringTextComponent] EUR = €
|
||||
literals/literals.rb:
|
||||
# 1| [Toplevel] literals.rb
|
||||
# 2| getStmt: [NilLiteral] nil
|
||||
|
||||
@@ -336,6 +336,18 @@ constants/constants.rb:
|
||||
# 20| getComponent: [StringTextComponent] Chuck
|
||||
# 20| getArgument: [StringLiteral] "Dave"
|
||||
# 20| getComponent: [StringTextComponent] Dave
|
||||
# 78| [ArrayLiteral] [...]
|
||||
# 78| getDesugared: [MethodCall] call to []
|
||||
# 78| getReceiver: [ConstantReadAccess] Array
|
||||
# 78| getArgument: [IntegerLiteral] 1
|
||||
# 78| getArgument: [IntegerLiteral] 2
|
||||
# 78| getArgument: [IntegerLiteral] 3
|
||||
# 79| [ArrayLiteral] [...]
|
||||
# 79| getDesugared: [MethodCall] call to []
|
||||
# 79| getReceiver: [ConstantReadAccess] Array
|
||||
# 79| getArgument: [IntegerLiteral] 1
|
||||
# 79| getArgument: [IntegerLiteral] 2
|
||||
# 79| getArgument: [IntegerLiteral] 3
|
||||
escape_sequences/escapes.rb:
|
||||
# 58| [ArrayLiteral] %w(...)
|
||||
# 58| getDesugared: [MethodCall] call to []
|
||||
|
||||
@@ -1656,10 +1656,56 @@ constants/constants.rb:
|
||||
# 73| 1: [ReservedWord] ::
|
||||
# 73| 2: [Constant] FOURTY_SIX
|
||||
# 74| 5: [ReservedWord] end
|
||||
# 78| 13: [Assignment] Assignment
|
||||
# 78| 0: [Identifier] a
|
||||
# 78| 1: [ReservedWord] =
|
||||
# 78| 2: [Array] Array
|
||||
# 78| 0: [ReservedWord] [
|
||||
# 78| 1: [Integer] 1
|
||||
# 78| 2: [ReservedWord] ,
|
||||
# 78| 3: [Integer] 2
|
||||
# 78| 4: [ReservedWord] ,
|
||||
# 78| 5: [Integer] 3
|
||||
# 78| 6: [ReservedWord] ]
|
||||
# 79| 14: [Assignment] Assignment
|
||||
# 79| 0: [Constant] A
|
||||
# 79| 1: [ReservedWord] =
|
||||
# 79| 2: [Array] Array
|
||||
# 79| 0: [ReservedWord] [
|
||||
# 79| 1: [Integer] 1
|
||||
# 79| 2: [ReservedWord] ,
|
||||
# 79| 3: [Integer] 2
|
||||
# 79| 4: [ReservedWord] ,
|
||||
# 79| 5: [Integer] 3
|
||||
# 79| 6: [ReservedWord] ]
|
||||
# 80| 15: [Assignment] Assignment
|
||||
# 80| 0: [Constant] B
|
||||
# 80| 1: [ReservedWord] =
|
||||
# 80| 2: [Identifier] a
|
||||
# 81| 16: [Assignment] Assignment
|
||||
# 81| 0: [Constant] C
|
||||
# 81| 1: [ReservedWord] =
|
||||
# 81| 2: [Constant] A
|
||||
# 82| 17: [Assignment] Assignment
|
||||
# 82| 0: [Identifier] b
|
||||
# 82| 1: [ReservedWord] =
|
||||
# 82| 2: [Constant] B
|
||||
# 84| 18: [If] If
|
||||
# 84| 0: [ReservedWord] if
|
||||
# 84| 1: [Identifier] condition
|
||||
# 84| 2: [Then] Then
|
||||
# 85| 0: [Assignment] Assignment
|
||||
# 85| 0: [Identifier] c
|
||||
# 85| 1: [ReservedWord] =
|
||||
# 85| 2: [Identifier] b
|
||||
# 86| 3: [ReservedWord] end
|
||||
# 87| 19: [Identifier] c
|
||||
# 26| [Comment] # A call to Kernel::Array; despite beginning with an upper-case character,
|
||||
# 27| [Comment] # we don't consider this to be a constant access.
|
||||
# 55| [Comment] # refers to ::ModuleA::FOURTY_FOUR
|
||||
# 57| [Comment] # refers to ::ModuleA::ModuleB::ClassB::FOURTY_FOUR
|
||||
# 76| [Comment] # Array constants
|
||||
# 87| [Comment] # not recognised
|
||||
control/cases.rb:
|
||||
# 1| [Program] Program
|
||||
# 2| 0: [Assignment] Assignment
|
||||
@@ -4558,6 +4604,17 @@ literals/literals.rb:
|
||||
# 193| cat file.txt
|
||||
# 193|
|
||||
# 195| 1: [HeredocEnd] SCRIPT
|
||||
misc/iso-8859-15.rb:
|
||||
# 1| [Program] Program
|
||||
# 4| 0: [Call] Call
|
||||
# 4| 0: [Identifier] print
|
||||
# 4| 1: [ArgumentList] ArgumentList
|
||||
# 4| 0: [String] String
|
||||
# 4| 0: [ReservedWord] "
|
||||
# 4| 1: [StringContent] EUR = €
|
||||
# 4| 2: [ReservedWord] "
|
||||
# 1| [Comment] #! /usr/bin/ruby
|
||||
# 2| [Comment] # coding: iso-8859-15
|
||||
misc/misc.erb:
|
||||
# 2| [Program] Program
|
||||
# 2| 0: [Call] Call
|
||||
|
||||
@@ -109,6 +109,12 @@ exprValue
|
||||
| constants/constants.rb:63:19:63:20 | 45 | 45 | int |
|
||||
| constants/constants.rb:65:19:65:35 | FOURTY_FIVE | 45 | int |
|
||||
| constants/constants.rb:71:18:71:19 | 46 | 46 | int |
|
||||
| constants/constants.rb:78:6:78:6 | 1 | 1 | int |
|
||||
| constants/constants.rb:78:9:78:9 | 2 | 2 | int |
|
||||
| constants/constants.rb:78:12:78:12 | 3 | 3 | int |
|
||||
| constants/constants.rb:79:6:79:6 | 1 | 1 | int |
|
||||
| constants/constants.rb:79:9:79:9 | 2 | 2 | int |
|
||||
| constants/constants.rb:79:12:79:12 | 3 | 3 | int |
|
||||
| control/cases.rb:2:5:2:5 | 0 | 0 | int |
|
||||
| control/cases.rb:3:5:3:5 | 0 | 0 | int |
|
||||
| control/cases.rb:4:5:4:5 | 0 | 0 | int |
|
||||
@@ -711,6 +717,7 @@ exprValue
|
||||
| literals/literals.rb:198:8:198:8 | 5 | 5 | int |
|
||||
| literals/literals.rb:199:2:199:2 | :y | :y | symbol |
|
||||
| literals/literals.rb:199:7:199:7 | :Z | :Z | symbol |
|
||||
| misc/iso-8859-15.rb:4:7:4:17 | "EUR = \u20ac" | EUR = \u20ac | string |
|
||||
| misc/misc.erb:2:15:2:37 | "main_include_admin.js" | main_include_admin.js | string |
|
||||
| misc/misc.rb:1:7:1:11 | "bar" | bar | string |
|
||||
| misc/misc.rb:3:7:3:9 | foo | foo | string |
|
||||
@@ -1004,6 +1011,12 @@ exprCfgNodeValue
|
||||
| constants/constants.rb:63:19:63:20 | 45 | 45 | int |
|
||||
| constants/constants.rb:65:19:65:35 | FOURTY_FIVE | 45 | int |
|
||||
| constants/constants.rb:71:18:71:19 | 46 | 46 | int |
|
||||
| constants/constants.rb:78:6:78:6 | 1 | 1 | int |
|
||||
| constants/constants.rb:78:9:78:9 | 2 | 2 | int |
|
||||
| constants/constants.rb:78:12:78:12 | 3 | 3 | int |
|
||||
| constants/constants.rb:79:6:79:6 | 1 | 1 | int |
|
||||
| constants/constants.rb:79:9:79:9 | 2 | 2 | int |
|
||||
| constants/constants.rb:79:12:79:12 | 3 | 3 | int |
|
||||
| control/cases.rb:2:5:2:5 | 0 | 0 | int |
|
||||
| control/cases.rb:3:5:3:5 | 0 | 0 | int |
|
||||
| control/cases.rb:4:5:4:5 | 0 | 0 | int |
|
||||
@@ -1580,6 +1593,7 @@ exprCfgNodeValue
|
||||
| literals/literals.rb:198:8:198:8 | 5 | 5 | int |
|
||||
| literals/literals.rb:199:2:199:2 | :y | :y | symbol |
|
||||
| literals/literals.rb:199:7:199:7 | :Z | :Z | symbol |
|
||||
| misc/iso-8859-15.rb:4:7:4:17 | "EUR = \u20ac" | EUR = \u20ac | string |
|
||||
| misc/misc.erb:2:15:2:37 | "main_include_admin.js" | main_include_admin.js | string |
|
||||
| misc/misc.rb:1:7:1:11 | "bar" | bar | string |
|
||||
| misc/misc.rb:3:7:3:9 | foo | foo | string |
|
||||
|
||||
@@ -61,6 +61,13 @@ constantAccess
|
||||
| constants.rb:71:5:71:14 | FOURTY_SIX | write | FOURTY_SIX | ConstantAssignment |
|
||||
| constants.rb:73:18:73:21 | Mod3 | read | Mod3 | ConstantReadAccess |
|
||||
| constants.rb:73:18:73:33 | FOURTY_SIX | read | FOURTY_SIX | ConstantReadAccess |
|
||||
| constants.rb:78:5:78:13 | Array | read | Array | ConstantReadAccess |
|
||||
| constants.rb:79:1:79:1 | A | write | A | ConstantAssignment |
|
||||
| constants.rb:79:5:79:13 | Array | read | Array | ConstantReadAccess |
|
||||
| constants.rb:80:1:80:1 | B | write | B | ConstantAssignment |
|
||||
| constants.rb:81:1:81:1 | C | write | C | ConstantAssignment |
|
||||
| constants.rb:81:5:81:5 | A | read | A | ConstantReadAccess |
|
||||
| constants.rb:82:5:82:5 | B | read | B | ConstantReadAccess |
|
||||
getConst
|
||||
| constants.rb:1:1:15:3 | ModuleA | CONST_B | constants.rb:6:15:6:23 | "const_b" |
|
||||
| constants.rb:1:1:15:3 | ModuleA | FOURTY_FOUR | constants.rb:53:17:53:29 | "fourty-four" |
|
||||
@@ -71,23 +78,41 @@ getConst
|
||||
| constants.rb:54:3:58:5 | ModuleA::ModuleB::ClassB | FOURTY_ONE | constants.rb:48:18:48:19 | 41 |
|
||||
| constants.rb:62:3:64:5 | Mod1::Mod3 | FOURTY_FIVE | constants.rb:63:19:63:20 | 45 |
|
||||
| constants.rb:70:3:72:5 | Mod1::Mod3::Mod5 | FOURTY_SIX | constants.rb:71:18:71:19 | 46 |
|
||||
| file://:0:0:0:0 | Object | A | constants.rb:79:5:79:13 | [...] |
|
||||
| file://:0:0:0:0 | Object | B | constants.rb:80:5:80:5 | a |
|
||||
| file://:0:0:0:0 | Object | C | constants.rb:81:5:81:5 | A |
|
||||
| file://:0:0:0:0 | Object | GREETING | constants.rb:17:12:17:64 | ... + ... |
|
||||
lookupConst
|
||||
| constants.rb:1:1:15:3 | ModuleA | CONST_B | constants.rb:6:15:6:23 | "const_b" |
|
||||
| constants.rb:1:1:15:3 | ModuleA | FOURTY_FOUR | constants.rb:53:17:53:29 | "fourty-four" |
|
||||
| constants.rb:2:5:4:7 | ModuleA::ClassA | A | constants.rb:79:5:79:13 | [...] |
|
||||
| constants.rb:2:5:4:7 | ModuleA::ClassA | B | constants.rb:80:5:80:5 | a |
|
||||
| constants.rb:2:5:4:7 | ModuleA::ClassA | C | constants.rb:81:5:81:5 | A |
|
||||
| constants.rb:2:5:4:7 | ModuleA::ClassA | CONST_A | constants.rb:3:19:3:27 | "const_a" |
|
||||
| constants.rb:2:5:4:7 | ModuleA::ClassA | GREETING | constants.rb:17:12:17:64 | ... + ... |
|
||||
| constants.rb:8:5:14:7 | ModuleA::ModuleB | MAX_SIZE | constants.rb:39:30:39:33 | 1024 |
|
||||
| constants.rb:12:9:13:11 | ModuleA::ModuleB::ClassC | A | constants.rb:79:5:79:13 | [...] |
|
||||
| constants.rb:12:9:13:11 | ModuleA::ModuleB::ClassC | B | constants.rb:80:5:80:5 | a |
|
||||
| constants.rb:12:9:13:11 | ModuleA::ModuleB::ClassC | C | constants.rb:81:5:81:5 | A |
|
||||
| constants.rb:12:9:13:11 | ModuleA::ModuleB::ClassC | GREETING | constants.rb:17:12:17:64 | ... + ... |
|
||||
| constants.rb:31:1:33:3 | ModuleA::ClassD | A | constants.rb:79:5:79:13 | [...] |
|
||||
| constants.rb:31:1:33:3 | ModuleA::ClassD | B | constants.rb:80:5:80:5 | a |
|
||||
| constants.rb:31:1:33:3 | ModuleA::ClassD | C | constants.rb:81:5:81:5 | A |
|
||||
| constants.rb:31:1:33:3 | ModuleA::ClassD | CONST_A | constants.rb:3:19:3:27 | "const_a" |
|
||||
| constants.rb:31:1:33:3 | ModuleA::ClassD | FOURTY_TWO | constants.rb:32:16:32:17 | 42 |
|
||||
| constants.rb:31:1:33:3 | ModuleA::ClassD | GREETING | constants.rb:17:12:17:64 | ... + ... |
|
||||
| constants.rb:35:1:37:3 | ModuleA::ModuleC | FOURTY_THREE | constants.rb:36:18:36:19 | 43 |
|
||||
| constants.rb:54:3:58:5 | ModuleA::ModuleB::ClassB | A | constants.rb:79:5:79:13 | [...] |
|
||||
| constants.rb:54:3:58:5 | ModuleA::ModuleB::ClassB | B | constants.rb:80:5:80:5 | a |
|
||||
| constants.rb:54:3:58:5 | ModuleA::ModuleB::ClassB | C | constants.rb:81:5:81:5 | A |
|
||||
| constants.rb:54:3:58:5 | ModuleA::ModuleB::ClassB | FOURTY_FOUR | constants.rb:56:19:56:20 | 44 |
|
||||
| constants.rb:54:3:58:5 | ModuleA::ModuleB::ClassB | FOURTY_ONE | constants.rb:48:18:48:19 | 41 |
|
||||
| constants.rb:54:3:58:5 | ModuleA::ModuleB::ClassB | GREETING | constants.rb:17:12:17:64 | ... + ... |
|
||||
| constants.rb:62:3:64:5 | Mod1::Mod3 | FOURTY_FIVE | constants.rb:63:19:63:20 | 45 |
|
||||
| constants.rb:70:3:72:5 | Mod1::Mod3::Mod5 | FOURTY_SIX | constants.rb:71:18:71:19 | 46 |
|
||||
| file://:0:0:0:0 | Object | A | constants.rb:79:5:79:13 | [...] |
|
||||
| file://:0:0:0:0 | Object | B | constants.rb:80:5:80:5 | a |
|
||||
| file://:0:0:0:0 | Object | C | constants.rb:81:5:81:5 | A |
|
||||
| file://:0:0:0:0 | Object | GREETING | constants.rb:17:12:17:64 | ... + ... |
|
||||
constantValue
|
||||
| constants.rb:17:22:17:45 | CONST_A | constants.rb:3:19:3:27 | "const_a" |
|
||||
@@ -101,6 +126,8 @@ constantValue
|
||||
| constants.rb:57:21:57:31 | FOURTY_FOUR | constants.rb:53:17:53:29 | "fourty-four" |
|
||||
| constants.rb:57:21:57:31 | FOURTY_FOUR | constants.rb:56:19:56:20 | 44 |
|
||||
| constants.rb:65:19:65:35 | FOURTY_FIVE | constants.rb:63:19:63:20 | 45 |
|
||||
| constants.rb:81:5:81:5 | A | constants.rb:79:5:79:13 | [...] |
|
||||
| constants.rb:82:5:82:5 | B | constants.rb:80:5:80:5 | a |
|
||||
constantWriteAccessQualifiedName
|
||||
| constants.rb:1:1:15:3 | ModuleA | ModuleA |
|
||||
| constants.rb:2:5:4:7 | ClassA | ModuleA::ClassA |
|
||||
@@ -133,3 +160,14 @@ constantWriteAccessQualifiedName
|
||||
| constants.rb:70:3:72:5 | Mod5 | Mod3::Mod5 |
|
||||
| constants.rb:71:5:71:14 | FOURTY_SIX | Mod1::Mod3::Mod5::FOURTY_SIX |
|
||||
| constants.rb:71:5:71:14 | FOURTY_SIX | Mod3::Mod5::FOURTY_SIX |
|
||||
| constants.rb:79:1:79:1 | A | A |
|
||||
| constants.rb:80:1:80:1 | B | B |
|
||||
| constants.rb:81:1:81:1 | C | C |
|
||||
arrayConstant
|
||||
| constants.rb:20:13:20:37 | call to [] | constants.rb:20:13:20:37 | call to [] |
|
||||
| constants.rb:78:5:78:13 | call to [] | constants.rb:78:5:78:13 | call to [] |
|
||||
| constants.rb:79:5:79:13 | call to [] | constants.rb:79:5:79:13 | call to [] |
|
||||
| constants.rb:80:5:80:5 | a | constants.rb:78:5:78:13 | call to [] |
|
||||
| constants.rb:81:5:81:5 | A | constants.rb:79:5:79:13 | call to [] |
|
||||
| constants.rb:82:5:82:5 | B | constants.rb:78:5:78:13 | call to [] |
|
||||
| constants.rb:85:7:85:7 | b | constants.rb:78:5:78:13 | call to [] |
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ruby
|
||||
import codeql.ruby.ast.internal.Module as M
|
||||
import codeql.ruby.ast.internal.Constant
|
||||
|
||||
query predicate constantAccess(ConstantAccess a, string kind, string name, string cls) {
|
||||
(
|
||||
@@ -20,3 +21,5 @@ query predicate constantValue(ConstantReadAccess a, Expr e) { e = a.getValue() }
|
||||
query predicate constantWriteAccessQualifiedName(ConstantWriteAccess w, string qualifiedName) {
|
||||
w.getAQualifiedName() = qualifiedName
|
||||
}
|
||||
|
||||
query predicate arrayConstant = isArrayConstant/2;
|
||||
|
||||
@@ -72,3 +72,16 @@ module Mod4
|
||||
end
|
||||
@@fourty_six = Mod3::FOURTY_SIX
|
||||
end
|
||||
|
||||
# Array constants
|
||||
|
||||
a = [1, 2, 3]
|
||||
A = [1, 2, 3]
|
||||
B = a
|
||||
C = A
|
||||
b = B
|
||||
|
||||
if condition
|
||||
c = b
|
||||
end
|
||||
c # not recognised
|
||||
|
||||
4
ruby/ql/test/library-tests/ast/misc/iso-8859-15.rb
Normal file
4
ruby/ql/test/library-tests/ast/misc/iso-8859-15.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
#! /usr/bin/ruby
|
||||
# coding: iso-8859-15
|
||||
|
||||
print "EUR = <20>"
|
||||
@@ -0,0 +1,22 @@
|
||||
WARNING: Type BarrierGuard has been deprecated and may be removed in future (barrier-guards.ql:8,3-15)
|
||||
oldStyleBarrierGuards
|
||||
| barrier-guards.rb:3:4:3:15 | ... == ... | barrier-guards.rb:4:5:4:7 | foo | barrier-guards.rb:3:4:3:6 | foo | true |
|
||||
| barrier-guards.rb:9:4:9:24 | call to include? | barrier-guards.rb:10:5:10:7 | foo | barrier-guards.rb:9:21:9:23 | foo | true |
|
||||
| barrier-guards.rb:15:4:15:15 | ... != ... | barrier-guards.rb:18:5:18:7 | foo | barrier-guards.rb:15:4:15:6 | foo | false |
|
||||
| barrier-guards.rb:21:8:21:19 | ... == ... | barrier-guards.rb:24:5:24:7 | foo | barrier-guards.rb:21:8:21:10 | foo | true |
|
||||
| barrier-guards.rb:27:8:27:19 | ... != ... | barrier-guards.rb:28:5:28:7 | foo | barrier-guards.rb:27:8:27:10 | foo | false |
|
||||
| barrier-guards.rb:37:4:37:20 | call to include? | barrier-guards.rb:38:5:38:7 | foo | barrier-guards.rb:37:17:37:19 | foo | true |
|
||||
| barrier-guards.rb:43:4:43:15 | ... == ... | barrier-guards.rb:45:9:45:11 | foo | barrier-guards.rb:43:4:43:6 | foo | true |
|
||||
| barrier-guards.rb:70:4:70:21 | call to include? | barrier-guards.rb:71:5:71:7 | foo | barrier-guards.rb:70:18:70:20 | foo | true |
|
||||
| barrier-guards.rb:82:4:82:25 | ... != ... | barrier-guards.rb:83:5:83:7 | foo | barrier-guards.rb:82:15:82:17 | foo | true |
|
||||
newStyleBarrierGuards
|
||||
| barrier-guards.rb:4:5:4:7 | foo |
|
||||
| barrier-guards.rb:10:5:10:7 | foo |
|
||||
| barrier-guards.rb:18:5:18:7 | foo |
|
||||
| barrier-guards.rb:24:5:24:7 | foo |
|
||||
| barrier-guards.rb:28:5:28:7 | foo |
|
||||
| barrier-guards.rb:38:5:38:7 | foo |
|
||||
| barrier-guards.rb:45:9:45:11 | foo |
|
||||
| barrier-guards.rb:71:5:71:7 | foo |
|
||||
| barrier-guards.rb:83:5:83:7 | foo |
|
||||
| barrier-guards.rb:91:5:91:7 | foo |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user