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

merge dev into main #40

Merged
merged 32 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6f6211b
Pin deps in requirements.txt
TheTeaCat Sep 20, 2023
804f6e0
pin jsonschema
TheTeaCat Sep 20, 2023
c545949
Merge pull request #35 from FireTail-io/pin-deps
rileyfiretail Sep 20, 2023
a93333c
Fix skip public, archived & fork repos being ignored in config
TheTeaCat Sep 28, 2023
cc97351
Fix repos not being scanned unless listed in repos list of config
TheTeaCat Sep 28, 2023
21f4a21
Fix missing skip private repos flag on orgs
TheTeaCat Sep 28, 2023
afe95c0
Fix get_file_contents returning wrong type
TheTeaCat Sep 28, 2023
8d97a6b
Remove poc skipped directories
TheTeaCat Sep 28, 2023
7a3f082
Fix post failure when spec contains datetimes
TheTeaCat Sep 28, 2023
0c87120
Fix type hints
TheTeaCat Sep 28, 2023
a3072de
Remove dead code
TheTeaCat Sep 28, 2023
1b33ec0
Always process appspecs as yaml
TheTeaCat Sep 28, 2023
1629270
Fix language names
TheTeaCat Sep 28, 2023
88beb9d
Format & type ignores
TheTeaCat Sep 28, 2023
3f3f153
Cache func to get file contents
TheTeaCat Sep 28, 2023
5ac27c9
Refactor analysers to take callable for file content
TheTeaCat Sep 28, 2023
091182e
Fix wait incorrect wait duration when rate limited
TheTeaCat Sep 28, 2023
e3110d5
Modify log on rate limit
TheTeaCat Sep 28, 2023
118d538
Update tests to use callables
TheTeaCat Sep 28, 2023
574877a
Fix return of incorrect type
TheTeaCat Sep 28, 2023
66beae8
Fix golang analysis creating invalid appspecs
TheTeaCat Sep 28, 2023
e8e2424
Add extra second to rate limit wait period
TheTeaCat Sep 28, 2023
75a7245
Add quickstart to README
TheTeaCat Sep 28, 2023
f225f55
Simplify github imports
TheTeaCat Sep 29, 2023
9034f21
Minor refactor for tests
TheTeaCat Sep 29, 2023
b60346c
Add tests for config usage
TheTeaCat Sep 29, 2023
482106c
Remove dead code from config
TheTeaCat Sep 29, 2023
28c3df2
Use patches in test_get_repos_to_scan_without_config
TheTeaCat Oct 2, 2023
1c62285
Add a test for scan_repositories
TheTeaCat Oct 2, 2023
96320a9
Merge pull request #36 from FireTail-io/main
TheTeaCat Oct 2, 2023
086f6ee
Add test-requirements install to test-lambda stage
TheTeaCat Oct 2, 2023
398103a
Merge pull request #38 from FireTail-io/test-config
rileyfiretail Oct 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,38 @@ This Docker image will discover APIs in your GitHub account by scanning for open



## Building
## Quickstart

You can build the image yourself by cloning the repository and using the following docker command:
First, clone this repo and build the scanner's image:

```bash
git clone git@github.com:FireTail-io/github-api-discovery.git
cd github-api-discovery
docker build --rm -t firetail-io/github-api-discovery:latest -f build_setup/Dockerfile . --target runtime
```

Make a copy of the provided [config-example.yml](./config-example.yml) and call it `config.yml`, then edit it for your use case.

```bash
cp config-example.yml config.yml
open config.yml
```

Running the image requires two environment variables to be set:

- `GITHUB_TOKEN`, [a classic GitHub personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic).
- `FIRETAIL_APP_TOKEN`, [a FireTail app token](https://www.firetail.io/docs/create-app-token).

Find a full list of environment variables under [Environment Variables](#environment-variables).

Once you have created a classic GitHub personal access token and a FireTail app token, you can run the scanner image:

```bash
export GITHUB_TOKEN=YOUR_GITHUB_TOKEN
export FIRETAIL_APP_TOKEN=YOUR_FIRETAIL_APP_TOKEN
docker run --rm -e GITHUB_TOKEN=${GITHUB_TOKEN} -e FIRETAIL_APP_TOKEN=${FIRETAIL_APP_TOKEN} --mount type=bind,source="$(pwd)"/config.yml,target=/config.yml,readonly firetail-io/github-api-discovery:latest
```



## Tests
Expand All @@ -33,19 +55,7 @@ docker run --rm --entrypoint cat firetail-io/github-api-discovery:test-golang co



## Running

Running the image requires two environment variables, `GITHUB_TOKEN` and `FIRETAIL_APP_TOKEN`. You can find a full list of environment variables used by the scanner below.

The scanner also requires a config file to determine the organisations, users and repositories to scan. You can find an example at [config-example.yml](./config-example.yml).

Copy [config-example.yml](./config-example.yml) to `config.yml` and adjust it to your use case, then run the image using the following docker command:

```bash
export GITHUB_TOKEN=YOUR_GITHUB_TOKEN
export FIRETAIL_APP_TOKEN=YOUR_FIRETAIL_APP_TOKEN
docker run --rm -e GITHUB_TOKEN=${GITHUB_TOKEN} -e FIRETAIL_APP_TOKEN=${FIRETAIL_APP_TOKEN} --mount type=bind,source="$(pwd)"/config.yml,target=/config.yml,readonly firetail-io/github-api-discovery:latest
```
## Environment Variables

| Variable Name | Description | Required? | Default |
| -------------------- | ------------------------------------------------------------ | --------- | ------------------------------------------------ |
Expand Down
22 changes: 16 additions & 6 deletions analysers/golang/analyse.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func analyse(filePath string, fileContents string) (map[string]string, map[strin
imports := map[string]string{}
for _, parsedImport := range parsedFile.Imports {
// parsedImport.Path.Value includes quotes on either end (e.g. "net/http"); these indexes strip them.
importPath := parsedImport.Path.Value[1:len(parsedImport.Path.Value)-1]
importPath := parsedImport.Path.Value[1 : len(parsedImport.Path.Value)-1]
if parsedImport.Name != nil {
imports[importPath] = parsedImport.Name.Name
} else {
Expand All @@ -51,23 +51,33 @@ func analyse(filePath string, fileContents string) (map[string]string, map[strin
if packageIdentifier, ok := importedSupportedFrameworks["net/http"]; ok {
netHttpPathsSlice := analyseNetHTTP(parsedFile, packageIdentifier)

netHttpPathsMap := map[string]map[string]map[string]map[string]string{}
netHttpPathsMap := map[string]map[string]map[string]map[string]map[string]string{}
for _, path := range netHttpPathsSlice {
netHttpPathsMap[path] = map[string]map[string]map[string]string{
responses := map[string]map[string]map[string]string{
"responses": {
"default": {
"description": "Discovered via static analysis",
},
},
}
netHttpPathsMap[path] = map[string]map[string]map[string]map[string]string{
"get": responses,
"post": responses,
"put": responses,
"patch": responses,
"delete": responses,
"head": responses,
"options": responses,
"trace": responses,
}
}

// Only make an appspec if there's at least one path detected
if len(netHttpPathsMap) > 0 {
openapiSpecs["static-analysis:net/http:" + filePath] = map[string]interface{}{
openapiSpecs["static-analysis:net/http:"+filePath] = map[string]interface{}{
"openapi": "3.0.0",
"info": map[string]string{"title": "Static Analysis - Golang net/http"},
"paths": netHttpPathsMap,
"info": map[string]string{"title": "Static Analysis - Golang net/http"},
"paths": netHttpPathsMap,
}
}
}
Expand Down
56 changes: 39 additions & 17 deletions analysers/golang/analyse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,28 @@ func main() {
imports, openapiSpecs, err := analyse(fileName, fileContents)
assert.Nil(t, err)
assert.Equal(t, map[string]string{"net/http": "http"}, imports)

responses := map[string]map[string]map[string]string{
"responses": {
"default": {
"description": "Discovered via static analysis",
},
},
}
assert.Equal(t, map[string]interface{}{
"static-analysis:net/http:net_http_hello_world.go": map[string]interface{}{
"static-analysis:net/http:net_http_hello_world.go": map[string]interface{}{
"openapi": "3.0.0",
"info": map[string]string{"title": "Static Analysis - Golang net/http"},
"paths": map[string]map[string]map[string]map[string]string{
"info": map[string]string{"title": "Static Analysis - Golang net/http"},
"paths": map[string]map[string]map[string]map[string]map[string]string{
"/hello": {
"responses": {
"default": {
"description": "Discovered via static analysis",
},
},
"get": responses,
"post": responses,
"put": responses,
"patch": responses,
"delete": responses,
"head": responses,
"options": responses,
"trace": responses,
}},
},
}, openapiSpecs)
Expand All @@ -64,17 +75,28 @@ func main() {
imports, openapiSpecs, err := analyse(fileName, fileContents)
assert.Nil(t, err)
assert.Equal(t, map[string]string{"net/http": "nethttp"}, imports)

responses := map[string]map[string]map[string]string{
"responses": {
"default": {
"description": "Discovered via static analysis",
},
},
}
assert.Equal(t, map[string]interface{}{
"static-analysis:net/http:net_http_hello_world.go": map[string]interface{}{
"static-analysis:net/http:net_http_hello_world.go": map[string]interface{}{
"openapi": "3.0.0",
"info": map[string]string{"title": "Static Analysis - Golang net/http"},
"paths": map[string]map[string]map[string]map[string]string{
"info": map[string]string{"title": "Static Analysis - Golang net/http"},
"paths": map[string]map[string]map[string]map[string]map[string]string{
"/hello": {
"responses": {
"default": {
"description": "Discovered via static analysis",
},
},
"get": responses,
"post": responses,
"put": responses,
"patch": responses,
"delete": responses,
"head": responses,
"options": responses,
"trace": responses,
},
},
},
Expand All @@ -90,4 +112,4 @@ func TestAnalyseGolangNotGoFile(t *testing.T) {
assert.Equal(t, "net_http_hello_world.go:1:1: expected 'package', found '{'", err.Error())
assert.Equal(t, map[string]string{}, imports)
assert.Equal(t, map[string]interface{}{}, openapiSpecs)
}
}
4 changes: 4 additions & 0 deletions build_setup/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ FROM build-python as test-python
WORKDIR /github-api-discovery
COPY setup.cfg /github-api-discovery/setup.cfg
RUN python3 -m pip install pytest pytest-cov
COPY tests/requirements.txt /github-api-discovery/tests/requirements.txt
RUN python3 -m pip install -r /github-api-discovery/tests/requirements.txt
COPY tests/ /github-api-discovery/tests/
RUN pytest --cov /github-api-discovery --cov-report=xml:coverage.xml -vv -x

Expand All @@ -47,6 +49,8 @@ RUN rm -rf /build_setup

FROM build-python-lambda as test-python-lambda
RUN python3 -m pip install pytest pytest-cov
COPY tests/requirements.txt /github-api-discovery/tests/requirements.txt
RUN python3 -m pip install -r /github-api-discovery/tests/requirements.txt
COPY tests/ ${LAMBDA_TASK_ROOT}/tests
RUN PYTHONPATH=${LAMBDA_TASK_ROOT} pytest --cov ${LAMBDA_TASK_ROOT} --cov-report=xml:coverage.xml -vv -x

Expand Down
13 changes: 7 additions & 6 deletions build_setup/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
PyGithub
openapi-spec-validator
pyyaml
prance
tree_sitter
dacite
openapi-spec-validator==0.6.0
prance==23.6.21.0
pyyaml==6.0.1
dacite==1.8.1
tree_sitter==0.20.2
PyGithub==1.59.1
jsonschema==4.19.0
7 changes: 4 additions & 3 deletions config-example.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# List organisations to scan their repositories
organisations: # default []
example-organisation:
# Under each org, you can skip public, internal, archived or fork repositories
# Under each org, you can skip public, private, internal, archived or fork repositories
skip_public_repositories: False # default False
skip_private_repositories: False # default False
skip_internal_repositories: False # default False
skip_archived_repositories: False # default False
skip_forks: False # default False
Expand All @@ -19,5 +20,5 @@ users: # default []
# List individual repositories to include or exclude them explicitly from scanning.
# Has higher prescedence than scanning via users or orgs.
repositories: # default []
example-user/example-repository: exclude # default "exclude"
example-organisation/example-repository: include # default "exclude"
example-user/example-repository: exclude
example-organisation/example-repository: include
26 changes: 10 additions & 16 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from github.Repository import Repository as GithubRepository


@dataclass
class AccountConfig(ABC):
skip_public_repositories: bool = False
skip_archived_repositories: bool = False
Expand All @@ -23,39 +24,39 @@ def skip_repo(self, repository: GithubRepository) -> bool:


@dataclass
class OrgConfig(AccountConfig):
skip_internal_repositories: bool = False
class UserConfig(AccountConfig):
skip_private_repositories: bool = False

def skip_repo(self, repository: GithubRepository) -> bool:
super_skip = super().skip_repo(repository)
if super_skip:
return True

if self.skip_internal_repositories and repository.visibility == "internal":
if self.skip_private_repositories and repository.visibility == "private":
return True

return False


@dataclass
class UserConfig(AccountConfig):
skip_private_repositories: bool = False
class OrgConfig(UserConfig):
skip_internal_repositories: bool = False

def skip_repo(self, repository: GithubRepository) -> bool:
super_skip = super().skip_repo(repository)
if super_skip:
return True

if self.skip_private_repositories and repository.visibility == "private":
if self.skip_internal_repositories and repository.visibility == "internal":
return True

return False


@dataclass
class Config:
organisations: dict[str, OrgConfig | None] | list[str] | None = field(default_factory=dict[str, OrgConfig])
users: dict[str, UserConfig | None] | list[str] | None = field(default_factory=dict[str, UserConfig])
organisations: dict[str, OrgConfig | None] | list[str] | None = field(default_factory=dict[str, OrgConfig | None])
users: dict[str, UserConfig | None] | list[str] | None = field(default_factory=dict[str, UserConfig | None])
repositories: dict[str, str] | None = field(default_factory=dict)

def __post_init__(self):
Expand All @@ -66,18 +67,11 @@ def __post_init__(self):
}
elif type(self.organisations) == list:
self.organisations = {organisation: OrgConfig() for organisation in self.organisations}
elif self.organisations is None:
self.organisations = {}

if type(self.users) == dict:
self.users = {user: config if config is not None else UserConfig() for user, config in self.users.items()}
elif type(self.users) == list:
self.users = {user: UserConfig() for user in self.users}
elif self.users is None:
self.users = {}

if self.repositories is None:
self.repositories = {}

def skip_repo(self, repository: GithubRepository) -> bool:
return self.repositories.get(repository.full_name) != "include"
return self.repositories.get(repository.full_name) == "exclude"
20 changes: 0 additions & 20 deletions src/credentials/auth_token.py

This file was deleted.

6 changes: 3 additions & 3 deletions src/openapi/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ def parse_resolve_and_validate_openapi_spec(file_path: str, get_file_contents: C
# First check it's a valid JSON/YAML file before passing it over to Prance
if file_path.endswith(".json"):
try:
json.loads(get_file_contents())
file_contents = json.loads(get_file_contents())
except: # noqa: E722
return None

elif file_path.endswith((".yaml", ".yml")):
try:
yaml.safe_load(get_file_contents())
file_contents = yaml.safe_load(get_file_contents())
except: # noqa: E722
return None

else:
return None

# If it was a valid JSON/YAML file, we can give it to Prance to load
return resolve_and_validate_openapi_spec(get_file_contents())
return resolve_and_validate_openapi_spec(yaml.dump(file_contents))
Loading