diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5233243 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + semgrep: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Semgrep + run: pip install semgrep + + - name: Run Semgrep bracket-assign guard + run: | + set -e + semgrep --config .semgrep/rules/no-bracket-assign-hot-paths.yml \ + --strict --json \ + src/providers/ src/parser.ts > semgrep-out.json + FINDINGS=$(jq '.results | length' semgrep-out.json) + if [ "$FINDINGS" -gt 0 ]; then + jq -r '.results[] | "::error file=\(.path),line=\(.start.line)::\(.extra.message)"' semgrep-out.json + exit 1 + fi diff --git a/.semgrep/rules/no-bracket-assign-hot-paths.yml b/.semgrep/rules/no-bracket-assign-hot-paths.yml new file mode 100644 index 0000000..e2e633d --- /dev/null +++ b/.semgrep/rules/no-bracket-assign-hot-paths.yml @@ -0,0 +1,22 @@ +rules: + - id: no-bracket-assign-on-literal-object-map + languages: [typescript] + severity: ERROR + message: > + Bracket-assign on a map created with `{}` allows prototype pollution when + the key comes from external data. Initialize the map with + `Object.create(null)` instead. + patterns: + - pattern-either: + - pattern: $MAP[$KEY] = $MAP[$KEY] ?? $INIT + - pattern: $MAP[$KEY] = $MAP[$KEY] || $INIT + - pattern: $MAP[$KEY] ??= $INIT + - pattern: | + if (!$MAP[$KEY]) $MAP[$KEY] = $INIT + - pattern-not-inside: | + const $MAP = Object.create(null) + ... + paths: + include: + - '/src/providers/*.ts' + - '/src/parser.ts' diff --git a/src/parser.ts b/src/parser.ts index 4adcf3b..ba0d31c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -173,7 +173,7 @@ function buildSessionSummary( const toolBreakdown: SessionSummary['toolBreakdown'] = Object.create(null) const mcpBreakdown: SessionSummary['mcpBreakdown'] = Object.create(null) const bashBreakdown: SessionSummary['bashBreakdown'] = Object.create(null) - const categoryBreakdown: SessionSummary['categoryBreakdown'] = {} as SessionSummary['categoryBreakdown'] + const categoryBreakdown: SessionSummary['categoryBreakdown'] = Object.create(null) let totalCost = 0 let totalInput = 0