Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use new Codecov uploader #392

Merged
merged 3 commits into from Jul 19, 2021
Merged

feat: Use new Codecov uploader #392

merged 3 commits into from Jul 19, 2021

Conversation

thomasrockhu
Copy link
Contributor

@thomasrockhu thomasrockhu commented Jun 24, 2021

Due to the deprecation of the bash uploader, this PR pushes the Action to v2 with the new uploader.

src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
@RA80533
Copy link
Contributor

RA80533 commented Jul 7, 2021

Is this PR intended to consume http://github.com/codecov/uploader?

src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
@thomasrockhu
Copy link
Contributor Author

@RA80533 if you have any suggestions on what is actually going wrong here with running exec.exec(...) on the file, it would be greatly appreciated

package.json Outdated Show resolved Hide resolved
@RA80533
Copy link
Contributor

RA80533 commented Jul 8, 2021

Was there a change made to codecov-linux some time today? It appears that the error originates from it.

src/index.ts Outdated Show resolved Hide resolved
@codecov
Copy link

codecov bot commented Jul 8, 2021

Codecov Report

Merging #392 (635d4e8) into master (ca5f3da) will increase coverage by 0.93%.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #392      +/-   ##
==========================================
+ Coverage   96.45%   97.39%   +0.93%     
==========================================
  Files           3        3              
  Lines         141      115      -26     
  Branches       43       34       -9     
==========================================
- Hits          136      112      -24     
+ Misses          5        3       -2     
Flag Coverage Δ
demo 87.50% <ø> (ø)
macos-latest 97.39% <100.00%> (?)
script 98.98% <100.00%> (+1.38%) ⬆️
ubuntu-latest 97.39% <100.00%> (?)
windows-latest 97.39% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
src/buildExec.ts 98.98% <100.00%> (+1.38%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update ca5f3da...635d4e8. Read the comment docs.

@RA80533
Copy link
Contributor

RA80533 commented Jul 8, 2021

It might be helpful to try to migrate this script into a local clone of codecov/uploader to run directly against its source. You might end up with an exact line number for the syntax error compared to what appears to be one produced from the minified source code. You could translate the TypeScript directly to a JavaScript file to run it directly with Node.js or use the ts-node package to run transpile and run the TypeScript directly.

Additionally, since GitHub Actions runs this in its own containerized environment wherein application dependencies such as Node.js are available, it would be possible to leverage the uploader project directly in JavaScript rather than through the binary it produces. Both projects are written with the same language and platform technologies so one would be able to consume code from the other (pre-build, of course) through package publication mechanisms available from the npm registry or Github Packages, etc.

@RA80533
Copy link
Contributor

RA80533 commented Jul 8, 2021

Remember to install both the current project dependencies and those of the now-nested one. It appears that the installation process requires there to be a Git repository in its folder which causes issues for installation (due to the requirement of having Husky hooks, etc.). The following change allowed me to install the project without a hitch:

diff --git a/uploader/package.json b/uploader/package.json
index 8ce71cb..14661a8 100644
--- a/uploader/package.json
+++ b/uploader/package.json
@@ -10,8 +10,7 @@
     "build-linux": "pkg . --targets linux --output out/codecov-linux",
     "build-macos": "pkg . --targets macos --output out/codecov-macos",
     "build-alpine": "pkg . --targets node14-alpine-x64 --output out/codecov-alpine",
-    "build-windows": "pkg . --targets win --output out/codecov.exe",
-    "prepare": "husky install"
+    "build-windows": "pkg . --targets win --output out/codecov.exe"
   },
   "repository": {
     "type": "git",

In other words, simply remove the prepare script from package.json.

@RA80533
Copy link
Contributor

RA80533 commented Jul 8, 2021

I enabled the sourceMap compiler option of TypeScript by making the following modification to tsconfig.json (related #413):

diff --git a/tsconfig.json b/tsconfig.json
index dbe3a5a..fa348fc 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,4 +1,7 @@
 {
+  "compilerOptions": {
+    "sourceMap": true
+  },
   "include": [
     "src"
   ]

@RA80533
Copy link
Contributor

RA80533 commented Jul 8, 2021

Based on the workflow file used in the GitHub Actions runner, main.yml:

$ INPUT_FILES='./coverage/calculator/coverage-final.json,./coverage/coverage-test/coverage-final.json' \
> INPUT_FILE='./coverage/coverage-final.json' \
> INPUT_FLAGS='demo' \
> INPUT_NAME='codecov-demo' \
> node --enable-source-maps dist/index.js
TypeError: Cannot read property 'url' of undefined
    at Object.main (/[…]/codecov-action/dist/index.js:25391:55)
    at /[…]/codecov-action/dist/index.js:25814:14
    at /[…]/codecov-action/dist/index.js:25821:3
    at Object.<anonymous> (/[…]/codecov-action/dist/index.js:25824:12)
    at Module._compile (node:internal/modules/cjs/loader:1095:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1124:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:816:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
    at node:internal/main/run_main_module:17:47

Interestingly, it appears that disabling Source Map v3 support (e.g., not passing the --enable-source-maps flag) gave better information as to where the issues are happening:

$ INPUT_FILES='./coverage/calculator/coverage-final.json,./coverage/coverage-test/coverage-final.json' \
> INPUT_FILE='./coverage/coverage-final.json' \
> INPUT_FLAGS='demo' \
> INPUT_NAME='codecov-demo' \
> node dist/index.js
/[…]/codecov-action/dist/index.js:25391
  const uploadHost = validateHelpers.validateURL(args.url)
                                                      ^

TypeError: Cannot read property 'url' of undefined
    at Object.main (/[…]/codecov-action/dist/index.js:25391:55)
    at /[…]/codecov-action/dist/index.js:25814:14
    at /[…]/codecov-action/dist/index.js:25821:3
    at Object.<anonymous> (/[…]/codecov-action/dist/index.js:25824:12)
    at Module._compile (node:internal/modules/cjs/loader:1095:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1124:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:816:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
    at node:internal/main/run_main_module:17:47

@thomasrockhu
Copy link
Contributor Author

@RA80533 we're getting closer 🙏

@RA80533
Copy link
Contributor

RA80533 commented Jul 8, 2021

diff --git a/tsconfig.json b/tsconfig.json
index dbe3a5a..99ea5db 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,13 @@
 {
+  "compilerOptions": {
+    "outDir": "./dist",
+    "rootDir": "./src",
+    "sourceMap": true
+  },
   "include": [
-    "src"
+    "src/**/*.ts"
+  ],
+  "exclude": [
+    "src/**/*.test.ts"
   ]
 }
  • Build the project with npx -p typescript tsc --build rather than npm run build at the root directory
  • Ensure there are coverage reports available by running npm run test

With --enables-source-maps

$ INPUT_FILES='./coverage/calculator/coverage-final.json,./coverage/coverage-test/coverage-final.json' \
> INPUT_FILE='./coverage/coverage-final.json' \
> INPUT_FLAGS='demo' \
> INPUT_NAME='codecov-demo' \
> node --enable-source-maps dist/index.js
TypeError: Cannot read property 'url' of undefined
    at Object.main (/[…]/codecov-action/uploader/src/index.js:68:55)
    at Object.<anonymous> (/[…]/codecov-action/src/index.ts:11:12)
    at Module._compile (node:internal/modules/cjs/loader:1095:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1124:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:816:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
    at node:internal/main/run_main_module:17:47
    at Object.<anonymous> (/[…]/codecov-action/src/index.ts:11:12)

Without

$ INPUT_FILES='./coverage/calculator/coverage-final.json,./coverage/coverage-test/coverage-final.json' \
> INPUT_FILE='./coverage/coverage-final.json' \
> INPUT_FLAGS='demo' \
> INPUT_NAME='codecov-demo' \
> node dist/index.js
/[…]/codecov-action/uploader/src/index.js:68
  const uploadHost = validateHelpers.validateURL(args.url)
                                                      ^

TypeError: Cannot read property 'url' of undefined
    at Object.main (/[…]/codecov-action/uploader/src/index.js:68:55)
    at Object.<anonymous> (/[…]/codecov-action/dist/index.js:7:14)
    at Module._compile (node:internal/modules/cjs/loader:1095:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1124:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:816:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
    at node:internal/main/run_main_module:17:47
/[…]/codecov-action/uploader/src/index.js:68
  const uploadHost = validateHelpers.validateURL(args.url)
                                                      ^

@drazisil
Copy link
Contributor

drazisil commented Jul 9, 2021

I'm confused. Why are we embedding the uploader? Doesn't that nullify all the binary security?

@thomasrockhu
Copy link
Contributor Author

@drazisil I'm not sure so correct me if I'm wrong, but from an engineering perspective, I don't think we need all the indirection. We get the advantage of having node environments because this is GHA

@RA80533
Copy link
Contributor

RA80533 commented Jul 9, 2021

There was a "this error exists on line 1 because everything is on line 1" error due everything being packed into the binary. Importing it allowed the error to become more apparent due to the additional context available from the source code.

@RA80533
Copy link
Contributor

RA80533 commented Jul 9, 2021

Doesn't that nullify all the binary security?

I might be able to shed some light on this perspective. Are there any other mechanisms in place aside from the checksums?

@drazisil
Copy link
Contributor

drazisil commented Jul 9, 2021

Doesn't that nullify all the binary security?

I might be able to shed some light on this perspective. Are there any other mechanisms in place aside from the checksums?

No, but part of the reason for making it a binary was so that folks couldn't use it as a package and thus possibly bypass the validation by calling upload directly, for example. In that sense, being a binary is part of the security, in the sense it's much harder to tamper with or use incorrectly.

Also if it's bundled like this it's not current and we now have multiple versions running around.

@drazisil
Copy link
Contributor

drazisil commented Jul 9, 2021

@drazisil I'm not sure so correct me if I'm wrong, but from an engineering perspective, I don't think we need all the indirection. We get the advantage of having node environments because this is GHA

You get a version that becomes out of date as soon as we update the uploader.

@drazisil
Copy link
Contributor

drazisil commented Jul 9, 2021

I've invested too much time and pain to get it working as a binary to objectively be ok with bypassing that because "node is easier". If product is ok with this approach, then I'm fine. 🤷‍♀️😁

@RA80533
Copy link
Contributor

RA80533 commented Jul 9, 2021

You get a version that becomes out of date as soon as we update the uploader.

Yes, with the way the uploader is currently embedded, it will immediately go out of date and require repository-level maintenance in order to bring it up to date. The problem its current embedding attempts to solve is somewhat of a classical problem in software architecture. Fortunately, it can be solved by approaching the problem a bit differently with the context of the technologies in use:

  1. Add the repository as a Git module
    Any changes made to the child repository are tracked by the parent repository in a manner similar to that of a symbolic link on a Unix file system.

  2. Publish the child repository in a package registry (public or private)
    This approach is native to Node.js thanks to npm. A conventional implementation of this would most likely take the form of the source JavaScript being published rather than the binary.

The binary itself should not be thought of as a security measure. Although it is a very common practice (it's even got a name: "security through obscurity"), it is somewhat misleading because it does not prevent the mechanisms through which exploitation would be possible. The practice has legitimate application when used alongside proven security measures, however, and can be quite effective with dealing with automated scanning and the like.

The binary itself has tremendous value in its portability. The Bash uploader had a handful of dependencies outside of its control that no doubt made it tricky to work with in certain environments. Those requirements are nowhere to be found in the new uploader.

@drazisil
Copy link
Contributor

drazisil commented Jul 9, 2021

practice (it's even got a name: "security through obscurity"), it is somewhat mislead

I respectfully disagree. This is not security though obscurity, in the sense that it's an obficated script. This is a true binary, and as such I believe it blocks quite a few ways to tamper with it that are possible with a node script.

@thomasrockhu
Copy link
Contributor Author

thomasrockhu commented Jul 9, 2021

Thanks for the discussion here. I think I'd rather us be more security conscious and go with the older version of this PR (downloading, validating, and running the binary). If it ends up being unnecessary, we can make that call later. But I think it would be worse to do it the other way around.

Unless of course, this is the WRONG way to publish this action securely

@RA80533
Copy link
Contributor

RA80533 commented Jul 9, 2021

I believe it blocks quite a few ways to tamper with it that are possible with a node script.

Simply for clarification, are you referring to a scenario in which an attacker modifies an executable that is later run by an unknowing party or the attacker themselves?

Thanks for the discussion here. I think I'd rather us be more security conscious and go with the older version of this PR (downloading, validating, and running the binary).

Be aware that the earlier version of the PR was susceptible to the same attack vector as the issue from earlier this year. One improvement upon which the process you described can take on is for it to download the binary from a place where changes made to it are transparently available for users to see.

@RA80533
Copy link
Contributor

RA80533 commented Jul 9, 2021

This is a true binary, and as such I believe it blocks quite a few ways to tamper with it that are possible with a node script.

This is, somewhat surprisingly, not too difficult to accomplish.


  1. Get the most recent tag
    The end of the first line is an escape character for a newline. This allows the user to visually separate a long string of commands into multiple lines.

    $ git ls-remote --tags --sort='version:refname' https://github.com/codecov/uploader.git | \
    > awk -F / 'END { print $3 }'
    v0.1.0_5313
  2. Download the latest release
    The destination URL is based on the filename of the binary which differs depending on its target platform. The available binaries are codecov-alpine, codecov-linux, codecov-macos, and codecov.exe (Windows).

    $ wget -q https://github.com/codecov/uploader/releases/download/:tag/:filename
  3. Get a hex dump of the binary

    $ hexdump -C codecov-macos > dump.txt

    At this point, hex-encoded strings embedded within the binary can be easily located:

    $ cat dump.txt | grep 'https://code'
    021aaab0  00 00 68 74 74 70 73 3a  2f 2f 63 6f 64 65 63 6f  |..https://codeco|
    $ cat dump.txt | grep '021aaac0'
    021aaac0  76 2e 69 6f 00 00 00 00  00 00 01 0c 51 61 16 e2  |v.io........Qa..|

    There's enough context in the region to understand the purpose of the string:

    $ hexdump -C -n 96 -s 0x21aaa50 codecov-macos
    021aaa50  75 72 6c 00 00 00 00 00  01 24 92 61 00 00 00 00  |url......$.a....|
    021aaa60  07 00 00 00 00 00 00 00  08 00 00 00 90 07 89 06  |................|
    021aaa70  94 01 1c 51 65 86 34 7f  ee 27 00 00 00 43 68 61  |...Qe.4..'...Cha|
    021aaa80  6e 67 65 20 74 68 65 20  75 70 6c 6f 61 64 20 68  |nge the upload h|
    021aaa90  6f 73 74 20 28 45 6e 74  65 72 70 72 69 73 65 20  |ost (Enterprise |
    021aaaa0  75 73 65 29 00 96 01 14  51 63 8a 2b cd 93 12 00  |use)....Qc.+....|
    021aaab0
     url: {
        type: "string",
        description: "Change the upload host (Enterprise use)",
        default: "https://codecov.io"
      },

    uploader/bin/codecov#L84-L88

@thomasrockhu
Copy link
Contributor Author

Fixing the uploader via codecov/uploader#200

jest.config.js Outdated
Comment on lines 4 to 6
testPathIgnorePatterns: [
'/node_modules/',
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jest should ignore this by default.

.github/workflows/main.yml Outdated Show resolved Hide resolved
@RA80533
Copy link
Contributor

RA80533 commented Jul 14, 2021

Even though this is written in TypeScript, there may be difficulty in obtaining type definitions during development due to a specific caveat with Node.js: require() is not typed with respect to its run-time capabilities which means that, as a result, TypeScript is unable to deduce the types that would otherwise be available.

If you're using an editor with first-class TypeScript capabilities such as VS Code, you can leverage editor hinting for the types you're interacting with by making a subtle change to module imports:

- const fs = require('fs');
+ import * as fs from 'fs';

With the enablement of the esModuleInterop TypeScript compiler option, this change is a one-size-fits-all (with the exception of modules with default exports).

tsconfig.json Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated
// TODO - validate step

fs.chmodSync(filename, '777');
exec.exec(filename, execArgs, options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be awaited via the await keyword. Two things to note:

  • The enclosing function should be made async on line 44
  • Lines 61 to 70 should be moved before line 51 to prevent a TypeError from being thrown
Suggested change
exec.exec(filename, execArgs, options)
await exec.exec(filename, execArgs, options)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, makes sense. For my future reference, how did you realize this was an issue?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question.

I was able to discover this due to looking at the imports and realizing two were still brought in via require() instead of import. This led me to understand that the type definitions of those modules were not brought in and available to the developer. Once they were, it became directly apparent that exec() was typed to return a Promise<number>.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unfortunate side effect when dealing with promises is that it's quite easy for them to "dangle" if their settlement is never handled. TypeScript does not alert for this but Node.js will emit an error message indicating an UnhandledRejection has occurred if a dangling promise is rejected / throws an error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a TSLint rule (no-floating-promises) that checks for this behavior. ESLint does not have a built-in rule that corresponds with this behavior although there are a few from the community (e.g., the one from the typescript-eslint project) that handles it well.

src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/helpers.ts Outdated Show resolved Hide resolved
@thomasrockhu thomasrockhu requested a review from a team July 15, 2021 19:29
@thomasrockhu thomasrockhu marked this pull request as ready for review July 15, 2021 19:33
CHANGELOG.md Outdated Show resolved Hide resolved
README.md Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
@thomasrockhu
Copy link
Contributor Author

Since there are commits on this PR that are not signed, I will need to do a rebase which will be destructive to this PR

@thomasrockhu
Copy link
Contributor Author

Sorry all, this PR has gotten a little out of hand, I'm going to squash it down to the original commit before the rebase

dependabot bot and others added 3 commits July 16, 2021 21:53
Bumps [eslint](https://github.com/eslint/eslint) from 7.27.0 to 7.28.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](eslint/eslint@v7.27.0...v7.28.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@briantist
Copy link

@thomasrockhu #423

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants