├── lib ├── typings │ └── .gitignore ├── aspect │ ├── community │ │ ├── ContentClassifier.ts │ │ ├── oss.ts │ │ ├── codeOfConduct.ts │ │ └── license.ts │ ├── git │ │ ├── dateUtils.ts │ │ ├── gitIgnore.ts │ │ └── branchCount.ts │ ├── compose │ │ ├── commonTypes.ts │ │ ├── microgrammarMatchAspect.ts │ │ ├── conditionalize.ts │ │ ├── matchAspect.ts │ │ └── fileMatchAspect.ts │ ├── reporters.ts │ ├── delivery │ │ ├── DeliveryAspect.ts │ │ ├── support │ │ │ └── goalListener.ts │ │ ├── storeFingerprintsPublisher.ts │ │ └── BuildAspect.ts │ ├── common │ │ ├── globVirtualizer.ts │ │ ├── inspectionVirtualizer.ts │ │ ├── codeOwnership.ts │ │ ├── virtualProjectAspect.ts │ │ └── inspectionAspect.ts │ ├── secret │ │ ├── snifferOptionsLoader.ts │ │ ├── exposedSecrets.ts │ │ └── secretSniffing.ts │ └── AspectReportDetailsRegistry.ts ├── graphql │ ├── mutation │ │ └── ingestScmCommit.graphql │ └── query │ │ ├── gitHubAppInstallationByOwner.graphql │ │ ├── scmProviderbyId.graphql │ │ └── aspectRegistrations.graphql ├── util │ ├── omit.ts │ ├── fileUtils.ts │ ├── fingerprintUtils.ts │ ├── showTiming.ts │ ├── commonBands.ts │ └── bands.ts ├── analysis │ ├── offline │ │ ├── SpideredRepo.ts │ │ ├── spider │ │ │ ├── ScmSearchCriteria.ts │ │ │ ├── github │ │ │ │ └── GitCommandGitProjectCloner.ts │ │ │ ├── analytics.ts │ │ │ ├── Spider.ts │ │ │ └── local │ │ │ │ └── LocalSpider.ts │ │ └── persist │ │ │ ├── pgClientFactory.ts │ │ │ └── pgUtils.ts │ ├── ProjectAnalysisResult.ts │ └── tracking │ │ └── analysisTrackingRoutes.ts ├── routes │ ├── web-app │ │ ├── webAppConfig.ts │ │ ├── computeAnalytics.ts │ │ ├── repositoryListPage.ts │ │ └── overviewPage.ts │ ├── support │ │ ├── tagUtils.ts │ │ └── treeMunging.ts │ ├── projectQueries.ts │ └── auth.ts ├── scorer │ ├── support │ │ └── exposeFingerprintScore.ts │ ├── scorerUtils.ts │ ├── commonWorkspaceScorers.ts │ └── scoring.ts ├── machine │ ├── machine.ts │ └── configureAspects.ts ├── tree │ └── sunburst.ts └── job │ └── registerAspect.ts ├── public ├── git.png ├── reset.png ├── exclude.png ├── require.png ├── taggydoober.png ├── taggydoober-error.png ├── taggydoober-warning.png ├── atomist-logo-small-white.png └── hexagonal-fruit-of-power.png ├── integration-test ├── docker-entrypoint.sh ├── Dockerfile └── README.md ├── .gitattributes ├── images └── dockerImageSunburst.png ├── ddl ├── cleanup.sql ├── migrations │ └── add_fingerprint_path.ddl └── create.ddl ├── webpack.config.js ├── .gitignore ├── .npmignore ├── views ├── utils.tsx ├── topLevelPage.tsx ├── aspectTrackingPage.tsx └── repoList.tsx ├── tsconfig.json ├── test ├── aspect │ ├── common │ │ ├── virtualProjectAspect.test.ts │ │ └── globAspect.test.ts │ ├── codeOfConduct.test.ts │ ├── license.test.ts │ ├── dockerParser.test.ts │ ├── secretSniffing.test.ts │ └── fileMatchAspect.test.ts ├── routes │ ├── api.test.ts │ └── wep-app │ │ └── webAppRoutes.test.ts ├── spider │ └── analytics.test.ts ├── analysis │ └── offline │ │ └── spider │ │ └── SpiderAnalyzer.test.ts ├── util │ └── bands.test.ts └── tree │ └── pruneLeaves.test.ts ├── SECURITY.md ├── secrets.yml ├── index.ts ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md └── CONTRIBUTING.md /lib/typings/.gitignore: -------------------------------------------------------------------------------- 1 | types.ts 2 | -------------------------------------------------------------------------------- /public/git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomist/sdm-pack-aspect/HEAD/public/git.png -------------------------------------------------------------------------------- /public/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomist/sdm-pack-aspect/HEAD/public/reset.png -------------------------------------------------------------------------------- /public/exclude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomist/sdm-pack-aspect/HEAD/public/exclude.png -------------------------------------------------------------------------------- /public/require.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomist/sdm-pack-aspect/HEAD/public/require.png -------------------------------------------------------------------------------- /public/taggydoober.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomist/sdm-pack-aspect/HEAD/public/taggydoober.png -------------------------------------------------------------------------------- /integration-test/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | service postgresql start 4 | cd /app 5 | 6 | exec $* -------------------------------------------------------------------------------- /public/taggydoober-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomist/sdm-pack-aspect/HEAD/public/taggydoober-error.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | legal/THIRD_PARTY.md linguist-generated=true 2 | 3 | # Happy WSL development 4 | * text=auto eol=lf 5 | -------------------------------------------------------------------------------- /images/dockerImageSunburst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomist/sdm-pack-aspect/HEAD/images/dockerImageSunburst.png -------------------------------------------------------------------------------- /lib/aspect/community/ContentClassifier.ts: -------------------------------------------------------------------------------- 1 | 2 | export type ContentClassifier = (content: string) => string | undefined; 3 | -------------------------------------------------------------------------------- /public/taggydoober-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomist/sdm-pack-aspect/HEAD/public/taggydoober-warning.png -------------------------------------------------------------------------------- /public/atomist-logo-small-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomist/sdm-pack-aspect/HEAD/public/atomist-logo-small-white.png -------------------------------------------------------------------------------- /public/hexagonal-fruit-of-power.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atomist/sdm-pack-aspect/HEAD/public/hexagonal-fruit-of-power.png -------------------------------------------------------------------------------- /ddl/cleanup.sql: -------------------------------------------------------------------------------- 1 | -- Clean up the database 2 | delete from 3 | repo_fingerprints; 4 | delete from 5 | repo_snapshots; 6 | delete from 7 | fingerprints; 8 | delete from 9 | fingerprint_analytics; -------------------------------------------------------------------------------- /lib/graphql/mutation/ingestScmCommit.graphql: -------------------------------------------------------------------------------- 1 | mutation IngestScmCommit($providerId: String!, $commit: SCMCommitInput!) { 2 | ingestSCMCommit(scmProviderId: $providerId, scmCommitInput: $commit) { 3 | id 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/graphql/query/gitHubAppInstallationByOwner.graphql: -------------------------------------------------------------------------------- 1 | query GitHubAppInstallationByOwner($name: String!) { 2 | GitHubAppInstallation(owner: $name) { 3 | token { 4 | secret 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "development", 3 | entry: { 4 | sunburstScript: "./lib/page/sunburstScript.js" 5 | }, 6 | output: { 7 | filename: "[name]-bundle.js", 8 | library: "SunburstYo" 9 | } 10 | } -------------------------------------------------------------------------------- /lib/graphql/query/scmProviderbyId.graphql: -------------------------------------------------------------------------------- 1 | query ScmProviderById($providerId: String!) { 2 | SCMProvider(providerId: $providerId) { 3 | url 4 | apiUrl 5 | providerType 6 | id 7 | providerId 8 | credential { 9 | scopes 10 | secret 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | .vscode/ 4 | *~ 5 | .#* 6 | .npmrc 7 | node_modules/ 8 | *.d.ts 9 | *.d.ts.map 10 | *.js 11 | *.js.map 12 | *.log 13 | *.txt 14 | /.nyc_output/ 15 | /build/ 16 | /coverage/ 17 | /doc/ 18 | /log/ 19 | git-info.json 20 | spidered 21 | !d3.*.min.js 22 | !jquery-3.4.1.min.js 23 | !webpack.config.js 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | .vscode/ 4 | *~ 5 | .#* 6 | .dockerignore 7 | .git* 8 | .npmrc* 9 | .travis.yml 10 | .atomist/ 11 | .nyc_output/ 12 | /build/ 13 | /doc/ 14 | /config/ 15 | /coverage/ 16 | /log/ 17 | /scripts/ 18 | /src/test/ 19 | /test/ 20 | /CO*.md 21 | /Dockerfile 22 | /assets/kubectl/ 23 | *.log 24 | *.txt 25 | -------------------------------------------------------------------------------- /ddl/migrations/add_fingerprint_path.ddl: -------------------------------------------------------------------------------- 1 | 2 | alter table repo_snapshots drop column path; 3 | 4 | alter table repo_fingerprints add column path varchar; 5 | 6 | update repo_fingerprints set path = ''; 7 | 8 | alter table repo_fingerprints drop constraint repo_fingerprints_pkey; 9 | 10 | alter table repo_fingerprints ADD PRIMARY KEY (repo_snapshot_id, fingerprint_id, path); -------------------------------------------------------------------------------- /lib/graphql/query/aspectRegistrations.graphql: -------------------------------------------------------------------------------- 1 | query AspectRegistrations($state: [AspectRegistrationState], $name: [String]) { 2 | AspectRegistration(state: $state, name: $name, _first: 100) { 3 | name 4 | owner 5 | displayName 6 | description 7 | shortName 8 | unit 9 | url 10 | manageable 11 | category 12 | endpoint 13 | uuid 14 | state 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /views/utils.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export function collapsible(key: string, title: string, content: React.ReactElement, startOpen: boolean): React.ReactElement { 4 | return
5 | 6 | 7 |
8 |
9 | {content} 10 |
; 11 | } 12 | -------------------------------------------------------------------------------- /lib/util/omit.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Type T with the given properties removed 19 | */ 20 | export type Omit = Pick>; 21 | -------------------------------------------------------------------------------- /integration-test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | RUN apt update && apt install -y postgresql 4 | 5 | USER postgres 6 | 7 | RUN service postgresql start &&\ 8 | psql --command "CREATE USER root WITH SUPERUSER PASSWORD 'sebastian';" && createdb root root 9 | 10 | ENV PGUSER root 11 | ENV PGPASSWORD sebastian 12 | 13 | USER root 14 | 15 | # copied from https://success.docker.com/article/use-a-script-to-initialize-stateful-container-data 16 | COPY docker-entrypoint.sh /usr/local/bin/ 17 | RUN ln -s /usr/local/bin/docker-entrypoint.sh / 18 | 19 | ENTRYPOINT [ "docker-entrypoint.sh" ] 20 | 21 | # docker build -t node-and-pg integration-test 22 | # docker run --rm --mount source=$(pwd),target=/app,type=bind -u postgres -it node-and-pg /bin/bash 23 | # > service postgresql start 24 | # > cd /app 25 | # > npm run integration-test 26 | 27 | CMD [ "npm", "run", "integration-test" ] 28 | -------------------------------------------------------------------------------- /lib/aspect/git/dateUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const oneDay = 24 * 60 * 60 * 1000; // hours*minutes*seconds*milliseconds 18 | 19 | export function daysSince(date: Date): number { 20 | const now = new Date(); 21 | return Math.round(Math.abs((now.getTime() - date.getTime()) / oneDay)); 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "newLine": "LF", 4 | "target": "ES2016", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "jsx": "react", 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "lib": [ 12 | "dom", 13 | "ES2017", 14 | "dom.iterable", 15 | "scripthost", 16 | "esnext.asynciterable" 17 | ], 18 | "strict": false, 19 | "strictNullChecks": false, 20 | "forceConsistentCasingInFileNames": true, 21 | "noImplicitReturns": true, 22 | "noUnusedLocals": false, 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true 25 | }, 26 | "include": [ 27 | "index.ts", 28 | "views/**/*.tsx", 29 | "lib/**/*.ts", 30 | "test/**/*.ts", 31 | "public/**/*.ts" 32 | ], 33 | "exclude": [ 34 | ".#*" 35 | ], 36 | "compileOnSave": true, 37 | "buildOnSave": false, 38 | "atom": { 39 | "rewriteTsconfig": false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/analysis/offline/SpideredRepo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ProjectAnalysisResult } from "../ProjectAnalysisResult"; 18 | 19 | /** 20 | * Spidered repo to persist 21 | */ 22 | export interface SpideredRepo extends ProjectAnalysisResult { 23 | 24 | query?: string; 25 | 26 | topics: string[]; 27 | 28 | /** 29 | * Comes from GitHub/wherever 30 | */ 31 | sourceData: any; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /integration-test/README.md: -------------------------------------------------------------------------------- 1 | # Integration tests for sdm-pack-aspect 2 | 3 | These test Postgres database setup and use. 4 | 5 | To run only these tests, from the repository root: `npm run test:integration` 6 | 7 | To DELETE YOUR DATABASE, re-create it, and run these tests: `npm run integration-test` 8 | 9 | ## Run them in Docker 10 | 11 | A much better idea is to run these in Docker. 12 | 13 | Here is a way to do this. 14 | From the root of this repository (not this directory), build a container: 15 | 16 | `docker build -t node-and-pg integration-test` 17 | 18 | Then run it, mounting this repo's files, and get a shell: 19 | 20 | `docker run --rm --name integration-test --mount source=$(pwd),target=/app,type=bind -it node-and-pg /bin/bash` 21 | 22 | Inside that shell, run the tests (repeat if necessary): 23 | 24 | `npm run integration-test` 25 | 26 | The first time, the db:delete command will fail because the database didn't exist. This is fine. 27 | 28 | Here's a handy command to look around in the database in the container, while it's running: 29 | 30 | `docker exec -it integration-test psql -d org_viz` -------------------------------------------------------------------------------- /test/aspect/common/virtualProjectAspect.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as assert from "assert"; 18 | import { virtualProjectAspect } from "../../../lib/aspect/common/virtualProjectAspect"; 19 | 20 | import { aspectSpecifiesNoEntropy } from "../../../lib/routes/api"; 21 | 22 | describe("the virtual projects aspect", () => { 23 | it("does not care about entropy", () => { 24 | const vpa = virtualProjectAspect({}); 25 | 26 | assert.strictEqual(aspectSpecifiesNoEntropy(vpa), true); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/aspect/compose/commonTypes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Aspect } from "@atomist/sdm-pack-fingerprint"; 18 | import { Omit } from "../../util/omit"; 19 | 20 | /** 21 | * Aspect metadata without extract or consolidate. Used in Aspect consolidation 22 | * and aspect creation utility functions. 23 | */ 24 | export type AspectMetadata = Omit, "extract" | "consolidate" | "apply">; 25 | 26 | export interface CountData { 27 | count: number; 28 | } 29 | 30 | /** 31 | * Aspect counting data. 32 | */ 33 | export type CountAspect = Aspect; 34 | -------------------------------------------------------------------------------- /lib/aspect/reporters.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { ReportBuilder } from "../tree/TreeBuilder"; 18 | import { Analyzed } from "./AspectRegistry"; 19 | 20 | /** 21 | * Implemented by objects that can report against a cohort of repos, 22 | * building a tree. 23 | */ 24 | export interface Reporter { 25 | summary: string; 26 | description: string; 27 | builder: ReportBuilder; 28 | } 29 | 30 | /** 31 | * Implemented by object exposing reports we can run against aspects 32 | */ 33 | export type Reporters = Record; 34 | -------------------------------------------------------------------------------- /lib/util/fileUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Project, 19 | ProjectFile, 20 | } from "@atomist/automation-client"; 21 | 22 | /** 23 | * Return the first file found of the given paths 24 | * @param {Project} p 25 | * @param {string} paths 26 | * @return {Promise} 27 | */ 28 | export async function firstFileFound(p: Project, ...paths: string[]): Promise { 29 | for (const path of paths) { 30 | const f = await p.getFile(path); 31 | if (f) { 32 | return f; 33 | } 34 | } 35 | return undefined; 36 | } 37 | -------------------------------------------------------------------------------- /lib/aspect/community/oss.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Aspect } from "@atomist/sdm-pack-fingerprint"; 18 | import { globAspect } from "../compose/globAspect"; 19 | 20 | /** 21 | * Look for the presence of a CHANGELOG.md 22 | */ 23 | export const ChangelogAspect: Aspect = 24 | globAspect({ 25 | name: "changelog", 26 | displayName: undefined, 27 | glob: "CHANGELOG.md", 28 | }); 29 | 30 | /** 31 | * Look for presence of a CONTRIBUTING.md file 32 | */ 33 | export const ContributingAspect: Aspect = 34 | globAspect({ name: "contributing", displayName: undefined, glob: "CONTRIBUTING.md" }); 35 | -------------------------------------------------------------------------------- /test/routes/api.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Aspect } from "@atomist/sdm-pack-fingerprint"; 18 | import * as assert from "assert"; 19 | import { aspectSpecifiesNoEntropy } from "../../lib/routes/api"; 20 | 21 | describe("checking aspect for entropiness", () => { 22 | it("returns true if the aspect specified no entropy", () => { 23 | const aspect: Partial> = { 24 | stats: { 25 | defaultStatStatus: { 26 | entropy: false, 27 | }, 28 | }, 29 | }; 30 | const result = aspectSpecifiesNoEntropy(aspect as Aspect); 31 | 32 | assert.strictEqual(result, true); 33 | 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /lib/analysis/offline/spider/ScmSearchCriteria.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Project } from "@atomist/automation-client"; 18 | 19 | /** 20 | * Used to query GitHub 21 | */ 22 | export interface ScmSearchCriteria { 23 | 24 | /** 25 | * Query in GitHub terminology 26 | */ 27 | githubQueries: string[]; 28 | 29 | /** 30 | * Max number of repos to return 31 | */ 32 | maxRetrieved: number; 33 | 34 | /** 35 | * Max number of repos to return 36 | */ 37 | maxReturned: number; 38 | 39 | /** 40 | * Are we interested in persisting this project 41 | * @param {Project} p 42 | * @return {Promise} 43 | */ 44 | projectTest?: (p: Project) => Promise; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /lib/routes/web-app/webAppConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Express, 19 | RequestHandler, 20 | } from "express"; 21 | 22 | import { ProjectAnalysisResultStore } from "../../analysis/offline/persist/ProjectAnalysisResultStore"; 23 | 24 | import { ExtensionPackMetadata } from "@atomist/sdm"; 25 | 26 | import { HttpClientFactory } from "@atomist/automation-client"; 27 | 28 | import { AnalysisTracker } from "../../analysis/tracking/analysisTracker"; 29 | import { AspectRegistry } from "../../aspect/AspectRegistry"; 30 | 31 | export interface WebAppConfig { 32 | express: Express; 33 | handlers: RequestHandler[]; 34 | aspectRegistry: AspectRegistry; 35 | store: ProjectAnalysisResultStore; 36 | instanceMetadata: ExtensionPackMetadata; 37 | httpClientFactory: HttpClientFactory; 38 | analysisTracking: AnalysisTracker; 39 | } 40 | -------------------------------------------------------------------------------- /lib/routes/support/tagUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as _ from "lodash"; 18 | import { 19 | AspectRegistry, 20 | TaggedRepo, 21 | } from "../../aspect/AspectRegistry"; 22 | import { TagUsage } from "../../tree/sunburst"; 23 | 24 | export function tagUsageIn(aspectRegistry: AspectRegistry, relevantRepos: TaggedRepo[]): TagUsage[] { 25 | const relevantTags = _.groupBy(_.flatten(relevantRepos.map(r => r.tags.map(tag => tag.name)))); 26 | return Object.getOwnPropertyNames(relevantTags).map(name => ({ 27 | name, 28 | description: aspectRegistry.availableTags.find(t => t.name === name).description, 29 | severity: aspectRegistry.availableTags.find(t => t.name === name).severity, 30 | parent: aspectRegistry.availableTags.find(t => t.name === name).parent, 31 | count: relevantTags[name].length, 32 | })); 33 | } 34 | -------------------------------------------------------------------------------- /lib/util/fingerprintUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { FP } from "@atomist/sdm-pack-fingerprint"; 18 | import * as _ from "lodash"; 19 | 20 | /** 21 | * Distinct non-root paths found in this fingerprints 22 | * @param {FP[]} fingerprints 23 | * @return {string[]} 24 | */ 25 | export function distinctNonRootPaths(fingerprints: Array<{path?: string}>): string[] { 26 | return _.uniq(fingerprints 27 | .map(fp => fp.path) 28 | .filter(p => !["", ".", undefined].includes(p)), 29 | ); 30 | } 31 | 32 | /** 33 | * Take the path before the literal. Can be empty 34 | * E.g. "thing/src/main/java/com/myco/Foo.java" could return "thing" 35 | * @param {string} path 36 | * @return {string} 37 | */ 38 | export function pathBefore(path: string, literal: string): string { 39 | const at = path.indexOf(literal); 40 | return at <= 0 ? undefined : path.slice(0, at); 41 | } 42 | -------------------------------------------------------------------------------- /lib/scorer/support/exposeFingerprintScore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { RepositoryScorer } from "../../aspect/AspectRegistry"; 18 | import { isScoredAspectFingerprint } from "../../aspect/score/ScoredAspect"; 19 | 20 | /** 21 | * Use as an inMemory scorer. Exposes persisted scores. 22 | * Useful during development. 23 | * @param {string} name 24 | * @return {RepositoryScorer} 25 | */ 26 | export function exposeFingerprintScore(name: string): RepositoryScorer { 27 | return { 28 | name, 29 | scoreFingerprints: async repo => { 30 | const found = repo.analysis.fingerprints 31 | .filter(isScoredAspectFingerprint) 32 | .find(fp => fp.type === name); 33 | return !!found ? 34 | { 35 | score: found.data.weightedScore, 36 | reason: JSON.stringify(found.data.weightedScores), 37 | } as any : 38 | undefined; 39 | }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /lib/aspect/git/gitIgnore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Aspect, 19 | filesAspect, 20 | } from "@atomist/sdm-pack-fingerprint"; 21 | import { conditionalize } from "../compose/conditionalize"; 22 | 23 | export const NodeGitIgnore: Aspect = 24 | conditionalize(filesAspect({ 25 | name: "node-gitignore", 26 | displayName: "Node git ignore", 27 | type: "node-gitignore", 28 | toDisplayableFingerprint: fp => fp.sha, 29 | canonicalize: c => c, 30 | }, ".gitignore", 31 | ), 32 | async p => p.hasFile("package.json")); 33 | 34 | export const JavaGitIgnore: Aspect = 35 | conditionalize(filesAspect({ 36 | name: "spring-gitignore", 37 | displayName: "git ignore", 38 | type: "spring-gitignore", 39 | toDisplayableFingerprint: fp => fp.sha, 40 | canonicalize: c => c, 41 | }, ".gitignore", 42 | ), 43 | async p => p.hasFile("pom.xml")); 44 | -------------------------------------------------------------------------------- /lib/aspect/delivery/DeliveryAspect.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | SoftwareDeliveryMachine, 19 | } from "@atomist/sdm"; 20 | import { DeliveryGoals } from "@atomist/sdm-core"; 21 | import { 22 | Aspect, 23 | PublishFingerprints, 24 | } from "@atomist/sdm-pack-fingerprint"; 25 | 26 | /** 27 | * Aspect that can register to extract fingerprints from Atomist events 28 | */ 29 | export interface DeliveryAspect extends Aspect { 30 | 31 | /** 32 | * Can this delivery aspect be registered given these goals 33 | */ 34 | canRegister(sdm: SoftwareDeliveryMachine, goals: GOALS): boolean; 35 | 36 | /** 37 | * Cause this to emit fingerprints 38 | */ 39 | register(sdm: SoftwareDeliveryMachine, deliveryGoals: GOALS, publisher: PublishFingerprints): void; 40 | 41 | } 42 | 43 | export function isDeliveryAspect(a: Aspect): a is DeliveryAspect { 44 | const maybe = a as DeliveryAspect; 45 | return !!maybe.register; 46 | } 47 | -------------------------------------------------------------------------------- /lib/analysis/ProjectAnalysisResult.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | RemoteRepoRef, 19 | RepoRef, 20 | } from "@atomist/automation-client"; 21 | import { Analyzed } from "../aspect/AspectRegistry"; 22 | 23 | /** 24 | * The result of running one analysis. Allows us to attach further information, 25 | * such as provenance if we spidered it. 26 | */ 27 | export interface ProjectAnalysisResult { 28 | 29 | /** 30 | * Unique database id. Available after persistence. 31 | */ 32 | readonly id?: string; 33 | 34 | readonly repoRef: RepoRef; 35 | 36 | readonly workspaceId: string; 37 | 38 | /** 39 | * analysis 40 | * This is not really optional 41 | */ 42 | readonly analysis?: Analyzed; 43 | 44 | /** 45 | * Date of this analysis 46 | */ 47 | readonly timestamp: Date; 48 | 49 | } 50 | 51 | export function isProjectAnalysisResult(r: any): r is ProjectAnalysisResult { 52 | const maybe = r as ProjectAnalysisResult; 53 | return !!maybe.repoRef && !!maybe.analysis; 54 | } 55 | -------------------------------------------------------------------------------- /lib/routes/support/treeMunging.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | PlantedTree, 19 | SunburstTree, 20 | } from "../../tree/sunburst"; 21 | import { 22 | introduceClassificationLayer, 23 | visit, 24 | } from "../../tree/treeUtils"; 25 | 26 | export function splitByOrg(pt: PlantedTree): PlantedTree { 27 | // Group by organization via an additional layer at the center 28 | return introduceClassificationLayer<{ owner: string }>(pt, 29 | { 30 | descendantClassifier: l => l.owner, 31 | newLayerDepth: 1, 32 | newLayerMeaning: "owner", 33 | }); 34 | } 35 | 36 | export function addRepositoryViewUrl(tree: SunburstTree): SunburstTree { 37 | interface RepoNode { viewUrl?: string; url?: string; id: string; } 38 | 39 | visit(tree, l => { 40 | const rn = l as any as RepoNode; 41 | if (rn.url && rn.id) { 42 | // It's an eligible end node 43 | rn.viewUrl = `/repository?id=${encodeURI(rn.id)}`; 44 | } 45 | return true; 46 | }); 47 | return tree; 48 | } 49 | -------------------------------------------------------------------------------- /lib/machine/machine.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { VirtualProjectFinder } from "@atomist/sdm-pack-fingerprint"; 18 | import { Aspect } from "@atomist/sdm-pack-fingerprint/lib/machine/Aspect"; 19 | import { ClientFactory } from "../analysis/offline/persist/pgUtils"; 20 | import { PostgresProjectAnalysisResultStore } from "../analysis/offline/persist/PostgresProjectAnalysisResultStore"; 21 | import { ProjectAnalysisResultStore } from "../analysis/offline/persist/ProjectAnalysisResultStore"; 22 | import { Analyzer } from "../analysis/offline/spider/Spider"; 23 | import { SpiderAnalyzer } from "../analysis/offline/spider/SpiderAnalyzer"; 24 | 25 | /** 26 | * Create the analyzer used for spidering repos. 27 | * @return {Analyzer} 28 | */ 29 | export function createAnalyzer(aspects: Aspect[], virtualProjectFinder: VirtualProjectFinder): Analyzer { 30 | return new SpiderAnalyzer(aspects, virtualProjectFinder); 31 | } 32 | 33 | export function analysisResultStore(factory: ClientFactory): ProjectAnalysisResultStore { 34 | return new PostgresProjectAnalysisResultStore(factory); 35 | } 36 | -------------------------------------------------------------------------------- /lib/util/showTiming.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { logger } from "@atomist/automation-client"; 18 | 19 | /** 20 | * Log the timing of this function 21 | * @param {string} description 22 | * @param {() => Promise} what 23 | * @return {Promise} 24 | */ 25 | export async function showTiming(description: string, 26 | what: () => Promise): Promise { 27 | const startTime = new Date().getTime(); 28 | try { 29 | const result = await what(); 30 | return result; 31 | } finally { 32 | const endTime = new Date().getTime(); 33 | logger.debug("<%s> took %d milliseconds", 34 | description, endTime - startTime); 35 | } 36 | } 37 | 38 | export async function time(what: () => Promise): Promise<{ result: T, millis: number }> { 39 | const startTime = new Date().getTime(); 40 | let endTime: number; 41 | let result: T; 42 | try { 43 | result = await what(); 44 | } finally { 45 | endTime = new Date().getTime(); 46 | } 47 | return { result, millis: endTime - startTime }; 48 | 49 | } 50 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Atomist Open Source Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for the 4 | Atomist Open Source projects as found on https://github.com/atomist. 5 | 6 | * [Reporting a Vulnerability](#reporting-a-vulnerability) 7 | * [Disclosure Policy](#disclosure-policy) 8 | 9 | ## Reporting a Vulnerability 10 | 11 | The Atomist OSS team and community take all security vulnerabilities 12 | seriously. Thank you for improving the security of our open source 13 | software. We appreciate your efforts and responsible disclosure and will 14 | make every effort to acknowledge your contributions. 15 | 16 | Report security vulnerabilities by emailing the Atomist security team at: 17 | 18 | security@atomist.com 19 | 20 | The lead maintainer will acknowledge your email within 24 hours, and will 21 | send a more detailed response within 48 hours indicating the next steps in 22 | handling your report. After the initial reply to your report, the security 23 | team will endeavor to keep you informed of the progress towards a fix and 24 | full announcement, and may ask for additional information or guidance. 25 | 26 | Report security vulnerabilities in third-party modules to the person or 27 | team maintaining the module. 28 | 29 | ## Disclosure Policy 30 | 31 | When the security team receives a security bug report, they will assign it 32 | to a primary handler. This person will coordinate the fix and release 33 | process, involving the following steps: 34 | 35 | * Confirm the problem and determine the affected versions. 36 | * Audit code to find any potential similar problems. 37 | * Prepare fixes for all releases still under maintenance. These fixes 38 | will be released as fast as possible to NPM. 39 | -------------------------------------------------------------------------------- /lib/tree/sunburst.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Tag } from "../aspect/AspectRegistry"; 18 | 19 | /** 20 | * Information about the use of tags within a repo 21 | */ 22 | export interface TagUsage extends Tag { 23 | count: number; 24 | } 25 | 26 | /** 27 | * SunburstTree with accompanying metadata about the meaning of each level 28 | */ 29 | export interface PlantedTree { 30 | tree: SunburstTree; 31 | circles: SunburstCircleMetadata[]; 32 | errors?: Array<{ message: string }>; 33 | tags?: TagUsage[]; 34 | } 35 | 36 | export interface SunburstCircleMetadata { 37 | meaning: string; 38 | } 39 | 40 | /** 41 | * Tree with leafs with a size element 42 | */ 43 | export interface SunburstTree { 44 | name: string; 45 | children: Array; 46 | } 47 | 48 | export interface SunburstLeaf { 49 | name: string; 50 | size: number; 51 | } 52 | 53 | export type SunburstLevel = SunburstTree | SunburstLeaf; 54 | 55 | export function isSunburstTree(level: SunburstLevel): level is SunburstTree { 56 | const maybe = level as SunburstTree; 57 | return maybe.children !== undefined; 58 | } 59 | -------------------------------------------------------------------------------- /lib/analysis/offline/persist/pgClientFactory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Configuration } from "@atomist/automation-client"; 18 | import * as _ from "lodash"; 19 | import { Pool } from "pg"; 20 | import { ClientFactory } from "./pgUtils"; 21 | 22 | const PoolHolder: { pool: Pool } = { pool: undefined }; 23 | 24 | export function sdmConfigClientFactory(config: Configuration): ClientFactory { 25 | const usedConfig = { 26 | database: "org_viz", 27 | ...(_.get(config, "sdm.postgres") || {}), 28 | }; 29 | if (!PoolHolder.pool) { 30 | PoolHolder.pool = new Pool(usedConfig); 31 | } 32 | return () => { 33 | return PoolHolder.pool.connect().catch(e => { 34 | throw new Error(`${ConnectionErrorHeading} 35 | Connection parameters: ${JSON.stringify(usedConfig, hidePassword)} 36 | Error message: ${e.message}`); 37 | }); 38 | }; 39 | } 40 | 41 | export const ConnectionErrorHeading = "Could not connect to Postgres."; 42 | 43 | function hidePassword(key: any, value: any): any { 44 | if (key === "password") { 45 | return "*****"; 46 | } 47 | return value; 48 | } 49 | -------------------------------------------------------------------------------- /lib/util/commonBands.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | BandCasing, 19 | bandFor, 20 | Bands, 21 | Default, 22 | } from "./bands"; 23 | 24 | export type SizeBands = "low" | "medium" | "high"; 25 | 26 | export type AgeBands = "current" | "recent" | "ancient" | "prehistoric"; 27 | 28 | export const EntropySizeBands: Bands = { 29 | zero: { exactly: 0 }, 30 | low: { upTo: 1 }, 31 | medium: { upTo: 2 }, 32 | high: Default, 33 | }; 34 | 35 | export type StarBands = "½" | "⭐" | "⭐½" | "⭐⭐" | "⭐⭐½" | "⭐⭐⭐" | "⭐⭐⭐½"| "⭐⭐⭐⭐" | "⭐⭐⭐⭐½" | "⭐⭐⭐⭐⭐"; 36 | 37 | const StarCountBands: Bands = { 38 | "½": { exactly: .5}, 39 | "⭐": { exactly: 1 }, 40 | "⭐½": { exactly: 1.5 }, 41 | "⭐⭐": { exactly: 2 }, 42 | "⭐⭐½": { exactly: 2.5 }, 43 | "⭐⭐⭐": { exactly: 3 }, 44 | "⭐⭐⭐½": { exactly: 3.5 }, 45 | "⭐⭐⭐⭐": { exactly: 4 }, 46 | "⭐⭐⭐⭐½": { exactly: 4.5 }, 47 | "⭐⭐⭐⭐⭐": Default, 48 | }; 49 | 50 | export function starBand(score: number): string { 51 | return bandFor(StarCountBands, 52 | Math.round(score * 2) / 2, 53 | { casing: BandCasing.Sentence, includeNumber: false }); 54 | } 55 | -------------------------------------------------------------------------------- /lib/routes/web-app/computeAnalytics.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Aspect } from "@atomist/sdm-pack-fingerprint"; 18 | import { ProjectAnalysisResultStore } from "../../analysis/offline/persist/ProjectAnalysisResultStore"; 19 | import { computeAnalytics } from "../../analysis/offline/spider/analytics"; 20 | import { WebAppConfig } from "./webAppConfig"; 21 | 22 | export function supportComputeAnalyticsButton(conf: WebAppConfig, 23 | analyzer: { 24 | aspectOf(aspectName: string): Aspect | undefined, 25 | }, 26 | persister: ProjectAnalysisResultStore) { 27 | conf.express.post("/computeAnalytics", ...conf.handlers, async (req, res, next) => { 28 | try { 29 | // Ideally we'd do this in the background somehow 30 | // but ideally, we'd have a proper React page call this. 31 | await computeAnalytics({ persister, analyzer }, "local"); 32 | res.send(`Great! You have recomputed analytics across repositories. Hopefully the Overview will be up to date now`); 33 | } catch (e) { 34 | next(e); 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/spider/analytics.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { analyzeCohort } from "../../lib/analysis/offline/spider/analytics"; 18 | 19 | import * as assert from "assert"; 20 | 21 | describe("analyzeCohort", () => { 22 | 23 | it("should analyze none", () => { 24 | const fps = []; 25 | const a = analyzeCohort(fps); 26 | assert.strictEqual(a.count, 0); 27 | assert.strictEqual(a.variants, 0); 28 | }); 29 | 30 | it("should analyze one", () => { 31 | const fps = [{ sha: "abc" }]; 32 | const a = analyzeCohort(fps as any); 33 | assert.strictEqual(a.count, 1); 34 | assert.strictEqual(a.variants, 1); 35 | }); 36 | 37 | it("should analyze two different", () => { 38 | const fps = [{ sha: "abc" }, { sha: "bbc" }]; 39 | const a = analyzeCohort(fps as any); 40 | assert.strictEqual(a.count, 2); 41 | assert.strictEqual(a.variants, 2); 42 | }); 43 | 44 | it("should analyze three", () => { 45 | const fps = [{ sha: "abc" }, { sha: "bbc" }, { sha: "abc" }]; 46 | const a = analyzeCohort(fps as any); 47 | assert.strictEqual(a.count, 3); 48 | assert.strictEqual(a.variants, 2); 49 | }); 50 | 51 | }); 52 | -------------------------------------------------------------------------------- /lib/routes/projectQueries.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | CodeMetricsData, 19 | isCodeMetricsFingerprint, 20 | } from "@atomist/sdm-pack-sloc/lib/aspect/codeMetricsAspect"; 21 | import { CodeStats } from "@atomist/sdm-pack-sloc/lib/slocReport"; 22 | import { Analyzed } from "../aspect/AspectRegistry"; 23 | import { 24 | ReportBuilder, 25 | treeBuilder, 26 | } from "../tree/TreeBuilder"; 27 | 28 | function findCodeMetricsData(ar: Analyzed): CodeMetricsData { 29 | const fp = ar.fingerprints.find(isCodeMetricsFingerprint); 30 | return fp ? fp.data : undefined; 31 | } 32 | 33 | /** 34 | * Languages used in this project 35 | * @type {ReportBuilder} 36 | */ 37 | export const languagesQuery: ReportBuilder = 38 | treeBuilder("by language") 39 | .split({ 40 | namer: ar => ar.id.repo, 41 | splitter: ar => { 42 | const cme = findCodeMetricsData(ar) || { languages: []}; 43 | return cme.languages; 44 | }, 45 | }) 46 | .renderWith(codeStats => { 47 | return { 48 | name: `${codeStats.language.name} (${codeStats.total})`, 49 | size: codeStats.total, 50 | }; 51 | }); 52 | -------------------------------------------------------------------------------- /lib/aspect/compose/microgrammarMatchAspect.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { MicrogrammarBasedFileParser } from "@atomist/automation-client"; 18 | import { Grammar } from "@atomist/microgrammar"; 19 | import { 20 | Aspect, 21 | FP, 22 | } from "@atomist/sdm-pack-fingerprint"; 23 | import { Omit } from "../../util/omit"; 24 | import { 25 | fileMatchAspect, 26 | FileMatchData, 27 | } from "./fileMatchAspect"; 28 | 29 | export interface MicrogrammarMatchParams { 30 | 31 | /** 32 | * Glob to look for 33 | */ 34 | glob: string; 35 | 36 | /** 37 | * Microgrammar to use 38 | */ 39 | grammar: Grammar; 40 | 41 | /** 42 | * Path within the microgrammar match to resolve. Property name. 43 | */ 44 | path: keyof T; 45 | } 46 | 47 | /** 48 | * Check for matches of the given microgrammar with the 49 | */ 50 | export function microgrammarMatchAspect(config: Omit & 51 | MicrogrammarMatchParams): Aspect { 52 | return fileMatchAspect({ 53 | ...config, 54 | parseWith: new MicrogrammarBasedFileParser("root", "matchName", 55 | config.grammar), 56 | pathExpression: `//matchName//${config.path}`, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /lib/aspect/common/globVirtualizer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Aspect, 19 | FP, 20 | } from "@atomist/sdm-pack-fingerprint"; 21 | import { distinctNonRootPaths } from "../../util/fingerprintUtils"; 22 | import { 23 | GlobAspectData, 24 | isGlobMatchFingerprint, 25 | } from "../compose/globAspect"; 26 | 27 | /** 28 | * Virtualize all glob fingerprints into virtual projects 29 | */ 30 | export const GlobVirtualizer: Aspect = { 31 | name: "globSprayer", 32 | displayName: undefined, 33 | extract: async () => [], 34 | consolidate: async fingerprints => { 35 | const emitted: Array> = []; 36 | const projectPaths = distinctNonRootPaths(fingerprints); 37 | const globFingerprints = fingerprints.filter(isGlobMatchFingerprint); 38 | for (const path of projectPaths) { 39 | for (const gf of globFingerprints) { 40 | const data = { 41 | ...gf.data, 42 | matches: gf.data.matches.filter(m => m.path.startsWith(path)), 43 | }; 44 | emitted.push({ 45 | ...gf, 46 | data, 47 | path, 48 | }); 49 | } 50 | } 51 | return emitted; 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /views/topLevelPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOMServer from "react-dom/server"; 3 | 4 | export function renderStaticReactNode(body: React.ReactElement, 5 | title: string, 6 | instanceMetadata: { name: string }, 7 | extraScripts?: string[]): string { 8 | return ReactDOMServer.renderToStaticMarkup( 9 | TopLevelPage({ 10 | bodyContent: body, 11 | pageTitle: title, 12 | instanceMetadata, 13 | extraScripts, 14 | })); 15 | } 16 | 17 | function extraScript(src: string): React.ReactElement { 18 | return ; 19 | } 20 | 21 | export function TopLevelPage(props: { 22 | bodyContent: React.ReactElement, 23 | pageTitle: string, 24 | instanceMetadata: { name: string }, 25 | extraScripts?: string[], 26 | }): React.ReactElement { 27 | return 28 | 29 | 30 | {props.pageTitle} 31 | 32 | 33 | 34 | 35 | {(props.extraScripts || []).map(extraScript)} 36 | 37 |
38 |
39 | 40 | 41 | {props.pageTitle} 42 | 43 | 44 |
45 |
46 |
47 | {props.bodyContent} 48 |
49 |
50 | 51 | {props.instanceMetadata.name} 52 | 53 |
54 | 55 | ; 56 | } 57 | -------------------------------------------------------------------------------- /lib/aspect/secret/snifferOptionsLoader.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | SecretDefinition, 19 | SnifferOptions, 20 | } from "./secretSniffing"; 21 | 22 | import * as yaml from "yamljs"; 23 | 24 | import { AllFiles } from "@atomist/automation-client"; 25 | import * as fs from "fs"; 26 | import * as path from "path"; 27 | 28 | /** 29 | * Based on regular expressions in https://www.ndss-symposium.org/wp-content/uploads/2019/02/ndss2019_04B-3_Meli_paper.pdf 30 | * @type {any[]} 31 | */ 32 | export async function loadSnifferOptions(): Promise { 33 | const secretsYmlPath = path.join(__dirname, "..", "..", "..", "secrets.yml"); 34 | const yamlString = fs.readFileSync(secretsYmlPath, "utf8"); 35 | try { 36 | const native = await yaml.parse(yamlString); 37 | 38 | const secretDefinitions: SecretDefinition[] = native.secrets 39 | .map((s: any) => s.secret) 40 | .map((s: any) => ({ 41 | pattern: new RegExp(s.pattern, "g"), 42 | description: s.description, 43 | })); 44 | 45 | return { 46 | secretDefinitions, 47 | whitelist: native.whitelist || [], 48 | globs: native.globs || [AllFiles], 49 | scanOnlyChangedFiles: native.scanOnlyChangedFiles || false, 50 | }; 51 | } catch (err) { 52 | throw new Error(`Unable to parse secrets.yml: ${err.message}`); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/aspect/compose/conditionalize.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | logger, 19 | Project, 20 | } from "@atomist/automation-client"; 21 | import { toArray } from "@atomist/sdm-core/lib/util/misc/array"; 22 | import { 23 | Aspect, 24 | FP, 25 | } from "@atomist/sdm-pack-fingerprint"; 26 | import { AspectMetadata } from "./commonTypes"; 27 | 28 | /** 29 | * Make this aspect conditional 30 | */ 31 | export function conditionalize(aspect: Aspect, 32 | test: (p: Project) => Promise, 33 | details: Partial = {}): Aspect { 34 | const metadata: AspectMetadata = { 35 | ...aspect, 36 | ...details, 37 | }; 38 | return { 39 | ...metadata, 40 | extract: async (p, pli) => { 41 | const testResult = await test(p); 42 | if (testResult) { 43 | const rawFingerprints = toArray(await aspect.extract(p, pli)); 44 | return rawFingerprints.map(raw => { 45 | const merged: FP = { 46 | ...raw, 47 | type: metadata.name, 48 | }; 49 | logger.debug("Merged fingerprints=%j", merged); 50 | return merged; 51 | }); 52 | } 53 | return undefined; 54 | }, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /test/aspect/codeOfConduct.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { InMemoryProject } from "@atomist/automation-client"; 18 | import { FP } from "@atomist/sdm-pack-fingerprint"; 19 | import * as assert from "power-assert"; 20 | import { 21 | codeOfConduct, 22 | CodeOfConductData, 23 | } from "../../lib/aspect/community/codeOfConduct"; 24 | 25 | // Otherwise necessary type casts are removed 26 | // tslint:disable 27 | 28 | describe("codeOfConduct", () => { 29 | 30 | it("should find no code of conduct", async () => { 31 | const p = InMemoryProject.of(); 32 | const s = await codeOfConduct().extract(p, undefined) as FP; 33 | assert(!s); 34 | }); 35 | 36 | it("should find test code of conduct", async () => { 37 | const p = InMemoryProject.of({ path: "CODE_OF_CONDUCT.md", content: testCoC }); 38 | const s = await codeOfConduct().extract(p, undefined) as FP; 39 | assert(!!s); 40 | assert.strictEqual(s.data.title, "The Benign Code of Conduct"); 41 | }); 42 | 43 | it("should do its best with code of conduct without title", async () => { 44 | const p = InMemoryProject.of({ path: "CODE_OF_CONDUCT.md", content: "meaningless" }); 45 | const s = await codeOfConduct().extract(p, undefined) as FP; 46 | assert(!!s); 47 | assert(!s.data.title); 48 | }); 49 | 50 | }); 51 | 52 | const testCoC = `# The Benign Code of Conduct 53 | 54 | Be nice`; 55 | -------------------------------------------------------------------------------- /secrets.yml: -------------------------------------------------------------------------------- 1 | # List of glob patterns to match files to look for secrets in 2 | globs: 3 | - "**" 4 | - "!**/package-lock.json" 5 | - "!**/shrinkwrap.yaml" 6 | - "!**/yarn.lock" 7 | 8 | secrets: 9 | # List of secrets, with regex and description 10 | # These come from Table III at https://www.ndss-symposium.org/wp-content/uploads/2019/02/ndss2019_04B-3_Meli_paper.pdf 11 | - secret: 12 | pattern: "[1-9][0-9]+-[0-9a-zA-Z]{40}" 13 | description: "Twitter access token" 14 | - secret: 15 | pattern: "EAACEdEose0cBA[0-9A-Za-z]+" 16 | description: "Facebook access token" 17 | - secret: 18 | pattern: "AIza[0-9A-Za-z\-_]{35}" 19 | description: "Google API key" 20 | - secret: 21 | pattern: "[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com" 22 | description: "Google Oauth ID" 23 | - secret: 24 | pattern: "sk_live_[0-9a-z]{32}" 25 | description: "Picatic API Key" 26 | - secret: 27 | pattern: "sk_live_[0-9a-zA-Z]{24}" 28 | description: "Stripe regular API key" 29 | - secret: 30 | pattern: "sq0csp-[0-9A-Za-z\-_]{43}" 31 | description: "Stripe restricted API key" 32 | - secret: 33 | pattern: "sq0atp-[0-9A-Za-z\-_]{22}" 34 | description: "Square access token" 35 | - secret: 36 | pattern: "sq0csp-[0-9A-Za-z\-_]{43}" 37 | description: "Square Oauth Secret" 38 | - secret: 39 | pattern: "access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}" 40 | description: "PayPal Braintree access token" 41 | - secret: 42 | pattern: "amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" 43 | description: "Amazon MWS auth token" 44 | - secret: 45 | pattern: "SK[0-9a-fA-F]{32}" 46 | description: "Twilio API key" 47 | - secret: 48 | pattern: "key-[0-9a-zA-Z]{32}" 49 | description: "MailGun API key" 50 | - secret: 51 | pattern: "[0-9a-f]{32}-us[0-9]{1,2}" 52 | description: "MailChimp API key" 53 | - secret: 54 | pattern: "AKIA[0-9A-Z]{16}" 55 | description: "AWS access key ID" 56 | 57 | # List of acceptable secret-like literals 58 | whitelist: -------------------------------------------------------------------------------- /lib/aspect/common/inspectionVirtualizer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Aspect, 19 | FP, 20 | } from "@atomist/sdm-pack-fingerprint"; 21 | import { distinctNonRootPaths } from "../../util/fingerprintUtils"; 22 | import { 23 | GlobAspectData, 24 | isGlobMatchFingerprint, 25 | } from "../compose/globAspect"; 26 | import { 27 | InspectionAspectData, 28 | isInspectionFingerprint, 29 | } from "./inspectionAspect"; 30 | 31 | /** 32 | * Virtualize all inspection fingerprints into virtual projects 33 | */ 34 | export const InspectionVirtualizer: Aspect = { 35 | name: "globSprayer", 36 | displayName: undefined, 37 | extract: async () => [], 38 | consolidate: async fingerprints => { 39 | const emitted: Array> = []; 40 | const projectPaths = distinctNonRootPaths(fingerprints); 41 | const globFingerprints = fingerprints.filter(isInspectionFingerprint); 42 | for (const path of projectPaths) { 43 | for (const gf of globFingerprints) { 44 | const data = { 45 | ...gf.data, 46 | matches: gf.data.comments 47 | .filter(c => c.sourceLocation) 48 | .filter(c => c.sourceLocation.path.startsWith(path)), 49 | }; 50 | emitted.push({ 51 | ...gf, 52 | data, 53 | path, 54 | }); 55 | } 56 | } 57 | return emitted; 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /lib/aspect/secret/exposedSecrets.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Aspect, sha256 } from "@atomist/sdm-pack-fingerprint"; 18 | import { 19 | ExposedSecret, 20 | sniffProject, 21 | } from "./secretSniffing"; 22 | import { loadSnifferOptions } from "./snifferOptionsLoader"; 23 | 24 | const ExposedSecretsType = "exposed-secret"; 25 | 26 | export type ExposedSecretsData = Pick; 27 | 28 | /** 29 | * Fingerprints the presence of exposed secrets, detected by 30 | * searching code for regular expressions. 31 | */ 32 | export const ExposedSecrets: Aspect = { 33 | name: ExposedSecretsType, 34 | displayName: "Exposed secrets", 35 | baseOnly: true, 36 | extract: async p => { 37 | const exposedSecretsResult = await sniffProject(p, await loadSnifferOptions()); 38 | return exposedSecretsResult.exposedSecrets.map(es => { 39 | const data = { 40 | secret: es.secret, 41 | path: es.path, 42 | description: es.description, 43 | }; 44 | return { 45 | type: ExposedSecretsType, 46 | name: ExposedSecretsType, 47 | data, 48 | sha: sha256(JSON.stringify(data)), 49 | }; 50 | }); 51 | }, 52 | toDisplayableFingerprintName: name => name, 53 | toDisplayableFingerprint: fp => `${fp.data.path}:${fp.data.description}`, 54 | stats: { 55 | defaultStatStatus: { 56 | entropy: false, 57 | }, 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /test/aspect/license.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { InMemoryProject } from "@atomist/automation-client"; 18 | import { license } from "../../lib/aspect/community/license"; 19 | 20 | import * as assert from "assert"; 21 | 22 | describe("license aspect", () => { 23 | 24 | it("should find no license", async () => { 25 | const p = InMemoryProject.of(); 26 | const fp: any = await license().extract(p, undefined); 27 | assert(!!fp.data); 28 | assert.deepStrictEqual(fp.data, { classification: "None", path: undefined }); 29 | }); 30 | 31 | it("should find Apache license at LICENSE", async () => { 32 | const p = InMemoryProject.of({ path: "LICENSE", content: asl }); 33 | const fp: any = await license().extract(p, undefined); 34 | assert(!!fp.data); 35 | assert.deepStrictEqual(fp.data, { classification: "Apache License", path: "LICENSE" }); 36 | }); 37 | 38 | it("should find Apache license at license.txt", async () => { 39 | const p = InMemoryProject.of({ path: "license.txt", content: asl }); 40 | const fp: any = await license().extract(p, undefined); 41 | assert(!!fp.data); 42 | assert.deepStrictEqual(fp.data, { classification: "Apache License", path: "license.txt" }); 43 | }); 44 | 45 | }); 46 | 47 | const asl = ` 48 | Apache License 49 | Version 2.0, January 2004 50 | http://www.apache.org/licenses/ 51 | 52 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 53 | 54 | 1. Definitions. 55 | `; 56 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export * from "./lib/machine/aspectSupport"; 18 | export * from "./lib/machine/configureAspects"; 19 | export * from "./lib/aspect/AspectRegistry"; 20 | export * from "./lib/aspect/AspectReportDetailsRegistry"; 21 | export * from "./lib/tagger/commonTaggers"; 22 | export * from "./lib/aspect/compose/commonTypes"; 23 | export * from "./lib/scorer/Score"; 24 | export * from "./lib/scorer/scoring"; 25 | export * from "./lib/aspect/common/codeOwnership"; 26 | export * from "./lib/aspect/community/codeOfConduct"; 27 | export * from "./lib/aspect/community/license"; 28 | export * from "./lib/aspect/community/oss"; 29 | export * from "./lib/aspect/compose/classificationAspect"; 30 | export * from "./lib/aspect/compose/fileMatchAspect"; 31 | export * from "./lib/aspect/compose/globAspect"; 32 | export * from "./lib/aspect/compose/matchAspect"; 33 | export * from "./lib/aspect/common/globVirtualizer"; 34 | export * from "./lib/aspect/common/inspectionVirtualizer"; 35 | export * from "./lib/aspect/compose/microgrammarMatchAspect"; 36 | export * from "./lib/aspect/git/branchCount"; 37 | export * from "./lib/aspect/git/gitActivity"; 38 | export * from "./lib/aspect/git/gitIgnore"; 39 | export * from "./lib/aspect/git/dateUtils"; 40 | export * from "./lib/aspect/secret/exposedSecrets"; 41 | export * from "./lib/aspect/score/ScoredAspect"; 42 | export * from "./lib/aspect/common/reviewerAspect"; 43 | export * from "./lib/aspect/common/virtualProjectAspect"; 44 | 45 | import * as commonScorers from "./lib/scorer/commonScorers"; 46 | import * as commonTaggers from "./lib/tagger/commonTaggers"; 47 | export * from "./lib/scorer/scorerUtils"; 48 | 49 | export * from "./lib/util/fingerprintUtils"; 50 | 51 | export { commonTaggers, commonScorers }; 52 | -------------------------------------------------------------------------------- /lib/scorer/scorerUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { FP } from "@atomist/sdm-pack-fingerprint"; 18 | import { 19 | RepositoryScorer, 20 | RepoToScore, 21 | } from "../aspect/AspectRegistry"; 22 | import { FiveStar } from "./Score"; 23 | 24 | /** 25 | * Emit the given score only when the condition is met. 26 | * Enables existing scorers to be reused in different context. 27 | */ 28 | export function makeConditional(scorer: RepositoryScorer, 29 | test: (rts: RepoToScore) => boolean): RepositoryScorer { 30 | return { 31 | ...scorer, 32 | scoreFingerprints: async rts => { 33 | return test(rts) ? 34 | scorer.scoreFingerprints(rts) : 35 | undefined; 36 | }, 37 | }; 38 | } 39 | 40 | /** 41 | * Score with the given score when the fingerprint is present 42 | */ 43 | export function scoreOnFingerprintPresence(opts: { 44 | name: string, 45 | scoreWhenPresent?: FiveStar, 46 | scoreWhenAbsent?: FiveStar, 47 | reason: string, 48 | test: (fp: FP) => boolean, 49 | }): RepositoryScorer { 50 | return { 51 | name: opts.name, 52 | scoreFingerprints: async rts => { 53 | const found = rts.analysis.fingerprints 54 | .find(opts.test); 55 | if (found && opts.scoreWhenPresent) { 56 | return { 57 | reason: opts.reason + " - present", 58 | score: opts.scoreWhenPresent, 59 | }; 60 | } 61 | if (!found && opts.scoreWhenAbsent) { 62 | return { 63 | reason: opts.reason + " - absent", 64 | score: opts.scoreWhenAbsent, 65 | }; 66 | } 67 | return undefined; 68 | }, 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /lib/analysis/offline/spider/github/GitCommandGitProjectCloner.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | GitCommandGitProject, 19 | GitProject, 20 | LocalProject, 21 | logger, 22 | Project, 23 | } from "@atomist/automation-client"; 24 | import { GitHubRepoRef } from "@atomist/automation-client/lib/operations/common/GitHubRepoRef"; 25 | import { DirectoryManager } from "@atomist/automation-client/lib/spi/clone/DirectoryManager"; 26 | import { execPromise } from "@atomist/sdm"; 27 | import { 28 | Cloner, 29 | GitHubSearchResult, 30 | } from "./GitHubSpider"; 31 | 32 | /** 33 | * Cloner implementation using GitCommandGitProject directly 34 | */ 35 | export class GitCommandGitProjectCloner implements Cloner { 36 | 37 | public async clone(sourceData: GitHubSearchResult): Promise { 38 | const project = await GitCommandGitProject.cloned( 39 | process.env.GITHUB_TOKEN ? { token: process.env.GITHUB_TOKEN } : undefined, 40 | GitHubRepoRef.from({ 41 | owner: sourceData.owner.login, 42 | repo: sourceData.name, 43 | rawApiBase: "https://api.github.com", // for GitHub Enterprise, make this something like github.yourcompany.com/api/v3 44 | }), { 45 | alwaysDeep: false, 46 | noSingleBranch: true, 47 | depth: 1, 48 | }, 49 | this.directoryManager); 50 | 51 | if (!project.id.sha) { 52 | const sha = await execPromise("git", ["rev-parse", "HEAD"], { 53 | cwd: (project as LocalProject).baseDir, 54 | }); 55 | project.id.sha = sha.stdout.trim(); 56 | logger.debug(`Set sha to ${project.id.sha}`); 57 | } 58 | return project; 59 | } 60 | 61 | public constructor(private readonly directoryManager: DirectoryManager) { } 62 | } 63 | -------------------------------------------------------------------------------- /lib/aspect/common/codeOwnership.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Project } from "@atomist/automation-client"; 18 | import { 19 | Aspect, 20 | ExtractFingerprint, 21 | fingerprintOf, 22 | FP, 23 | } from "@atomist/sdm-pack-fingerprint"; 24 | 25 | export interface CodeOwnershipData { 26 | 27 | /** 28 | * Content of the CODEOWNERS 29 | */ 30 | content: string; 31 | 32 | /** 33 | * JIRA team specified in a comment, if any 34 | */ 35 | jiraTeam?: string; 36 | } 37 | 38 | const CodeOwnershipFingerprintName = "code-ownership"; 39 | 40 | /* 41 | * Find a code ownership file if possible 42 | */ 43 | export const CodeOwnershipExtractor: ExtractFingerprint = 44 | async (p: Project) => { 45 | const codeownersFile = await p.getFile("CODEOWNERS"); 46 | if (codeownersFile) { 47 | const content = await codeownersFile.getContent(); 48 | const jiraTeamMatch = /JiraTeam\((?.*)\)/.exec(content); 49 | const jiraTeam = jiraTeamMatch ? jiraTeamMatch.groups.teamId : "No Jira Team"; 50 | const data: CodeOwnershipData = { 51 | jiraTeam, 52 | content, 53 | }; 54 | return fingerprintOf({ 55 | type: CodeOwnershipFingerprintName, 56 | data, 57 | }); 58 | } 59 | return undefined; 60 | }; 61 | 62 | export function codeOwnership(): Aspect { 63 | return { 64 | displayName: "Code Ownership", 65 | name: "codeOwnership", 66 | baseOnly: true, 67 | extract: CodeOwnershipExtractor, 68 | toDisplayableFingerprint: (fp: FP) => fp.data, 69 | apply: async (p, tsi) => { 70 | throw new Error(`Applying code ownership is not yet supported. But it could be.`); 71 | }, 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /lib/aspect/git/branchCount.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | LocalProject, 19 | logger, 20 | } from "@atomist/automation-client"; 21 | import { execPromise } from "@atomist/sdm"; 22 | import { 23 | fingerprintOf, 24 | } from "@atomist/sdm-pack-fingerprint"; 25 | import { 26 | bandFor, 27 | Default, 28 | } from "../../util/bands"; 29 | import { SizeBands } from "../../util/commonBands"; 30 | import { CountAspect } from "../compose/commonTypes"; 31 | 32 | export const BranchCountType = "branch-count"; 33 | 34 | /** 35 | * Aspect that counts branches in git 36 | */ 37 | export const BranchCount: CountAspect = { 38 | name: BranchCountType, 39 | displayName: "Branch count", 40 | baseOnly: true, 41 | extract: async p => { 42 | const lp = p as LocalProject; 43 | const commandResult = await execPromise( 44 | "git", ["branch", "--list", "-r", "origin/*"], 45 | { 46 | cwd: lp.baseDir, 47 | }); 48 | const count = commandResult.stdout 49 | .split("\n") 50 | .filter(l => !l.includes("origin/HEAD")).length - 1; 51 | const data = { count }; 52 | logger.debug("Branch count for %s is %d", p.id.url, count); 53 | return fingerprintOf({ 54 | type: BranchCountType, 55 | data, 56 | }); 57 | }, 58 | toDisplayableFingerprintName: () => "Branch count", 59 | toDisplayableFingerprint: fp => { 60 | return bandFor({ 61 | low: { upTo: 5 }, 62 | medium: { upTo: 12 }, 63 | high: { upTo: 12 }, 64 | excessive: Default, 65 | }, fp.data.count, { includeNumber: true }); 66 | }, 67 | stats: { 68 | defaultStatStatus: { 69 | entropy: false, 70 | }, 71 | basicStatsPath: "count", 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /lib/aspect/AspectReportDetailsRegistry.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Aspect } from "@atomist/sdm-pack-fingerprint"; 18 | 19 | /** 20 | * Details needed for the web-app to show the aspects 21 | */ 22 | export interface AspectReportDetails { 23 | /** Short name of the aspect as used in headlines etc */ 24 | shortName?: string; 25 | /** Longish description of the aspect */ 26 | description?: string; 27 | /** What is the aspect measuring: a version, a docker image tag */ 28 | unit?: string; 29 | /** The url to the sunburst data to use when rendering the chart on the web-app */ 30 | url?: string; 31 | /** Category to place the aspect in */ 32 | category?: string; 33 | /** Does this aspect support setting targets etc */ 34 | manage?: boolean; 35 | } 36 | 37 | /** 38 | * Extension to core Aspect to have additional metadata 39 | */ 40 | export interface AspectWithReportDetails extends Aspect { 41 | details?: AspectReportDetails; 42 | } 43 | 44 | /** 45 | * Add report details to the provided Aspect 46 | */ 47 | export function enrich(aspect: Aspect, details: AspectReportDetails): AspectWithReportDetails { 48 | return { 49 | ...aspect, 50 | details, 51 | }; 52 | } 53 | 54 | /** 55 | * Test if Aspect is AspectWithReportDetails 56 | */ 57 | export function hasReportDetails(aspect: Aspect | AspectWithReportDetails): aspect is AspectWithReportDetails { 58 | return !!aspect && !!(aspect as AspectWithReportDetails).details; 59 | } 60 | 61 | /** 62 | * Manages aspect metadata such as description, short names etc as used by the web-app 63 | */ 64 | export interface AspectReportDetailsRegistry { 65 | 66 | /** 67 | * Find the known AspectReportDetails for the provided aspect 68 | */ 69 | reportDetailsOf(type: string | Aspect, workspaceId: string): Promise; 70 | } 71 | -------------------------------------------------------------------------------- /test/aspect/dockerParser.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | astUtils, 19 | InMemoryProject, 20 | InMemoryProjectFile, 21 | } from "@atomist/automation-client"; 22 | import { DockerFileParser } from "@atomist/sdm-pack-docker"; 23 | 24 | describe("docker parser", () => { 25 | 26 | it("finds port", async () => { 27 | const file = new InMemoryProjectFile("Dockerfile", df); 28 | const images = await astUtils.findValues(InMemoryProject.of(file), DockerFileParser, "Dockerfile", 29 | "//EXPOSE"); 30 | // console.log(JSON.stringify(images)); 31 | }); 32 | 33 | }); 34 | 35 | const df = `FROM ubuntu:18.04 36 | 37 | LABEL maintainer="Atomist " 38 | 39 | RUN apt-get update && apt-get install -y \\ 40 | dumb-init \\ 41 | && rm -rf /var/lib/apt/lists/* 42 | 43 | RUN mkdir -p /opt/app 44 | 45 | WORKDIR /opt/app 46 | 47 | EXPOSE 2866 48 | 49 | ENV BLUEBIRD_WARNINGS 0 50 | ENV NODE_ENV production 51 | ENV NPM_CONFIG_LOGLEVEL warn 52 | ENV SUPPRESS_NO_CONFIG_WARNING true 53 | 54 | ENTRYPOINT ["dumb-init", "node", "--trace-warnings", "--expose_gc", "--optimize_for_size", "--always_compact", "--max_old_space_size=384"] 55 | 56 | CMD ["/opt/app/node_modules/.bin/atm-start"] 57 | 58 | RUN apt-get update && apt-get install -y \\ 59 | build-essential \\ 60 | curl \\ 61 | git \\ 62 | && rm -rf /var/lib/apt/lists/* 63 | 64 | RUN git config --global user.email "bot@atomist.com" \\ 65 | && git config --global user.name "Atomist Bot" 66 | 67 | RUN curl -sL https://deb.nodesource.com/setup_11.x | bash - \\ 68 | && apt-get install -y nodejs \\ 69 | && rm -rf /var/lib/apt/lists/* 70 | 71 | COPY package.json package-lock.json ./ 72 | 73 | RUN npm ci \\ 74 | && npm cache clean --force 75 | 76 | COPY . . 77 | 78 | # Remove before running in production 79 | ENV ATOMIST_ENV development 80 | `; 81 | -------------------------------------------------------------------------------- /test/analysis/offline/spider/SpiderAnalyzer.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { InMemoryProject } from "@atomist/automation-client"; 18 | import { 19 | Aspect, 20 | fingerprintOf, 21 | } from "@atomist/sdm-pack-fingerprint"; 22 | import * as assert from "assert"; 23 | import { SpiderAnalyzer } from "../../../../lib/analysis/offline/spider/SpiderAnalyzer"; 24 | import { RepoBeingTracked } from "../../../../lib/analysis/tracking/analysisTracker"; 25 | 26 | describe("SpiderAnalyzer", () => { 27 | describe("consolidation", () => { 28 | it("should use aspects created by earlier consolidating aspects", async () => { 29 | const aspect1: Aspect = { 30 | name: "test1", 31 | displayName: "test1", 32 | extract: async () => [], 33 | consolidate: async fps => { 34 | return fingerprintOf({type: "test1fp", data: []}); 35 | }, 36 | }; 37 | const aspect2: Aspect = { 38 | name: "test2", 39 | displayName: "test2", 40 | extract: async () => [], 41 | consolidate: async fps => { 42 | if (fps.find(fp => fp.name === "test1fp")) { 43 | return fingerprintOf({type: "test2fp", data: []}); 44 | } else { 45 | return []; 46 | } 47 | }, 48 | }; 49 | const spiderAnalyzer = new SpiderAnalyzer([aspect1, aspect2], undefined); 50 | const project = InMemoryProject.of(); 51 | const analysis = await spiderAnalyzer.analyze(project, new RepoBeingTracked({description: "foo", repoKey: "bar"})); 52 | assert.strictEqual(analysis.fingerprints.length, 2); 53 | assert(analysis.fingerprints.some(fp => fp.type === "test1fp")); 54 | assert(analysis.fingerprints.some(fp => fp.type === "test2fp")); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /lib/aspect/delivery/support/goalListener.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Aspect, 19 | FP, 20 | PublishFingerprints, 21 | } from "@atomist/sdm-pack-fingerprint"; 22 | /* 23 | * Copyright © 2019 Atomist, Inc. 24 | * 25 | * Licensed under the Apache License, Version 2.0 (the "License"); 26 | * you may not use this file except in compliance with the License. 27 | * You may obtain a copy of the License at 28 | * 29 | * http://www.apache.org/licenses/LICENSE-2.0 30 | * 31 | * Unless required by applicable law or agreed to in writing, software 32 | * distributed under the License is distributed on an "AS IS" BASIS, 33 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 34 | * See the License for the specific language governing permissions and 35 | * limitations under the License. 36 | */ 37 | 38 | import { 39 | GoalExecutionListener, 40 | GoalExecutionListenerInvocation, 41 | PushImpactListenerInvocation, 42 | SdmGoalState, 43 | } from "@atomist/sdm"; 44 | import { toArray } from "@atomist/sdm-core/lib/util/misc/array"; 45 | 46 | export type FindFingerprintsFromGoalExecution = (gei: GoalExecutionListenerInvocation) => Promise; 47 | 48 | export function goalExecutionFingerprinter(fingerprintFinder: FindFingerprintsFromGoalExecution, 49 | publisher: PublishFingerprints, 50 | aspects: Aspect[]): GoalExecutionListener { 51 | return async gei => { 52 | if (gei.goalEvent.state !== SdmGoalState.in_process) { 53 | const fps = await fingerprintFinder(gei); 54 | const pili: PushImpactListenerInvocation = { 55 | ...gei, 56 | project: undefined, 57 | impactedSubProject: undefined, 58 | filesChanged: undefined, 59 | commit: gei.goalEvent.push.after, 60 | push: gei.goalEvent.push, 61 | }; 62 | return publisher(pili, aspects, toArray(fps), {}); 63 | } 64 | return false; 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /lib/aspect/community/codeOfConduct.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Aspect, 19 | fingerprintOf, 20 | } from "@atomist/sdm-pack-fingerprint"; 21 | import { ContentClassifier } from "./ContentClassifier"; 22 | 23 | export const CodeOfConductType = "code-of-conduct"; 24 | 25 | export interface CodeOfConductData { 26 | 27 | /** 28 | * Title inferred from the code of conduct, if it was possible to do so 29 | */ 30 | title?: string; 31 | } 32 | 33 | /** 34 | * Find a code of conduct in a repository if possible. 35 | * Returns no fingerprint if the repository lacks a code of conduct file. 36 | * @constructor 37 | */ 38 | export function codeOfConduct(opts: { classifier: ContentClassifier } = 39 | { classifier: extractTitle }): Aspect { 40 | return { 41 | name: CodeOfConductType, 42 | displayName: "Code of conduct", 43 | baseOnly: true, 44 | extract: async p => { 45 | const codeOfConductFile = await p.getFile("CODE_OF_CONDUCT.md"); 46 | if (codeOfConductFile) { 47 | const content = await codeOfConductFile.getContent(); 48 | const data = { 49 | title: extractTitle(content), 50 | content, 51 | }; 52 | return fingerprintOf({ 53 | type: CodeOfConductType, 54 | data, 55 | }); 56 | } 57 | return undefined; 58 | }, 59 | toDisplayableFingerprintName: () => "Code of conduct", 60 | toDisplayableFingerprint: fpi => { 61 | return fpi.data.title || "untitled"; 62 | }, 63 | }; 64 | } 65 | 66 | const markdownTitleRegex = /^# (.*)\n/; 67 | 68 | /** 69 | * Try to extract the title from this markdown document 70 | * @param {string} mdString 71 | * @return {string | undefined} 72 | */ 73 | function extractTitle(mdString: string): string | undefined { 74 | const match = markdownTitleRegex.exec(mdString); 75 | return (match && match.length === 2) ? 76 | match[1] : 77 | undefined; 78 | } 79 | -------------------------------------------------------------------------------- /lib/scorer/commonWorkspaceScorers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { WorkspaceScorer } from "../aspect/AspectRegistry"; 18 | import { FiveStar } from "./Score"; 19 | 20 | import * as _ from "lodash"; 21 | 22 | export const average = (array: number[]) => array.reduce((a, b) => a + b, 0) / array.length; 23 | 24 | /** 25 | * Use the average repo score as the score 26 | */ 27 | export const AverageRepoScore: WorkspaceScorer = { 28 | name: "average", 29 | description: "Average score for repositories", 30 | score: async od => { 31 | const scores: number[] = od.repos.map(r => r.score); 32 | const score: FiveStar = average(scores) as any as FiveStar; 33 | return { 34 | reason: "mean of all scores", 35 | score, 36 | }; 37 | }, 38 | }; 39 | 40 | /** 41 | * Use the score of the lowest scoring repo as a score 42 | */ 43 | export const WorstRepoScore: WorkspaceScorer = { 44 | name: "worst", 45 | description: "Worst repository", 46 | score: async od => { 47 | const scores: number[] = od.repos.map(r => r.score); 48 | const score = _.min(scores) as FiveStar; 49 | return { 50 | reason: "score of lowest scored repository", 51 | score, 52 | }; 53 | }, 54 | }; 55 | 56 | /** 57 | * Score based on entropy across the organization 58 | */ 59 | export const EntropyScore: WorkspaceScorer = { 60 | name: "entropy", 61 | description: "Entropy across workspace", 62 | score: async od => { 63 | const scores: number[] = od.fingerprintUsage.map(f => { 64 | if (f.entropy > 3) { 65 | return 1; 66 | } 67 | if (f.entropy > 2) { 68 | return 2; 69 | } 70 | if (f.entropy > 1) { 71 | return 3; 72 | } 73 | if (f.entropy > .5) { 74 | return 4; 75 | } 76 | return 5; 77 | }); 78 | const score: FiveStar = average(scores) as any as FiveStar; 79 | return { 80 | reason: "variance among aspects identified in this workspace", 81 | score, 82 | }; 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /lib/analysis/offline/persist/pgUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { logger } from "@atomist/automation-client"; 18 | import { 19 | PoolClient, 20 | } from "pg"; 21 | 22 | export type ClientFactory = () => Promise; 23 | 24 | export type DoWithClientError = Error & { operationDescription: string }; 25 | 26 | /** 27 | * Perform the given operations with a database client connection 28 | * 29 | * Connection errors result in an exception. 30 | * Errors thrown by the passed-in function result are logged, and the 31 | * provided defaultResult is returned (or else undefined). 32 | * The defaultResult can be a constant _or_ a function of the error. 33 | * 34 | * @param {() => } clientFactory factory for clients 35 | * @param {(c: ) => Promise} what a function to run with the client 36 | * @param {R} defaultResult return this in case of error. If not provided, return undefined 37 | * @param description description of what we're doing. Allows for timing 38 | * @return {Promise} 39 | */ 40 | export async function doWithClient(description: string, 41 | clientFactory: ClientFactory, 42 | what: (c: PoolClient) => Promise, 43 | defaultResult?: R | ((e: DoWithClientError) => R)): Promise { 44 | const startTime = new Date().getTime(); 45 | const client = await clientFactory(); 46 | let result: R; 47 | try { 48 | result = await what(client); 49 | } catch (err) { 50 | logger.warn(`Error accessing database in '${description}': `, err); 51 | if (typeof defaultResult === "function") { 52 | err.operationDescription = description; 53 | // if you really want a default value that is a function, 54 | // then pass a function of error that returns that function, please. 55 | return (defaultResult as (e: DoWithClientError) => R)(err); 56 | } 57 | return defaultResult; 58 | } finally { 59 | client.release(); 60 | const endTime = new Date().getTime(); 61 | logger.debug("============= RDBMS operation ==================\n%s\n>>> Executed in %d milliseconds", 62 | description, endTime - startTime); 63 | } 64 | return result; 65 | } 66 | -------------------------------------------------------------------------------- /ddl/create.ddl: -------------------------------------------------------------------------------- 1 | -- create the database if needed (in psql): 2 | 3 | -- DROP DATABASE org_viz; 4 | -- CREATE DATABASE org_viz; 5 | 6 | -- Connect to that database (in psql): 7 | -- \connect org_viz 8 | 9 | -- Or, run this in psql from the terminal: 10 | -- psql -d org_viz -f ddl/create.ddl 11 | 12 | DROP TABLE IF EXISTS repo_fingerprints; 13 | 14 | DROP TYPE IF EXISTS severity; 15 | 16 | DROP TABLE IF EXISTS fingerprints; 17 | 18 | DROP TABLE IF EXISTS repo_snapshots; 19 | 20 | DROP TABLE IF EXISTS fingerprint_analytics; 21 | 22 | -- Contains the latest snapshot for the given repository 23 | -- Application code should delete any previously held data for this 24 | -- repository so we only have one snapshot for every repository 25 | CREATE TABLE repo_snapshots ( 26 | workspace_id varchar NOT NULL, 27 | id varchar NOT NULL, 28 | provider_id text NOT NULL, 29 | owner text NOT NULL, 30 | name text NOT NULL, 31 | url text NOT NULL, 32 | branch text, 33 | commit_sha varchar NOT NULL, 34 | timestamp TIMESTAMP NOT NULL, 35 | query text, 36 | PRIMARY KEY(workspace_id, id) 37 | ); 38 | 39 | -- Each fingerprint we've seen 40 | CREATE TABLE fingerprints ( 41 | workspace_id varchar NOT NULL, 42 | id varchar NOT NULL, 43 | name text NOT NULL, 44 | feature_name text NOT NULL, 45 | sha varchar NOT NULL, 46 | data jsonb, 47 | display_name varchar, 48 | display_value varchar, 49 | PRIMARY KEY(workspace_id, id) 50 | ); 51 | 52 | -- Join table between repo_snapshots and fingerprints 53 | CREATE TABLE IF NOT EXISTS repo_fingerprints ( 54 | workspace_id varchar NOT NULL, 55 | repo_snapshot_id varchar, 56 | fingerprint_id varchar NOT NULL, 57 | path varchar, 58 | FOREIGN KEY (workspace_id, fingerprint_id) REFERENCES fingerprints (workspace_id, id) ON DELETE CASCADE, 59 | FOREIGN KEY (workspace_id, repo_snapshot_id) REFERENCES repo_snapshots(workspace_id, id) ON DELETE CASCADE, 60 | PRIMARY KEY (repo_snapshot_id, fingerprint_id, workspace_id, path) 61 | ); 62 | 63 | -- Usage information about fingerprints 64 | -- This table must be kept up to date by application code 65 | -- whenever a fingerprint is inserted 66 | CREATE TABLE fingerprint_analytics ( 67 | name text NOT NULL, 68 | feature_name text NOT NULL, 69 | workspace_id varchar NOT NULL, 70 | count numeric, 71 | entropy numeric, 72 | variants numeric, 73 | compliance numeric, 74 | PRIMARY KEY (name, feature_name, workspace_id) 75 | ); 76 | 77 | CREATE TYPE severity AS ENUM ('info', 'warn', 'error'); 78 | 79 | CREATE INDEX ON repo_fingerprints (workspace_id, repo_snapshot_id); 80 | CREATE INDEX ON repo_fingerprints (workspace_id, fingerprint_id); 81 | 82 | CREATE INDEX ON fingerprints (name); 83 | CREATE INDEX ON fingerprints (feature_name); 84 | 85 | CREATE INDEX ON fingerprint_analytics (workspace_id); 86 | CREATE INDEX ON fingerprint_analytics (feature_name); 87 | 88 | -- Index for tag data 89 | CREATE INDEX tag_index ON fingerprints ((data->'reason')); 90 | -------------------------------------------------------------------------------- /lib/machine/configureAspects.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Configuration } from "@atomist/automation-client"; 18 | import { PushImpact } from "@atomist/sdm"; 19 | import { 20 | configure, 21 | DeliveryGoals, 22 | } from "@atomist/sdm-core"; 23 | import { toArray } from "@atomist/sdm-core/lib/util/misc/array"; 24 | import { 25 | Aspect, 26 | RebaseFailure, 27 | RebaseStrategy, 28 | } from "@atomist/sdm-pack-fingerprint"; 29 | import { DeepPartial } from "ts-essentials"; 30 | import { 31 | aspectSupport, 32 | AspectSupportOptions, 33 | } from "./aspectSupport"; 34 | 35 | // This SDM only has a single PushImpact goal which is used 36 | // to run your aspects on Git pushes 37 | interface AnalyzeGoals extends DeliveryGoals { 38 | 39 | pushImpact: PushImpact; 40 | } 41 | 42 | /** 43 | * Configure a single or multiple aspects with an SDM 44 | */ 45 | export async function configureAspects(aspects: Aspect | Aspect[], 46 | options: DeepPartial = {}): Promise { 47 | return configure(async sdm => { 48 | // This creates and configures the goal instance 49 | const goals = await sdm.createGoals(async () => ({ pushImpact: new PushImpact() })); 50 | 51 | // This installs the required extension pack into the SDM 52 | // to run aspects and expose the local web ui for testing 53 | sdm.addExtensionPacks( 54 | aspectSupport({ 55 | 56 | // Pass the aspects you want to run in this SDM 57 | aspects: toArray(aspects), 58 | 59 | // Pass the PushImpact goal into the aspect support for it 60 | // to get configured 61 | goals, 62 | 63 | // Configure how existing branches should be rebased 64 | // during aspect apply executions 65 | rebase: { 66 | rebase: true, 67 | rebaseStrategy: RebaseStrategy.Ours, 68 | onRebaseFailure: RebaseFailure.DeleteBranch, 69 | }, 70 | 71 | ...options as any, 72 | }), 73 | ); 74 | 75 | // Return a signal goal set to run the push impact goal 76 | // on any push 77 | return { 78 | analyze: { 79 | goals: goals.pushImpact, 80 | }, 81 | }; 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /views/aspectTrackingPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | interface AspectPerformance { 3 | aspectName: string; 4 | runs: number; 5 | totalFingerprints: number; 6 | minMillis: number; 7 | maxMillis: number; 8 | failures: number; 9 | totalTimeTaken: number; 10 | } 11 | interface AspectTrackingProps { 12 | aspectPerformances: AspectPerformance[]; 13 | } 14 | 15 | function displayAspectPerformance(world: { widthOfGraph: number, pixelsPerMilli: number }, ap: AspectPerformance): React.ReactElement { 16 | 17 | let belowLowWidth = Math.round(ap.minMillis * world.pixelsPerMilli); 18 | let betweenWidth = Math.round((ap.maxMillis - ap.minMillis) * world.pixelsPerMilli); 19 | if (betweenWidth < 5) { 20 | betweenWidth = 6; 21 | belowLowWidth = Math.max(0, belowLowWidth - 3); 22 | } 23 | if ((belowLowWidth + betweenWidth) > world.widthOfGraph) { 24 | belowLowWidth = world.widthOfGraph - betweenWidth; 25 | } 26 | 27 | return 28 | {ap.aspectName}{ap.runs}{ap.totalFingerprints} 29 | {ap.failures}{ap.minMillis}ms 30 |
31 |
{ap.minMillis}
32 |
- {ap.maxMillis}
34 | 35 |
36 | {ap.maxMillis}ms 37 | ; 38 | } 39 | 40 | function sortAspectPerformance(a1: AspectPerformance, a2: AspectPerformance): number { 41 | return a2.totalTimeTaken - a1.totalTimeTaken; 42 | } 43 | 44 | export function AspectTrackingPage(props: AspectTrackingProps): React.ReactElement { 45 | if (props.aspectPerformances.length === 0) { 46 | return
No analyses in progress. 47 | Start one at the command line:{" "} 48 | atomist analyze local repositories
; 49 | } 50 | 51 | const widthOfGraph = 500; 52 | const minniestMin = Math.min(...props.aspectPerformances.map(a => a.minMillis)); 53 | const maxiestMax = Math.max(...props.aspectPerformances.map(a => a.maxMillis)); 54 | const pixelsPerMilli = widthOfGraph / (maxiestMax - minniestMin); 55 | return 56 | 57 | 58 | 61 | 62 | 63 | {props.aspectPerformances.sort(sortAspectPerformance).map(ap => displayAspectPerformance({ widthOfGraph, pixelsPerMilli }, ap))} 64 |
NameRunsFPsFailsFastest run
59 | {minniestMin} ms --- Range of time taken --- 60 | {maxiestMax} ms
Slowest run
; 65 | } 66 | -------------------------------------------------------------------------------- /lib/analysis/tracking/analysisTrackingRoutes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { logger } from "@atomist/automation-client"; 18 | import * as _ from "lodash"; 19 | import { AnalysisTrackerPage } from "../../../views/analysisTrackingPage"; 20 | import { AspectTrackingPage } from "../../../views/aspectTrackingPage"; 21 | import { renderStaticReactNode } from "../../../views/topLevelPage"; 22 | import { WebAppConfig } from "../../routes/web-app/webAppConfig"; 23 | 24 | export function exposeAnalysisTrackerPage(conf: WebAppConfig): void { 25 | conf.express.get("/analysis", ...conf.handlers, async (req, res, next) => { 26 | try { 27 | const data = conf.analysisTracking.report(); 28 | 29 | res.send(renderStaticReactNode( 30 | AnalysisTrackerPage(data), `Analyzing projects`, 31 | conf.instanceMetadata)); 32 | } catch (e) { 33 | logger.error(e.stack); 34 | next(e); 35 | } 36 | }); 37 | 38 | conf.express.get("/analysis/aspects", ...conf.handlers, async (req, res, next) => { 39 | try { 40 | const data = conf.analysisTracking.report(); 41 | 42 | const allAspectTimings = _.flatten(data.analyses 43 | .map(a => _.flatten(a.repos 44 | .filter(r => r.progress === "Stopped") 45 | .map(r => r.aspects)))); 46 | const timingsByAspect = _.groupBy(allAspectTimings, a => a.aspectName); 47 | 48 | const summarized = Object.values(timingsByAspect).map(timings => { 49 | return { 50 | aspectName: timings[0].aspectName, 51 | runs: timings.length, 52 | totalFingerprints: timings.map(t => t.fingerprintsFound).reduce((a, b) => a + b, 0), 53 | failures: timings.filter(f => !!f.error).length, 54 | minMillis: Math.min(...timings.map(t => t.millisTaken)), 55 | maxMillis: Math.max(...timings.map(t => t.millisTaken)), 56 | totalTimeTaken: timings.map(t => t.millisTaken).reduce((a, b) => a + b, 0), 57 | }; 58 | }); 59 | 60 | res.send(renderStaticReactNode( 61 | AspectTrackingPage({ aspectPerformances: summarized }), `Performance of Aspects during Analysis`, 62 | conf.instanceMetadata)); 63 | } catch (e) { 64 | logger.error(e.stack); 65 | next(e); 66 | } 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /lib/aspect/compose/matchAspect.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { MatchResult } from "@atomist/automation-client"; 18 | import { 19 | fileMatches, 20 | PathExpressionQueryOptions, 21 | } from "@atomist/automation-client/lib/tree/ast/astUtils"; 22 | import { 23 | Aspect, 24 | fingerprintOf, 25 | } from "@atomist/sdm-pack-fingerprint"; 26 | 27 | import * as _ from "lodash"; 28 | import { Omit } from "../../util/omit"; 29 | import { 30 | GlobAspectData, 31 | GlobAspectMetadata, 32 | GlobMatch, 33 | } from "./globAspect"; 34 | 35 | export interface MatchAspectOptions extends GlobAspectMetadata, Omit { 36 | mapper: (m: MatchResult) => D; 37 | } 38 | 39 | /** 40 | * Take a glob pattern and parser 41 | * @param {MatchAspectOptions} config 42 | * @return {Aspect>} 43 | */ 44 | export function matchAspect(config: MatchAspectOptions): Aspect> { 45 | return { 46 | toDisplayableFingerprintName: name => `Glob pattern '${name}'`, 47 | toDisplayableFingerprint: fp => 48 | fp.data.matches.length === 0 ? 49 | "None" : 50 | fp.data.matches 51 | .map(m => `${m.path}(${m.size})`) 52 | .join(), 53 | stats: { 54 | defaultStatStatus: { 55 | entropy: false, 56 | }, 57 | }, 58 | ...config, 59 | extract: async p => { 60 | const fms = _.flatten((await fileMatches(p, { ...config, globPatterns: config.glob })) 61 | .map(fileHit => fileHit.matches.map(match => ({ file: fileHit.file, match })))); 62 | const matches: Array = fms.map(fm => { 63 | const d = config.mapper(fm.match); 64 | if (!d) { 65 | return undefined; 66 | } 67 | return { 68 | ...d, 69 | kind: "glob", 70 | glob: config.glob, 71 | path: fm.file.path, 72 | size: -1, 73 | }; 74 | }).filter(x => !!x); 75 | const data = { 76 | glob: config.glob, 77 | kind: "globMatch" as any, 78 | matches, 79 | }; 80 | if (!config.alwaysEmit && data.matches.length === 0) { 81 | return []; 82 | } 83 | return fingerprintOf({ 84 | type: config.name, 85 | data, 86 | }); 87 | }, 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /lib/aspect/common/virtualProjectAspect.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Project, 19 | ProjectFile, 20 | } from "@atomist/automation-client"; 21 | import { 22 | Aspect, 23 | fingerprintOf, 24 | } from "@atomist/sdm-pack-fingerprint"; 25 | 26 | import * as _ from "lodash"; 27 | 28 | import { isFile } from "@atomist/automation-client/lib/project/File"; 29 | import * as pathlib from "path"; 30 | 31 | export interface VirtualProjectData { 32 | 33 | readonly reason: string; 34 | 35 | /** 36 | * Virtual project paths within this project 37 | */ 38 | readonly path: string; 39 | } 40 | 41 | export interface VirtualProjectFinding { 42 | 43 | readonly reason: string; 44 | 45 | /** 46 | * Virtual project paths within this project 47 | */ 48 | readonly paths: string[]; 49 | } 50 | 51 | export const VirtualProjectType = "virtual-projects"; 52 | 53 | export interface VirtualProjectAspectConfig { 54 | 55 | /** 56 | * Number of virtual projects at which to veto further analysis 57 | */ 58 | virtualProjectLimit?: number; 59 | 60 | } 61 | 62 | /** 63 | * Emit a fingerprint for each virtual project in the repository 64 | */ 65 | export function virtualProjectAspect( 66 | config: VirtualProjectAspectConfig, 67 | ...finders: Array<(p: Project) => Promise>): Aspect { 68 | return { 69 | name: VirtualProjectType, 70 | displayName: "Virtual project", 71 | extract: async p => { 72 | const findings = []; 73 | for (const finder of finders) { 74 | findings.push(await finder(p)); 75 | } 76 | return _.flatten(findings.map(finding => 77 | finding.paths.map(path => fingerprintOf({ 78 | type: VirtualProjectType, 79 | data: { reason: finding.reason, path }, 80 | path, 81 | }), 82 | ))); 83 | }, 84 | vetoWhen: fps => 85 | fps.length > (config.virtualProjectLimit || Number.MAX_VALUE) ? 86 | { reason: `Too many virtual projects: Found ${fps.length}, limit at ${config.virtualProjectLimit}` } : 87 | false, 88 | stats: { 89 | defaultStatStatus: { 90 | entropy: false, 91 | }, 92 | }, 93 | }; 94 | } 95 | 96 | /** 97 | * Convenient function to extract directory name from the file 98 | * @param {File} f 99 | * @return {string} 100 | */ 101 | export function dirName(f: ProjectFile | string): string { 102 | return pathlib.dirname(isFile(f) ? f.path : f); 103 | } 104 | -------------------------------------------------------------------------------- /lib/routes/web-app/repositoryListPage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | RepoForDisplay, 19 | RepoList, 20 | } from "../../../views/repoList"; 21 | import { renderStaticReactNode } from "../../../views/topLevelPage"; 22 | import { WebAppConfig } from "./webAppConfig"; 23 | 24 | export type SortOrder = "name" | "score"; 25 | 26 | /** 27 | * Takes sortOrder optional parameter to dictate sorting 28 | */ 29 | export function exposeRepositoryListPage(conf: WebAppConfig): void { 30 | conf.express.get("/repositories", ...conf.handlers, async (req, res, next) => { 31 | try { 32 | const workspaceId = req.query.workspace || req.params.workspace_id || "local"; 33 | const sortOrder: SortOrder = req.query.sortOrder || "score"; 34 | const byOrg = req.query.byOrg !== "false"; 35 | const category = req.query.category || "*"; 36 | 37 | const allAnalysisResults = await conf.store.loadInWorkspace(workspaceId, true); 38 | 39 | // optional query parameter: owner 40 | const relevantAnalysisResults = allAnalysisResults.filter(ar => req.query.owner ? ar.analysis.id.owner === req.query.owner : true); 41 | if (relevantAnalysisResults.length === 0) { 42 | res.send(`No matching repos for organization ${req.query.owner}`); 43 | return; 44 | } 45 | 46 | const relevantRepos = await conf.aspectRegistry.tagAndScoreRepos(workspaceId, relevantAnalysisResults, { category }); 47 | const repos: RepoForDisplay[] = relevantRepos 48 | .map(ar => ({ 49 | url: ar.analysis.id.url, 50 | repo: ar.analysis.id.repo, 51 | owner: ar.analysis.id.owner, 52 | id: ar.id, 53 | score: ar.weightedScore.weightedScore, 54 | showFullPath: !byOrg, 55 | })); 56 | const virtualProjectCount = await conf.store.virtualProjectCount(workspaceId); 57 | 58 | const fingerprintUsage = await conf.store.fingerprintUsageForType(workspaceId); 59 | 60 | const orgScore = await conf.aspectRegistry.scoreWorkspace(workspaceId, { fingerprintUsage, repos }); 61 | res.send(renderStaticReactNode( 62 | RepoList({ 63 | orgScore, 64 | repos, 65 | virtualProjectCount, 66 | sortOrder, 67 | byOrg, 68 | expand: !byOrg, 69 | category, 70 | }), 71 | byOrg ? "Repositories by Organization" : "Repositories Ranked", 72 | conf.instanceMetadata)); 73 | return; 74 | } catch (e) { 75 | next(e); 76 | } 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | In the interest of fostering an open and welcoming environment, we as 4 | contributors and maintainers pledge to making participation in our 5 | project and our community a harassment-free experience for everyone, 6 | regardless of age, body size, disability, ethnicity, gender identity 7 | and expression, level of experience, nationality, personal appearance, 8 | race, religion, or sexual identity and orientation. 9 | 10 | Examples of behavior that contributes to creating a positive 11 | environment include: 12 | 13 | * Using welcoming and inclusive language 14 | * Being respectful of differing viewpoints and experiences 15 | * Gracefully accepting constructive criticism 16 | * Focusing on what is best for the community 17 | * Showing empathy towards other community members 18 | 19 | Examples of unacceptable behavior by participants include: 20 | 21 | * The use of sexualized language or imagery and unwelcome sexual 22 | attention or advances 23 | * Trolling, insulting/derogatory comments, and personal or political 24 | attacks 25 | * Public or private harassment 26 | * Publishing others' private information, such as a physical or 27 | electronic address, without explicit permission 28 | * Other conduct which could reasonably be considered inappropriate 29 | in a professional setting 30 | 31 | Project maintainers are responsible for clarifying the standards of 32 | acceptable behavior and are expected to take appropriate and fair 33 | corrective action in response to any instances of unacceptable 34 | behavior. 35 | 36 | Project maintainers have the right and responsibility to remove, edit, 37 | or reject comments, commits, code, wiki edits, issues, and other 38 | contributions that are not aligned to this Code of Conduct, or to ban 39 | temporarily or permanently any contributor for other behaviors that 40 | they deem inappropriate, threatening, offensive, or harmful. 41 | 42 | This Code of Conduct applies both within project spaces and in public 43 | spaces when an individual is representing the project or its 44 | community. Examples of representing a project or community include 45 | using an official project e-mail address, posting via an official 46 | social media account, or acting as an appointed representative at an 47 | online or offline event. Representation of a project may be further 48 | defined and clarified by project maintainers. 49 | 50 | Instances of abusive, harassing, or otherwise unacceptable behavior 51 | may be reported by contacting the project team 52 | at [code-of-conduct@atomist.com][email]. All complaints will be 53 | reviewed and investigated and will result in a response that is deemed 54 | necessary and appropriate to the circumstances. The project team is 55 | obligated to maintain confidentiality with regard to the reporter of 56 | an incident. Further details of specific enforcement policies may be 57 | posted separately. 58 | 59 | Project maintainers who do not follow or enforce the Code of Conduct 60 | in good faith may face temporary or permanent repercussions as 61 | determined by other members of the project's leadership. 62 | 63 | This Code of Conduct is adapted from 64 | the [Contributor Covenant][homepage], version 1.4, available 65 | at [http://contributor-covenant.org/version/1/4][version] 66 | 67 | [homepage]: http://contributor-covenant.org 68 | [version]: http://contributor-covenant.org/version/1/4/ 69 | [email]: mailto:code-of-conduct@atomist.com 70 | -------------------------------------------------------------------------------- /lib/aspect/secret/secretSniffing.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Project, 19 | projectUtils, 20 | RepoRef, 21 | } from "@atomist/automation-client"; 22 | import * as _ from "lodash"; 23 | 24 | export interface ExposedSecret { 25 | 26 | repoRef: RepoRef; 27 | 28 | /** 29 | * File path within project 30 | */ 31 | path: string; 32 | 33 | secret: string; 34 | 35 | description: string; 36 | 37 | // TODO add source location extraction 38 | } 39 | 40 | /** 41 | * Definition of a secret we can find in a project 42 | */ 43 | export interface SecretDefinition { 44 | 45 | /** 46 | * Regexp for the secret 47 | */ 48 | pattern: RegExp; 49 | 50 | /** 51 | * Description of the problem. For example, what kind of secret this is. 52 | */ 53 | description: string; 54 | } 55 | 56 | export interface SnifferOptions { 57 | 58 | scanOnlyChangedFiles: boolean; 59 | 60 | globs: string[]; 61 | 62 | secretDefinitions: SecretDefinition[]; 63 | 64 | /** 65 | * Whitelisted secrets 66 | */ 67 | whitelist: string[]; 68 | } 69 | 70 | /** 71 | * Result of sniffing 72 | */ 73 | export interface SniffResult { 74 | options: SnifferOptions; 75 | exposedSecrets: ExposedSecret[]; 76 | filesSniffed: number; 77 | timeMillis: number; 78 | } 79 | 80 | /** 81 | * Sniff this project for exposed secrets. 82 | * Open every file. 83 | */ 84 | export async function sniffProject(project: Project, options: SnifferOptions): Promise { 85 | let filesSniffed = 0; 86 | const startTime = new Date().getTime(); 87 | const exposedSecrets = _.flatten(await projectUtils.gatherFromFiles(project, options.globs, async f => { 88 | if (await f.isBinary()) { 89 | return undefined; 90 | } 91 | ++filesSniffed; 92 | return sniffFileContent(project.id, f.path, await f.getContent(), options); 93 | })); 94 | return { 95 | options, 96 | filesSniffed, 97 | exposedSecrets, 98 | timeMillis: new Date().getTime() - startTime, 99 | }; 100 | } 101 | 102 | export async function sniffFileContent(repoRef: RepoRef, path: string, content: string, opts: SnifferOptions): Promise { 103 | const exposedSecrets: ExposedSecret[] = []; 104 | for (const sd of opts.secretDefinitions) { 105 | const matches = content.match(sd.pattern) || []; 106 | matches 107 | .filter(m => !opts.whitelist.includes(m)) 108 | .forEach(m => exposedSecrets.push(({ 109 | repoRef, 110 | path, 111 | description: sd.description, 112 | secret: m, 113 | }))); 114 | } 115 | return exposedSecrets; 116 | } 117 | -------------------------------------------------------------------------------- /test/aspect/common/globAspect.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | InMemoryProject, 19 | Project, 20 | } from "@atomist/automation-client"; 21 | import { 22 | Aspect, 23 | FP, 24 | } from "@atomist/sdm-pack-fingerprint"; 25 | import { 26 | globAspect, 27 | GlobAspectData, 28 | } from "../../../lib/aspect/compose/globAspect"; 29 | 30 | import * as assert from "assert"; 31 | 32 | describe("glob aspect", () => { 33 | 34 | it("should find none and not emit by default in empty project", async () => { 35 | const ga = globAspect({ name: "foo", glob: "thing", displayName: "" }); 36 | const fp = await extractify(ga, InMemoryProject.of()); 37 | assert.deepStrictEqual(fp, []); 38 | }); 39 | 40 | it("should find none and emit in empty project", async () => { 41 | const ga = globAspect({ name: "foo", glob: "thing", displayName: "", alwaysEmit: true }); 42 | const fp = await extractify(ga, InMemoryProject.of()); 43 | assert.strictEqual(fp.data.matches.length, 0); 44 | }); 45 | 46 | it("should find one with no content test", async () => { 47 | const ga = globAspect({ name: "foo", glob: "thing", displayName: "" }); 48 | const fp = await extractify(ga, InMemoryProject.of({ path: "thing", content: "x" })); 49 | assert.strictEqual(fp.data.matches.length, 1); 50 | assert.strictEqual(fp.data.matches[0].path, "thing"); 51 | }); 52 | 53 | it("should find none with content test", async () => { 54 | const ga = globAspect({ name: "foo", glob: "thing", displayName: "", contentTest: () => false }); 55 | const fp = await extractify(ga, InMemoryProject.of({ path: "thing", content: "x" })); 56 | assert.deepStrictEqual(fp, []); 57 | }); 58 | 59 | it("should find none but emit with content test", async () => { 60 | const ga = globAspect({ name: "foo", glob: "thing", alwaysEmit: true, displayName: "", contentTest: () => false }); 61 | const fp = await extractify(ga, InMemoryProject.of({ path: "thing", content: "x" })); 62 | assert.deepStrictEqual(fp.data.matches, []); 63 | }); 64 | 65 | it("should add custom data to match", async () => { 66 | const ga = globAspect<{color: string}>({ 67 | name: "foo", glob: "thing", displayName: "", 68 | extract: async content => ({ color: "yellow" }), 69 | }); 70 | const fp = await extractify<{color: string}>(ga, InMemoryProject.of({ path: "thing", content: "x" })); 71 | assert.strictEqual(fp.data.matches.length, 1); 72 | assert.strictEqual(fp.data.matches[0].path, "thing"); 73 | assert.strictEqual(fp.data.matches[0].color, "yellow"); 74 | }); 75 | 76 | }); 77 | 78 | async function extractify(ga: Aspect, p: Project): Promise>> { 79 | return ga.extract(p, undefined) as any; 80 | } 81 | -------------------------------------------------------------------------------- /lib/util/bands.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as _ from "lodash"; 18 | 19 | export interface UpTo { 20 | upTo: number; 21 | } 22 | 23 | export interface Exactly { 24 | exactly: number; 25 | } 26 | 27 | export const Default = "default"; 28 | 29 | export type Band = UpTo | Exactly | typeof Default; 30 | 31 | export type Bands = Record; 32 | 33 | function isExactly(b: Band): b is Exactly { 34 | const maybe = b as Exactly; 35 | return !!maybe && maybe.exactly !== undefined; 36 | } 37 | 38 | function isUpTo(b: Band): b is UpTo { 39 | const maybe = b as UpTo; 40 | return !!maybe && maybe.upTo !== undefined; 41 | } 42 | 43 | export enum BandCasing { 44 | NoChange, 45 | Sentence, 46 | } 47 | 48 | /** 49 | * Return the band for the given value 50 | * @param {Bands} bands 51 | * @param {number} value 52 | * @return {string} 53 | */ 54 | export function bandFor(bands: Bands, 55 | value: number, 56 | options: { 57 | includeNumber?: boolean, 58 | casing?: BandCasing, 59 | } = { includeNumber: false, casing: BandCasing.NoChange }): string { 60 | const bandNames = Object.getOwnPropertyNames(bands); 61 | for (const bandName of bandNames) { 62 | const band = bands[bandName]; 63 | if (isExactly(band) && band.exactly === value) { 64 | return format(value, bandName, band, options); 65 | } 66 | } 67 | const upToBands: Array<{ name: string, band: UpTo }> = _.sortBy( 68 | bandNames 69 | .map(name => ({ name, band: bands[name] })) 70 | .filter(nb => isUpTo(nb.band)) as any, 71 | b => b.band.upTo); 72 | for (const upTo of upToBands) { 73 | if (upTo.band.upTo > value) { 74 | return format(value, upTo.name, upTo.band, options); 75 | } 76 | } 77 | return formatName(bandNames.find(n => bands[n] === Default), options); 78 | } 79 | 80 | function format(value: number, name: string, band: Band, options: { includeNumber?: boolean, casing?: BandCasing }): string { 81 | const includeNumber = options.includeNumber; 82 | const fName = formatName(name, options); 83 | 84 | if (includeNumber && isExactly(band)) { 85 | return `${fName} (=${band.exactly})`; 86 | } 87 | if (includeNumber && isUpTo(band)) { 88 | return `${fName} (<${band.upTo})`; 89 | } 90 | return fName; 91 | } 92 | 93 | function formatName(name: string, options: { casing?: BandCasing } = {}): string { 94 | if (options.casing === BandCasing.Sentence) { 95 | return _.upperFirst(name); 96 | } else { 97 | return name; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/aspect/delivery/storeFingerprintsPublisher.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { logger } from "@atomist/automation-client"; 18 | import { 19 | PublishFingerprints, 20 | PublishFingerprintsFor, 21 | RepoIdentification, 22 | } from "@atomist/sdm-pack-fingerprint"; 23 | import { ProjectAnalysisResultStore } from "../../analysis/offline/persist/ProjectAnalysisResultStore"; 24 | import { computeAnalyticsForFingerprintKind } from "../../analysis/offline/spider/analytics"; 25 | import { ProjectAnalysisResult } from "../../analysis/ProjectAnalysisResult"; 26 | import { Analyzed } from "../AspectRegistry"; 27 | 28 | /** 29 | * "Publish" fingerprints to local store 30 | * @param {ProjectAnalysisResultStore} store 31 | * @return {PublishFingerprints} 32 | */ 33 | export function storeFingerprintsFor(store: ProjectAnalysisResultStore): PublishFingerprintsFor { 34 | return async (ctx, aspects, repoIdentification, fingerprints, previous) => { 35 | if (fingerprints.length === 0) { 36 | return true; 37 | } 38 | 39 | const analysis: Analyzed = { 40 | id: repoIdentification as any, 41 | fingerprints, 42 | }; 43 | const workspaceId = ctx.context.workspaceId; 44 | const paResult: ProjectAnalysisResult = { 45 | repoRef: repoIdentification as any, 46 | workspaceId, 47 | timestamp: undefined, 48 | analysis, 49 | }; 50 | logger.info("Routing %d fingerprints to local database for workspace %s", 51 | fingerprints.length, ctx.context.workspaceId); 52 | const found = await store.loadByRepoRef(workspaceId, paResult.analysis.id, false); 53 | if (!!found) { 54 | const results = await store.persistAdditionalFingerprints({ 55 | fingerprints: paResult.analysis.fingerprints, 56 | id: paResult.analysis.id, 57 | workspaceId: ctx.context.workspaceId, 58 | }); 59 | logger.info("Persisting additional fingerprint results for %s: %j", paResult.analysis.id.url, results); 60 | 61 | for (const fp of fingerprints) { 62 | await computeAnalyticsForFingerprintKind(store, ctx.context.workspaceId, fp.type, fp.name); 63 | } 64 | 65 | return results.failures.length === 0; 66 | } else { 67 | const results = await store.persist(paResult); 68 | logger.info("Persisting snapshot for %s: %j", paResult.analysis.id.url, results); 69 | 70 | for (const fp of fingerprints) { 71 | await computeAnalyticsForFingerprintKind(store, ctx.context.workspaceId, fp.type, fp.name); 72 | } 73 | return results.failed.length === 0; 74 | } 75 | }; 76 | } 77 | 78 | export function storeFingerprints(store: ProjectAnalysisResultStore): PublishFingerprints { 79 | return (i, aspects, fps, previous) => { 80 | return storeFingerprintsFor(store)(i, aspects, i.id as RepoIdentification, fps, previous); 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased](https://github.com/atomist-seeds/empty-sdm/compare/0.1.28...HEAD) 9 | 10 | ### Added 11 | 12 | - Allow to optionally include repos in drift report. [#237](https://github.com/atomist/sdm-pack-aspect/issues/237) 13 | 14 | ### Changed 15 | 16 | - Support for dynamic aspects, report details. [#209](https://github.com/atomist/sdm-pack-aspect/issues/209) 17 | 18 | ## [0.1.28](https://github.com/atomist-seeds/empty-sdm/compare/0.1.27...0.1.28) - 2019-08-02 19 | 20 | ## [0.1.27](https://github.com/atomist-seeds/empty-sdm/compare/0.1.26...0.1.27) - 2019-08-02 21 | 22 | ## [0.1.26](https://github.com/atomist-seeds/empty-sdm/compare/0.1.25...0.1.26) - 2019-08-02 23 | 24 | ## [0.1.25](https://github.com/atomist-seeds/empty-sdm/compare/0.1.24...0.1.25) - 2019-08-01 25 | 26 | ## [0.1.24](https://github.com/atomist-seeds/empty-sdm/compare/0.1.23...0.1.24) - 2019-08-01 27 | 28 | ## [0.1.23](https://github.com/atomist-seeds/empty-sdm/compare/0.1.22...0.1.23) - 2019-08-01 29 | 30 | ## [0.1.22](https://github.com/atomist-seeds/empty-sdm/compare/0.1.21...0.1.22) - 2019-08-01 31 | 32 | ## [0.1.21](https://github.com/atomist-seeds/empty-sdm/compare/0.1.20...0.1.21) - 2019-07-26 33 | 34 | ## [0.1.20](https://github.com/atomist-seeds/empty-sdm/compare/0.1.19...0.1.20) - 2019-07-17 35 | 36 | ## [0.1.19](https://github.com/atomist-seeds/empty-sdm/compare/0.1.18...0.1.19) - 2019-07-09 37 | 38 | ## [0.1.18](https://github.com/atomist-seeds/empty-sdm/compare/0.1.17...0.1.18) - 2019-07-08 39 | 40 | ## [0.1.17](https://github.com/atomist-seeds/empty-sdm/compare/0.1.16...0.1.17) - 2019-07-08 41 | 42 | ## [0.1.16](https://github.com/atomist-seeds/empty-sdm/compare/0.1.15...0.1.16) - 2019-07-05 43 | 44 | ## [0.1.15](https://github.com/atomist-seeds/empty-sdm/compare/0.1.14...0.1.15) - 2019-07-04 45 | 46 | ## [0.1.14](https://github.com/atomist-seeds/empty-sdm/compare/0.1.13...0.1.14) - 2019-07-04 47 | 48 | ## [0.1.13](https://github.com/atomist-seeds/empty-sdm/compare/0.1.12...0.1.13) - 2019-07-04 49 | 50 | ## [0.1.12](https://github.com/atomist-seeds/empty-sdm/compare/0.1.11...0.1.12) - 2019-07-01 51 | 52 | ## [0.1.11](https://github.com/atomist-seeds/empty-sdm/compare/0.1.10...0.1.11) - 2019-07-01 53 | 54 | ## [0.1.10](https://github.com/atomist-seeds/empty-sdm/compare/0.1.9...0.1.10) - 2019-06-17 55 | 56 | ## [0.1.9](https://github.com/atomist-seeds/empty-sdm/compare/0.1.8...0.1.9) - 2019-06-13 57 | 58 | ## [0.1.8](https://github.com/atomist-seeds/empty-sdm/compare/0.1.7...0.1.8) - 2019-06-13 59 | 60 | ## [0.1.7](https://github.com/atomist-seeds/empty-sdm/compare/0.1.6...0.1.7) - 2019-06-13 61 | 62 | ## [0.1.6](https://github.com/atomist-seeds/empty-sdm/compare/0.1.5...0.1.6) - 2019-06-13 63 | 64 | ## [0.1.5](https://github.com/atomist-seeds/empty-sdm/compare/0.1.4...0.1.5) - 2019-06-13 65 | 66 | ## [0.1.4](https://github.com/atomist-seeds/empty-sdm/compare/0.1.3...0.1.4) - 2019-06-13 67 | 68 | ## [0.1.3](https://github.com/atomist-seeds/empty-sdm/compare/0.1.2...0.1.3) - 2019-05-07 69 | 70 | ## [0.1.2](https://github.com/atomist-seeds/empty-sdm/compare/0.1.1...0.1.2) - 2019-05-07 71 | 72 | ## [0.1.1](https://github.com/atomist-seeds/empty-sdm/compare/0.1.0...0.1.1) - 2019-05-07 73 | 74 | ## [0.1.0](https://github.com/atomist-seeds/empty-sdm/tree/0.1.0) - 2019-05-07 75 | 76 | ### Added 77 | 78 | - Empty SDM. 79 | 80 | ### Security 81 | 82 | - Removed nodemon and updated npm-run-all. [#15](https://github.com/atomist-seeds/empty-sdm/issues/15) 83 | -------------------------------------------------------------------------------- /lib/aspect/delivery/BuildAspect.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { logger } from "@atomist/automation-client"; 18 | import { Build } from "@atomist/sdm-pack-build"; 19 | import { 20 | Aspect, 21 | fingerprintOf, 22 | } from "@atomist/sdm-pack-fingerprint"; 23 | import { 24 | bandFor, 25 | Default, 26 | } from "../../util/bands"; 27 | import { Omit } from "../../util/omit"; 28 | import { AspectMetadata } from "../compose/commonTypes"; 29 | import { DeliveryAspect } from "./DeliveryAspect"; 30 | import { 31 | FindFingerprintsFromGoalExecution, 32 | goalExecutionFingerprinter, 33 | } from "./support/goalListener"; 34 | 35 | export type BuildAspect = DeliveryAspect<{ build: Build }, DATA>; 36 | 37 | /** 38 | * Create an SDM BuildListener from BuildAspect 39 | */ 40 | export function buildOutcomeAspect(opts: AspectMetadata & { 41 | fingerprintFinder: FindFingerprintsFromGoalExecution, 42 | }): BuildAspect { 43 | return { 44 | ...opts, 45 | extract: async () => [], 46 | canRegister: (sdm, goals) => !!goals.build, 47 | register: (sdm, goals, publisher) => { 48 | if (!goals.build) { 49 | throw new Error("No build goal supplied. Cannot register a build aspect"); 50 | } 51 | logger.info("Registering build outcome aspect '%s'", opts.name); 52 | return goals.build.withExecutionListener(goalExecutionFingerprinter(opts.fingerprintFinder, publisher, [opts as any])); 53 | }, 54 | stats: { 55 | basicStatsPath: "elapsedMillis", 56 | defaultStatStatus: { 57 | entropy: false, 58 | }, 59 | }, 60 | }; 61 | } 62 | 63 | export interface BuildTimeData { 64 | elapsedMillis: number; 65 | } 66 | 67 | export const BuildTimeType = "build-time"; 68 | 69 | /** 70 | * Capture build time 71 | */ 72 | export function buildTimeAspect(opts: Omit = {}): BuildAspect { 73 | return buildOutcomeAspect({ 74 | ...opts, 75 | name: BuildTimeType, 76 | displayName: "Build time", 77 | fingerprintFinder: async gei => { 78 | const elapsedMillis = Date.now() - gei.goalEvent.ts; 79 | const data = { elapsedMillis }; 80 | return fingerprintOf({ 81 | type: BuildTimeType, 82 | data, 83 | }); 84 | }, 85 | toDisplayableFingerprintName: () => "Build time", 86 | toDisplayableFingerprint: fp => { 87 | const seconds = fp.data.elapsedMillis / 1000; 88 | return bandFor<"interminable" | "slow" | "ok" | "fast" | "blistering">({ 89 | blistering: { upTo: 10 }, 90 | fast: { upTo: 60 }, 91 | ok: { upTo: 180 }, 92 | slow: { upTo: 600 }, 93 | interminable: Default, 94 | }, seconds, 95 | { includeNumber: true }); 96 | }, 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /lib/aspect/compose/fileMatchAspect.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | FileParser, 19 | } from "@atomist/automation-client"; 20 | import { fileHitIterator } from "@atomist/automation-client/lib/tree/ast/astUtils"; 21 | import { 22 | Aspect, 23 | fingerprintOf, 24 | FP, 25 | } from "@atomist/sdm-pack-fingerprint"; 26 | import { Omit } from "../../util/omit"; 27 | 28 | export interface FileMatch { 29 | filePath: string; 30 | pathExpression: string; 31 | matchValue: string; 32 | } 33 | 34 | const FileMatchType = "file-match"; 35 | type FileMatchType = "file-match"; 36 | 37 | export interface FileMatchData { 38 | kind: FileMatchType; 39 | glob: string; 40 | matches: FileMatch[]; 41 | } 42 | 43 | export function isFileMatchFingerprint(fp: FP): fp is FP { 44 | const maybe = fp.data as FileMatchData; 45 | return !!maybe && maybe.kind === FileMatchType && maybe.kind === "file-match" && !!maybe.glob; 46 | } 47 | 48 | export interface FileMatchParams { 49 | 50 | /** 51 | * Glob to look for 52 | */ 53 | glob: string; 54 | 55 | parseWith: FileParser; 56 | 57 | pathExpression: string; 58 | } 59 | 60 | /** 61 | * Check for presence of a match within the AST of files matching the glob. 62 | * Always return something, but may have an empty path. 63 | */ 64 | export function fileMatchAspect(config: Omit & 65 | FileMatchParams): Aspect { 66 | return { 67 | toDisplayableFingerprintName: name => `File match '${config.glob}'`, 68 | toDisplayableFingerprint: fp => 69 | fp.data.matches.length === 0 ? 70 | "None" : 71 | fp.data.matches 72 | .map(m => m.matchValue) 73 | .join(), 74 | stats: { 75 | defaultStatStatus: { 76 | entropy: false, 77 | }, 78 | }, 79 | ...config, 80 | extract: 81 | async p => { 82 | const matches: FileMatch[] = []; 83 | const it = fileHitIterator(p, { 84 | parseWith: config.parseWith, 85 | pathExpression: config.pathExpression, 86 | globPatterns: config.glob, 87 | }); 88 | for await (const match of it) { 89 | matches.push({ 90 | filePath: match.file.path, 91 | pathExpression: config.pathExpression, 92 | matchValue: match.matches[0].$value, 93 | }); 94 | } 95 | const data = { 96 | kind: FileMatchType as any, 97 | matches, 98 | glob: config.glob, 99 | }; 100 | return fingerprintOf({ 101 | type: config.name, 102 | data, 103 | }); 104 | }, 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /lib/analysis/offline/spider/analytics.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Aspect, 19 | FP, 20 | } from "@atomist/sdm-pack-fingerprint"; 21 | import * as _ from "lodash"; 22 | import { aspectSpecifiesNoEntropy } from "../../../routes/api"; 23 | import { 24 | FingerprintKind, 25 | ProjectAnalysisResultStore, 26 | } from "../persist/ProjectAnalysisResultStore"; 27 | 28 | /** 29 | * Retrieve all fingerprints, then compute and store fingerprint_analytics for the whole workspace 30 | */ 31 | export async function computeAnalytics( 32 | world: { 33 | persister: ProjectAnalysisResultStore, 34 | analyzer: { 35 | aspectOf(aspectName: string): Aspect | undefined, 36 | }, 37 | }, 38 | workspaceId: string): Promise { 39 | const allFingerprints = await world.persister.fingerprintsInWorkspace(workspaceId, false); 40 | const fingerprintKinds = await world.persister.distinctFingerprintKinds(workspaceId); 41 | 42 | const persistThese = fingerprintKinds.filter(k => !aspectSpecifiesNoEntropy(world.analyzer.aspectOf(k.type))) 43 | .map((kind: FingerprintKind) => { 44 | const fingerprintsOfKind = allFingerprints.filter(f => f.type === kind.type && f.name === kind.name); 45 | const cohortAnalysis = analyzeCohort(fingerprintsOfKind); 46 | return { workspaceId, kind, cohortAnalysis }; 47 | }); 48 | 49 | await world.persister.persistAnalytics(persistThese); 50 | } 51 | 52 | /** 53 | * Calculate and persist entropy for one fingerprint kind 54 | */ 55 | export async function computeAnalyticsForFingerprintKind(persister: ProjectAnalysisResultStore, 56 | workspaceId: string, 57 | type: string, 58 | name: string): Promise { 59 | const fingerprints = await persister.fingerprintsInWorkspace(workspaceId, false, type, name); 60 | if (fingerprints.length === 0) { 61 | return; 62 | } 63 | const cohortAnalysis = analyzeCohort(fingerprints); 64 | await persister.persistAnalytics([{ workspaceId, kind: { type, name }, cohortAnalysis }]); 65 | } 66 | 67 | /** 68 | * Result of analyzing a cohort of fingerprints. 69 | */ 70 | export interface CohortAnalysis { 71 | count: number; 72 | variants: number; 73 | entropy: number; 74 | compliance: number; 75 | } 76 | 77 | /** 78 | * Analyze a cohort of the same kind of fingerprints 79 | */ 80 | export function analyzeCohort(fps: FP[]): CohortAnalysis { 81 | const groups: Record = _.groupBy(fps, fp => fp.sha); 82 | const count: number = fps.length; 83 | const entropy = -1 * Object.values(groups).reduce( 84 | (agg, fp: FP[]) => { 85 | const p: number = fp.length / count; 86 | return agg + p * Math.log(p); 87 | }, 88 | 0, 89 | ); 90 | return { 91 | entropy, 92 | variants: Object.values(groups).length, 93 | count, 94 | compliance: undefined, 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /test/routes/wep-app/webAppRoutes.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const sampleData = { 18 | tree: { 19 | name: "drift", 20 | children: [ 21 | { 22 | name: "high", 23 | children: [ 24 | { 25 | name: "NPM dependencies", 26 | type: "npm-project-deps", 27 | children: [{ 28 | name: "@atomist/automation-client", 29 | fingerprint_name: "atomist::automation-client", 30 | type: "npm-project-deps", variants: 23, count: 23, entropy: 3.1354942159291497, size: 23, 31 | }, { 32 | name: "@types/node", 33 | fingerprint_name: "types::node", type: "npm-project-deps", variants: 17, count: 17, entropy: 2.833213344056216, size: 17, 34 | }, { 35 | name: "typescript", 36 | fingerprint_name: "typescript", type: "npm-project-deps", variants: 16, count: 16, entropy: 2.772588722239781, size: 16, 37 | }], 38 | }], 39 | }, 40 | ], 41 | }, 42 | circles: [ 43 | { 44 | meaning: "report", 45 | }, 46 | { 47 | meaning: "entropy band", 48 | }, 49 | { 50 | meaning: "aspect name", 51 | }, 52 | { 53 | meaning: "fingerprint name", 54 | }, 55 | ], 56 | }; 57 | 58 | import * as assert from "assert"; 59 | import * as _ from "lodash"; 60 | import { populateLocalURLs } from "../../../lib/routes/web-app/webAppRoutes"; 61 | 62 | describe("adding URLs to local pages", () => { 63 | it("Can add the URL to an aspect", async () => { 64 | const copy = _.cloneDeep(sampleData); 65 | populateLocalURLs(copy); 66 | 67 | // Any node (with a "type" property) at the level with meaning "aspect name" should get a URL 68 | // to "/fingerprint/${type}" 69 | 70 | const aspectNodeInThisSampleData = copy.tree.children[0].children[0] as any; 71 | 72 | assert(!(copy.tree as any).url, "This node should not have a URL added"); 73 | assert.strictEqual(aspectNodeInThisSampleData.url, "/fingerprint/npm-project-deps/*", 74 | "Found url: " + aspectNodeInThisSampleData.url); 75 | }); 76 | 77 | it("Can add the URL to a fingerprint", async () => { 78 | const copy = _.cloneDeep(sampleData); 79 | populateLocalURLs(copy); 80 | 81 | // Any node (with a "type" and "fingerprint_name" property) at the level with meaning "fingerprint name" should get a URL 82 | // to "/fingerprint/${type}/${fingerprint_name}" 83 | 84 | const fingerprintNodesInThisSampleData = copy.tree.children[0].children[0].children as any[]; 85 | 86 | fingerprintNodesInThisSampleData.forEach(n => 87 | assert.strictEqual(n.url, `/fingerprint/npm-project-deps/${encodeURIComponent(n.fingerprint_name)}`, 88 | `Found url: ${n.url} for ${n.name}`)); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/aspect/secretSniffing.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { InMemoryProject } from "@atomist/automation-client"; 18 | import * as assert from "power-assert"; 19 | import { 20 | sniffFileContent, 21 | sniffProject, 22 | } from "../../lib/aspect/secret/secretSniffing"; 23 | import { loadSnifferOptions } from "../../lib/aspect/secret/snifferOptionsLoader"; 24 | 25 | describe("secret sniffing", () => { 26 | 27 | describe("tests project", () => { 28 | 29 | it("finds not secrets in empty project", async () => { 30 | const p = InMemoryProject.of(); 31 | const sniffed = await sniffProject(p, await loadSnifferOptions()); 32 | assert.strictEqual(sniffed.exposedSecrets.length, 0); 33 | }); 34 | 35 | it("doesn't object to innocent JS file in project", async () => { 36 | const p = InMemoryProject.of({ 37 | path: "evil.js", 38 | content: "const myString = 'kinder than the Dalai Lama'", 39 | }); 40 | const sniffed = await sniffProject(p, await loadSnifferOptions()); 41 | assert.strictEqual(sniffed.exposedSecrets.length, 0); 42 | }); 43 | 44 | it("finds leaky JS file in project", async () => { 45 | const p = InMemoryProject.of({ 46 | path: "evil.js", 47 | content: "const awsLeak = 'AKIAIMW6ASF43DFX57X9'", 48 | }); 49 | const sniffed = await sniffProject(p, 50 | await loadSnifferOptions()); 51 | assert.strictEqual(sniffed.exposedSecrets.length, 1); 52 | assert.strictEqual(sniffed.exposedSecrets[0].path, "evil.js"); 53 | assert.strictEqual(sniffed.exposedSecrets[0].secret, "AKIAIMW6ASF43DFX57X9"); 54 | assert.deepStrictEqual(sniffed.exposedSecrets[0].repoRef, p.id); 55 | }); 56 | 57 | }); 58 | 59 | describe("tests file", () => { 60 | 61 | it("doesn't object to innocent JS file", async () => { 62 | const exposedSecrets = await sniffFileContent(undefined, "evil.js", "const myString = 'kinder than the Dalai Lama'", 63 | await loadSnifferOptions()); 64 | assert.strictEqual(exposedSecrets.length, 0); 65 | }); 66 | 67 | it("finds leaky JS file", async () => { 68 | const exposedSecrets = await sniffFileContent(undefined, "evil.js", 69 | "const awsLeak = 'AKIAIMW6ASF43DFX57X9'", 70 | await loadSnifferOptions()); 71 | assert.strictEqual(exposedSecrets.length, 1); 72 | assert.strictEqual(exposedSecrets[0].path, "evil.js"); 73 | assert.strictEqual(exposedSecrets[0].secret, "AKIAIMW6ASF43DFX57X9"); 74 | }); 75 | 76 | it("respects whitelist", async () => { 77 | const opts = await loadSnifferOptions(); 78 | opts.whitelist = ["AKIAIMW6ASF43DFX57X9"]; 79 | const exposedSecrets = await sniffFileContent(undefined, "evil.js", 80 | "const awsLeak = 'AKIAIMW6ASF43DFX57X9'", 81 | opts); 82 | assert.strictEqual(exposedSecrets.length, 0); 83 | }); 84 | }); 85 | 86 | }); 87 | -------------------------------------------------------------------------------- /lib/aspect/common/inspectionAspect.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | logger, 19 | NoParameters, 20 | ProjectReview, 21 | ReviewComment, 22 | } from "@atomist/automation-client"; 23 | import { 24 | CodeTransform, 25 | ReviewerRegistration, 26 | } from "@atomist/sdm"; 27 | import { ApplyFingerprint, Aspect, FP, sha256 } from "@atomist/sdm-pack-fingerprint"; 28 | import { CodeInspection } from "@atomist/sdm/lib/api/registration/CodeInspectionRegistration"; 29 | import { AspectMetadata } from "../compose/commonTypes"; 30 | 31 | export type EligibleReviewer = ReviewerRegistration | CodeInspection; 32 | 33 | export interface InspectionAspectOptions extends AspectMetadata { 34 | 35 | /** 36 | * Reviewer that can provide the fingerprint 37 | */ 38 | readonly reviewer: EligibleReviewer; 39 | 40 | /** 41 | * Code transform that can remove usages of this problematic fingerprint 42 | */ 43 | readonly terminator?: CodeTransform; 44 | 45 | } 46 | 47 | export interface InspectionAspectData { 48 | magnitude: number; 49 | comments: ReviewComment[]; 50 | } 51 | 52 | export function isInspectionFingerprint(fp: FP): fp is FP { 53 | return !!fp.data && !!fp.data.magnitude && !!fp.data.comments; 54 | } 55 | 56 | /** 57 | * Create fingerprints from the output of this reviewer. 58 | * Every fingerprint is unique 59 | */ 60 | export function inspectionAspect(opts: InspectionAspectOptions): Aspect { 61 | const inspection = isReviewerRegistration(opts.reviewer) ? opts.reviewer.inspection : opts.reviewer; 62 | const type = opts.name; 63 | return { 64 | ...opts, 65 | name: type, 66 | extract: async (p, pli) => { 67 | const result = await inspection(p, { ...pli, push: pli }); 68 | if (!result) { 69 | return []; 70 | } 71 | const magnitude = result.comments.length; 72 | return { 73 | type, 74 | name: type, 75 | data: { 76 | comments: result.comments, 77 | magnitude, 78 | }, 79 | sha: sha256({ magnitude }), 80 | }; 81 | }, 82 | apply: opts.terminator ? terminateWithExtremePrejudice(opts) : undefined, 83 | }; 84 | } 85 | 86 | function terminateWithExtremePrejudice(opts: InspectionAspectOptions): ApplyFingerprint { 87 | return async (p, pi) => { 88 | const to = pi.parameters.fp; 89 | if (to.data.count !== 0) { 90 | const msg = `Doesn't make sense to keep a non-zero number of fingerprints in ${opts.name}`; 91 | logger.warn(msg); 92 | return { target: p, success: false, error: new Error(msg) }; 93 | } 94 | return opts.terminator(p, pi); 95 | }; 96 | } 97 | 98 | function isReviewerRegistration(er: EligibleReviewer): er is ReviewerRegistration { 99 | const maybe = er as ReviewerRegistration; 100 | return !!maybe.inspection; 101 | } 102 | -------------------------------------------------------------------------------- /test/util/bands.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as assert from "assert"; 18 | import { 19 | BandCasing, 20 | bandFor, 21 | Bands, 22 | Default, 23 | } from "../../lib/util/bands"; 24 | 25 | describe("bands", () => { 26 | 27 | it("should use default band", () => { 28 | const bands: Bands<"speckled"> = { 29 | speckled: Default, 30 | }; 31 | assert.strictEqual(bandFor(bands, 25), "speckled"); 32 | }); 33 | 34 | it("should use exactly and default band", () => { 35 | const bands: Bands<"gotcha" | "speckled"> = { 36 | gotcha: { exactly: 0.5 }, 37 | speckled: Default, 38 | }; 39 | assert.strictEqual(bandFor(bands, 0.5), "gotcha"); 40 | assert.strictEqual(bandFor(bands, 25), "speckled"); 41 | }); 42 | 43 | it("should use exactly and upTo and default band", () => { 44 | const bands: Bands<"gotcha" | "low" | "speckled"> = { 45 | gotcha: { exactly: 0.5 }, 46 | low: { upTo: 25 }, 47 | speckled: Default, 48 | }; 49 | assert.strictEqual(bandFor(bands, 0), "low"); 50 | assert.strictEqual(bandFor(bands, 0.5), "gotcha"); 51 | assert.strictEqual(bandFor(bands, 11), "low"); 52 | assert.strictEqual(bandFor(bands, 16), "low"); 53 | assert.strictEqual(bandFor(bands, 25), "speckled"); 54 | }); 55 | 56 | it("should use exactly and upTo and default band with different order", () => { 57 | const bands: Bands<"gotcha" | "low" | "speckled" | "other"> = { 58 | gotcha: { exactly: 0.5 }, 59 | other: { upTo: 100 }, 60 | low: { upTo: 25 }, 61 | speckled: Default, 62 | }; 63 | assert.strictEqual(bandFor(bands, 0), "low"); 64 | assert.strictEqual(bandFor(bands, 0.5), "gotcha"); 65 | assert.strictEqual(bandFor(bands, 11), "low"); 66 | assert.strictEqual(bandFor(bands, 16), "low"); 67 | assert.strictEqual(bandFor(bands, 25), "other"); 68 | assert.strictEqual(bandFor(bands, 250), "speckled"); 69 | }); 70 | 71 | it("should include number", () => { 72 | const bands: Bands<"gotcha" | "speckled" | "low"> = { 73 | gotcha: { exactly: 0.5 }, 74 | low: { upTo: 1 }, 75 | speckled: Default, 76 | }; 77 | assert.strictEqual(bandFor(bands, 0.5, { includeNumber: true }), "gotcha (=0.5)"); 78 | assert.strictEqual(bandFor(bands, 0.6, { includeNumber: true }), "low (<1)"); 79 | assert.strictEqual(bandFor(bands, 25, { includeNumber: true }), "speckled"); 80 | }); 81 | 82 | it("should include number and correct case", () => { 83 | const bands: Bands<"gotcha" | "speckled" | "low"> = { 84 | gotcha: { exactly: 0.5 }, 85 | low: { upTo: 1 }, 86 | speckled: Default, 87 | }; 88 | assert.strictEqual(bandFor(bands, 0.5, { includeNumber: true, casing: BandCasing.Sentence }), "Gotcha (=0.5)"); 89 | assert.strictEqual(bandFor(bands, 0.6, { includeNumber: true }), "low (<1)"); 90 | assert.strictEqual(bandFor(bands, 25, { includeNumber: true, casing: BandCasing.Sentence }), "Speckled"); 91 | }); 92 | 93 | }); 94 | -------------------------------------------------------------------------------- /test/tree/pruneLeaves.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as assert from "assert"; 18 | import { 19 | SunburstTree, 20 | } from "../../lib/tree/sunburst"; 21 | import { pruneLeaves } from "../../lib/tree/treeUtils"; 22 | 23 | describe("pruneLeaves", () => { 24 | 25 | it("should prune empty tree", () => { 26 | const t1: SunburstTree = { 27 | name: "name", 28 | children: [], 29 | }; 30 | const pruned = pruneLeaves(t1, () => true); 31 | assert.deepStrictEqual(pruned, t1); 32 | }); 33 | 34 | it("should not prune tree with single node and no prune match", () => { 35 | const t1: SunburstTree = { 36 | name: "name", 37 | children: [ 38 | { 39 | name: "tony", 40 | size: 1, 41 | }, 42 | ], 43 | }; 44 | const pruned = pruneLeaves(t1, () => false); 45 | assert.deepStrictEqual(pruned, t1); 46 | }); 47 | 48 | it("should prune tree with single node and prune match", () => { 49 | const t1: SunburstTree = { 50 | name: "name", 51 | children: [ 52 | { 53 | name: "tony", 54 | size: 1, 55 | }, 56 | ], 57 | }; 58 | const pruned = pruneLeaves(t1, () => true); 59 | assert.deepStrictEqual(pruned, { name: "name", children: [] }); 60 | }); 61 | 62 | it("should prune tree with single node and single prune match", () => { 63 | const t1: SunburstTree = { 64 | name: "name", 65 | children: [ 66 | { 67 | name: "tony", 68 | size: 1, 69 | }, 70 | { 71 | name: "jeremy", 72 | size: 1, 73 | }, 74 | ], 75 | }; 76 | const pruned = pruneLeaves(t1, l => l.name === "jeremy"); 77 | assert.deepStrictEqual(pruned, { 78 | name: "name", children: [{ 79 | name: "tony", size: 1, 80 | }], 81 | }); 82 | }); 83 | 84 | it("should prune two level tree tree with single node and single prune match", () => { 85 | const t1: SunburstTree = { 86 | name: "name", 87 | children: [ 88 | { 89 | name: "leaders", 90 | children: [ 91 | { 92 | name: "tony", 93 | size: 1, 94 | }, 95 | { 96 | name: "jeremy", 97 | size: 1, 98 | }, 99 | ], 100 | }, 101 | ], 102 | }; 103 | const pruned = pruneLeaves(t1, l => l.name === "jeremy"); 104 | assert.deepStrictEqual(pruned, { 105 | name: "name", children: [{ 106 | name: "leaders", children: [{ 107 | name: "tony", size: 1, 108 | }], 109 | }], 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /lib/analysis/offline/spider/Spider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | logger, 19 | Project, 20 | } from "@atomist/automation-client"; 21 | import { Analyzed } from "../../../aspect/AspectRegistry"; 22 | import { ProjectAnalysisResult } from "../../ProjectAnalysisResult"; 23 | import { ProjectAnalysisResultStore } from "../persist/ProjectAnalysisResultStore"; 24 | import { SpideredRepo } from "../SpideredRepo"; 25 | import { ScmSearchCriteria } from "./ScmSearchCriteria"; 26 | 27 | import { Aspect } from "@atomist/sdm-pack-fingerprint"; 28 | import * as _ from "lodash"; 29 | import { 30 | AnalysisTracker, 31 | RepoBeingTracked, 32 | } from "../../tracking/analysisTracker"; 33 | 34 | export type ProjectAnalysisResultFilter = (pa: ProjectAnalysisResult) => Promise; 35 | 36 | /** 37 | * Options for spidering source code hosts 38 | */ 39 | export interface SpiderOptions { 40 | 41 | workspaceId: string; 42 | 43 | persister: ProjectAnalysisResultStore; 44 | 45 | poolSize: number; 46 | 47 | /** 48 | * Is this record OK or should it be refreshed? 49 | */ 50 | keepExistingPersisted: ProjectAnalysisResultFilter; 51 | } 52 | 53 | export type RepoUrl = string; 54 | 55 | export type PersistenceResult = string; // filename 56 | 57 | export interface SpiderFailure { 58 | repoUrl: string; 59 | whileTryingTo: string; 60 | message: string; 61 | error?: Error; 62 | } 63 | 64 | export interface SpiderResult { 65 | repositoriesDetected: number; 66 | failed: SpiderFailure[]; 67 | keptExisting: RepoUrl[]; 68 | persistedAnalyses: PersistenceResult[]; 69 | millisTaken?: number; 70 | } 71 | 72 | export const EmptySpiderResult: SpiderResult = { 73 | repositoriesDetected: 0, 74 | failed: [], 75 | keptExisting: [], 76 | persistedAnalyses: [], 77 | }; 78 | 79 | export interface Timing { 80 | totalMillis: number; 81 | extractions: number; 82 | } 83 | 84 | /** 85 | * Aspect type to total time taken to extract it 86 | */ 87 | export type TimeRecorder = Record; 88 | 89 | /** 90 | * Interface for types that can extract fingerprints from projects 91 | */ 92 | export interface Analyzer { 93 | 94 | analyze(p: Project, repoTracking: RepoBeingTracked): Promise; 95 | 96 | aspectOf(aspectName: string): Aspect | undefined; 97 | 98 | readonly timings: TimeRecorder; 99 | } 100 | 101 | export function logTimings(recorder: TimeRecorder): void { 102 | const timings: Array = Object.getOwnPropertyNames(recorder) 103 | .map(name => ({ 104 | name, 105 | ...recorder[name], 106 | })); 107 | const totalSeconds = _.sum(timings.map(t => t.totalMillis)) / 1000; 108 | const sorted = _.sortBy(timings, t => -t.totalMillis); 109 | logger.info("Aspect extraction total so far: %d seconds...", totalSeconds); 110 | logger.info("\t" + sorted.map(s => `${s.name}: ${s.totalMillis / 1000} seconds`).join("\n\t")); 111 | } 112 | 113 | /** 114 | * Spider a data source and progressively persist what we find. 115 | */ 116 | export interface Spider { 117 | 118 | spider(criteria: ScmSearchCriteria, 119 | analyzer: Analyzer, 120 | analysisTracking: AnalysisTracker, 121 | opts: SpiderOptions): Promise; 122 | } 123 | -------------------------------------------------------------------------------- /lib/scorer/scoring.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | RepositoryScorer, 19 | RepoToScore, 20 | ScoredRepo, 21 | TagAndScoreOptions, 22 | WorkspaceScorer, 23 | WorkspaceToScore, 24 | } from "../aspect/AspectRegistry"; 25 | import { fingerprintScoresFor } from "../aspect/score/ScoredAspect"; 26 | import { 27 | AlwaysIncludeCategory, 28 | FiveStar, 29 | Score, 30 | Scores, 31 | ScoreWeightings, 32 | weightedCompositeScore, 33 | WeightedScore, 34 | } from "./Score"; 35 | 36 | export async function scoreRepos(scorers: RepositoryScorer[], 37 | repos: RepoToScore[], 38 | weightings: ScoreWeightings, 39 | opts: TagAndScoreOptions): Promise { 40 | return Promise.all(repos.map(repo => scoreRepo(scorers, repo, weightings, opts))); 41 | } 42 | 43 | /** 44 | * Score the repo 45 | */ 46 | export async function scoreOrg(scorers: WorkspaceScorer[], 47 | od: WorkspaceToScore, 48 | weightings: ScoreWeightings): Promise { 49 | const scores: Scores = {}; 50 | for (const scorer of scorers) { 51 | scores[scorer.name] = { ...scorer, ...await scorer.score(od) }; 52 | } 53 | return weightedCompositeScore({ scores }, weightings); 54 | } 55 | 56 | export async function scoreRepo(scorers: RepositoryScorer[], 57 | repo: RepoToScore, 58 | weightings: ScoreWeightings, 59 | opts: TagAndScoreOptions): Promise { 60 | const scores = await fingerprintScoresFor(scorers, repo); 61 | // Remove scores that don't match our desired category 62 | for (const key of Object.keys(scores)) { 63 | const score = scores[key]; 64 | if (opts.category && score.category !== opts.category && opts.category !== AlwaysIncludeCategory) { 65 | delete scores[key]; 66 | } 67 | } 68 | return { 69 | ...repo, 70 | weightedScore: weightedCompositeScore({ scores }, weightings), 71 | }; 72 | } 73 | 74 | /** 75 | * Score the given object in the given context 76 | * @param scoreFunctions scoring functions. Undefined returns will be ignored 77 | * @param {T} toScore what to score 78 | * @param {CONTEXT} context 79 | * @return {Promise} 80 | */ 81 | async function scoresFor(scoreFunctions: Array<(t: T, c: CONTEXT) => Promise>, 82 | toScore: T, 83 | context: CONTEXT): Promise { 84 | const scores: Scores = {}; 85 | const runFunctions = scoreFunctions.map(scorer => scorer(toScore, context).then(score => { 86 | if (score) { 87 | scores[score.name] = score; 88 | } 89 | })); 90 | await Promise.all(runFunctions); 91 | return scores; 92 | } 93 | 94 | /** 95 | * Adjust a score within FiveStar range. 96 | * If merits is negative, reduce 97 | * @param {number} merits 98 | * @param {FiveStar} startAt 99 | * @return {FiveStar} 100 | */ 101 | export function adjustBy(merits: number, startAt: FiveStar = 5): FiveStar { 102 | const score = startAt + merits; 103 | return Math.min(Math.max(score, 1), 5) as FiveStar; 104 | } 105 | -------------------------------------------------------------------------------- /views/repoList.tsx: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import * as React from "react"; 3 | import { SortOrder } from "../lib/routes/web-app/repositoryListPage"; 4 | import { Score, WeightedScore, Weighting } from "../lib/scorer/Score"; 5 | import { bandFor, Default } from "../lib/util/bands"; 6 | import { collapsible } from "./utils"; 7 | 8 | export interface RepoForDisplay { 9 | url: string; 10 | repo: string; 11 | owner: string; 12 | id: string; 13 | score?: number; 14 | 15 | /** 16 | * Whether to show the full path of the repo 17 | */ 18 | showFullPath?: boolean; 19 | } 20 | 21 | export interface RepoListProps { 22 | orgScore: WeightedScore; 23 | repos: RepoForDisplay[]; 24 | virtualProjectCount: number; 25 | sortOrder: SortOrder; 26 | byOrg: boolean; 27 | expand: boolean; 28 | category: "*" | string; 29 | } 30 | 31 | function toRepoListItem(category: string, rfd: RepoForDisplay): React.ReactElement { 32 | let linkToIndividualProjectPage = `/repository?id=${encodeURI(rfd.id)}`; 33 | if (category && category !== "*") { 34 | linkToIndividualProjectPage += `&category=${category}`; 35 | } 36 | return
  • {rfd.showFullPath && `${rfd.owner} / `}{rfd.repo} {rfd.score && `(${rfd.score.toFixed(2)})`}:{" "} 37 | 38 | Source 39 | {" "} 40 | 41 | Insights 42 | 43 |
  • ; 44 | } 45 | 46 | function displayProjects(owner: string, 47 | repos: RepoForDisplay[], 48 | props: RepoListProps): React.ReactElement { 49 | const sorted = _.sortBy(repos, 50 | p => props.sortOrder === "score" ? 51 | p.score : 52 | p.repo.toLowerCase()); 53 | return collapsible(owner, `${owner} (${repos.length} repositories)`, 54 |
      55 | {sorted.map(r => toRepoListItem(props.category, r))} 56 |
    , 57 | repos.length === 1 || props.expand, 58 | ); 59 | } 60 | 61 | export function RepoList(props: RepoListProps): React.ReactElement { 62 | const projectsByOrg = _.groupBy(props.repos, p => p.owner); 63 | const orgCount = Object.entries(projectsByOrg).length; 64 | const categoryDescription = props.category === "*" ? undefined : 65 |

    Scoring by category: {props.category}

    ; 66 | return
    67 |

    {orgCount} organizations: {" "} 68 | {props.repos.length} repositories, {" "} 69 | {props.virtualProjectCount} virtual projects, {" "} 70 | {props.orgScore.weightedScore.toFixed(2)} / 5

    71 | 72 |

    Workspace Summary

    73 | {Object.keys(props.orgScore.weightedScores).map(k => explainScore(props.orgScore.weightedScores[k]))} 74 | 75 |

    {categoryDescription || "Repositories"}

    76 | 77 | {props.byOrg ? reposByOrg(props) : reposRanked(props)} 78 |
    ; 79 | } 80 | 81 | export function explainScore(score: Score & { weighting: Weighting }): React.ReactElement { 82 | const conclusion = bandFor({ 83 | horrible: { upTo: 1 }, 84 | poor: { upTo: 2 }, 85 | disappointing: { upTo: 3 }, 86 | satisfactory: { upTo: 4 }, 87 | good: { upTo: 4.5 }, 88 | great: Default, 89 | }, score.score); 90 | return
  • {score.description || score.name} is {conclusion} at {score.score.toFixed(2)}: {_.lowerFirst(score.reason)}
  • ; 91 | } 92 | 93 | function reposByOrg(props: RepoListProps): React.ReactElement { 94 | const projectsByOrg = _.groupBy(props.repos, p => p.owner); 95 | return
      96 | {Object.entries(projectsByOrg).map(kv => displayProjects(kv[0], kv[1], props))} 97 |
    ; 98 | } 99 | 100 | function reposRanked(props: RepoListProps): React.ReactElement { 101 | return
      102 | {displayProjects("Ranked", props.repos, props)} 103 |
    ; 104 | } 105 | -------------------------------------------------------------------------------- /lib/aspect/community/license.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Aspect, fingerprintOf, FP, sha256 } from "@atomist/sdm-pack-fingerprint"; 18 | import { firstFileFound } from "../../util/fileUtils"; 19 | import { ContentClassifier } from "./ContentClassifier"; 20 | 21 | export const NoLicense = "None"; 22 | 23 | export const LicenseType = "license"; 24 | 25 | export function hasNoLicense(ld: LicenseData): boolean { 26 | return ld.classification === NoLicense; 27 | } 28 | 29 | export function isLicenseFingerprint(fp: FP): fp is FP { 30 | return fp.type === LicenseType && !!fp.data.classification; 31 | } 32 | 33 | export interface LicenseData { 34 | 35 | /** 36 | * Path to the license file 37 | */ 38 | path: string; 39 | 40 | /** 41 | * What we've classified the license as by parsing the license file. 42 | */ 43 | classification: string; 44 | 45 | } 46 | 47 | const defaultClassifier: ContentClassifier = content => content.trim().split("\n")[0].trim(); 48 | 49 | /** 50 | * License aspect. Every repository gets a license fingerprint, which may have unknown 51 | * as a license. 52 | * @param opts provides classifier function, taking the license content and returning 53 | * a classificiation 54 | */ 55 | export function license(opts: { classifier: ContentClassifier } = 56 | { classifier: defaultClassifier }): Aspect { 57 | return { 58 | name: LicenseType, 59 | displayName: "License", 60 | baseOnly: true, 61 | extract: async p => { 62 | const licenseFile = await firstFileFound(p, "LICENSE", "LICENSE.txt", "license.txt", "LICENSE.md"); 63 | let classification: string = NoLicense; 64 | let content: string; 65 | if (!!licenseFile) { 66 | content = await licenseFile.getContent(); 67 | classification = opts.classifier(content); 68 | } 69 | const data: LicenseData = { classification, path: licenseFile ? licenseFile.path : undefined }; 70 | return fingerprintOf({ 71 | type: LicenseType, 72 | data, 73 | }); 74 | }, 75 | toDisplayableFingerprintName: () => "License", 76 | toDisplayableFingerprint: fp => { 77 | return fp.data.classification === NoLicense ? 78 | "None" : 79 | `${fp.data.path}:${fp.data.classification}`; 80 | }, 81 | stats: { 82 | defaultStatStatus: { 83 | entropy: false, 84 | }, 85 | }, 86 | }; 87 | } 88 | 89 | export const LicensePresenceType: string = "license-presence"; 90 | 91 | /** 92 | * Does this repository have a license? 93 | * Works with data from the license aspect. 94 | */ 95 | export const LicensePresence: Aspect<{ present: boolean }> = { 96 | name: LicensePresenceType, 97 | displayName: "License presence", 98 | extract: async () => [], 99 | consolidate: async fps => { 100 | const lfp = fps.find(isLicenseFingerprint); 101 | const present = !!lfp && !hasNoLicense(lfp.data); 102 | const data = { present }; 103 | return { 104 | name: LicensePresenceType, 105 | type: LicensePresenceType, 106 | data, 107 | sha: sha256(JSON.stringify(data)), 108 | }; 109 | }, 110 | toDisplayableFingerprint: fp => fp.data.present ? "Yes" : "No", 111 | }; 112 | -------------------------------------------------------------------------------- /lib/routes/auth.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | Configuration, 19 | configurationValue, 20 | logger, 21 | } from "@atomist/automation-client"; 22 | import { ApolloGraphClient } from "@atomist/automation-client/lib/graph/ApolloGraphClient"; 23 | import { isInLocalMode } from "@atomist/sdm-core"; 24 | import { CorsOptions } from "cors"; 25 | import * as exp from "express"; 26 | import * as _ from "lodash"; 27 | 28 | const PersonByIdentityQuery = `query PersonByIdentity { 29 | personByIdentity { 30 | team { 31 | id 32 | name 33 | } 34 | } 35 | } 36 | `; 37 | 38 | interface PersonByIdentity { 39 | personByIdentity: Array<{ team: { id: string, name: string } }>; 40 | } 41 | 42 | export function configureAuth(express: exp.Express): void { 43 | const authParser = require("express-auth-parser"); 44 | express.use(authParser); 45 | } 46 | 47 | export function corsHandler(): exp.RequestHandler { 48 | const cors = require("cors"); 49 | const origin = _.get(configurationValue(), "cors.origin", []); 50 | const corsOptions: CorsOptions = { 51 | origin, 52 | credentials: true, 53 | allowedHeaders: ["x-requested-with", "authorization", "content-type", "credential", "X-XSRF-TOKEN"], 54 | exposedHeaders: "*", 55 | }; 56 | return cors(corsOptions); 57 | } 58 | 59 | export function authHandlers(secure: boolean): exp.RequestHandler[] { 60 | // In local mode we don't need auth 61 | if (isInLocalMode() || !secure) { 62 | return [(req: exp.Request, res: exp.Response, next: exp.NextFunction) => next()]; 63 | } 64 | 65 | const cookieParser = require("cookie-parser"); 66 | return [cookieParser(), (req: exp.Request, res: exp.Response, next: exp.NextFunction) => { 67 | let creds: string; 68 | if (!!req.cookies && !!req.cookies.access_token) { 69 | creds = req.cookies.access_token; 70 | } else { 71 | creds = (req as any).authorization.credentials; 72 | } 73 | 74 | const workspaceId = req.params.workspace_id || req.query.workspace_id; 75 | if (!workspaceId) { 76 | next(); 77 | } else { 78 | // Creds are missing; just return 401 error here instead of calling the backend 79 | if (!creds) { 80 | res.sendStatus(401); 81 | } 82 | 83 | const graphClient = new ApolloGraphClient( 84 | configurationValue().endpoints.graphql.replace("/team", ""), 85 | { 86 | Authorization: `Bearer ${creds}`, 87 | }); 88 | 89 | graphClient.query({ query: PersonByIdentityQuery, variables: {} }) 90 | .then(result => { 91 | if (result.personByIdentity && result.personByIdentity.some(p => p.team && p.team.id === workspaceId)) { 92 | logger.info("Granting access to workspaceId '%s'", workspaceId); 93 | next(); 94 | } else { 95 | logger.info("Denying access to workspaceId '%s'", workspaceId); 96 | res.sendStatus(401); 97 | } 98 | }) 99 | .catch(err => { 100 | logger.warn("Error granting access to workspaceId '%s'", workspaceId); 101 | logger.warn(err); 102 | res.sendStatus(401); 103 | }); 104 | } 105 | }]; 106 | } 107 | -------------------------------------------------------------------------------- /lib/job/registerAspect.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | addressEvent, 19 | Configuration, 20 | guid, 21 | HandlerContext, 22 | } from "@atomist/automation-client"; 23 | import { WebSocketLifecycle } from "@atomist/automation-client/lib/internal/transport/websocket/WebSocketLifecycle"; 24 | import { AbstractWebSocketMessageClient } from "@atomist/automation-client/lib/internal/transport/websocket/WebSocketMessageClient"; 25 | import { 26 | SoftwareDeliveryMachine, 27 | StartupListener, 28 | } from "@atomist/sdm"; 29 | import { Aspect } from "@atomist/sdm-pack-fingerprint"; 30 | import * as cluster from "cluster"; 31 | import { hasReportDetails } from "../aspect/AspectReportDetailsRegistry"; 32 | import { 33 | AspectRegistrations, 34 | AspectRegistrationState, 35 | } from "../typings/types"; 36 | 37 | async function getAspectRegistrations(ctx: Pick, name?: string): Promise { 38 | const aspects = (await ctx.graphClient.query({ 39 | name: "AspectRegistrations", 40 | variables: { 41 | name: !!name ? [name] : undefined, 42 | }, 43 | })); 44 | return !!aspects ? aspects.AspectRegistration : []; 45 | } 46 | 47 | /** 48 | * Register aspect report details on start up 49 | */ 50 | export function registerAspects(sdm: SoftwareDeliveryMachine, 51 | allAspects: Aspect[]): StartupListener { 52 | return async () => { 53 | // Only run this on the cluster master if cluster mode is enabled 54 | if (sdm.configuration.cluster.enabled && !cluster.isMaster) { 55 | return; 56 | } 57 | 58 | const workspaceIds = sdm.configuration.workspaceIds; 59 | const aspects = allAspects.filter(hasReportDetails); 60 | 61 | for (const workspaceId of workspaceIds) { 62 | 63 | // Set up graphql and message clients 64 | const messageClient = new TriggeredMessageClient( 65 | (sdm.configuration.ws as any).lifecycle, 66 | workspaceId, 67 | sdm.configuration) as any; 68 | const graphClient = sdm.configuration.graphql.client.factory.create(workspaceId, sdm.configuration); 69 | 70 | const registeredAspects = await getAspectRegistrations({ graphClient }); 71 | 72 | for (const aspect of aspects) { 73 | const details = aspect.details; 74 | const registeredAspect = registeredAspects.find(a => a.name === aspect.name && a.owner === sdm.configuration.name); 75 | 76 | const aspectRegistration: AspectRegistrations.AspectRegistration = { 77 | name: aspect.name, 78 | owner: sdm.configuration.name, 79 | displayName: aspect.displayName, 80 | unit: details.unit, 81 | shortName: details.shortName, 82 | description: details.description, 83 | category: details.category, 84 | url: details.url, 85 | manageable: details.manage !== undefined ? details.manage : !!aspect.apply, 86 | state: !!registeredAspect && !!registeredAspect.state ? registeredAspect.state : AspectRegistrationState.Enabled, 87 | }; 88 | 89 | await messageClient.send(aspectRegistration, addressEvent("AspectRegistration")); 90 | } 91 | } 92 | }; 93 | } 94 | 95 | class TriggeredMessageClient extends AbstractWebSocketMessageClient { 96 | 97 | constructor(ws: WebSocketLifecycle, 98 | workspaceId: string, 99 | configuration: Configuration) { 100 | super(ws, {} as any, guid(), { id: workspaceId }, {} as any, configuration); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/routes/web-app/overviewPage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { logger } from "@atomist/automation-client"; 18 | import { 19 | Aspect, 20 | supportsEntropy, 21 | } from "@atomist/sdm-pack-fingerprint"; 22 | import * as _ from "lodash"; 23 | import { 24 | AspectFingerprintsForDisplay, 25 | FingerprintForDisplay, 26 | Overview, 27 | } from "../../../views/overview"; 28 | import { renderStaticReactNode } from "../../../views/topLevelPage"; 29 | import { ConnectionErrorHeading } from "../../analysis/offline/persist/pgClientFactory"; 30 | import { FingerprintUsage } from "../../analysis/offline/persist/ProjectAnalysisResultStore"; 31 | import { defaultedToDisplayableFingerprintName } from "../../aspect/DefaultAspectRegistry"; 32 | import { WebAppConfig } from "./webAppConfig"; 33 | 34 | export function exposeOverviewPage(conf: WebAppConfig, 35 | topLevelRoute: string): void { 36 | conf.express.get(topLevelRoute, ...conf.handlers, async (req, res, next) => { 37 | try { 38 | const repos = await conf.store.loadInWorkspace(req.query.workspace || req.params.workspace_id, false); 39 | const workspaceId = "local"; 40 | const fingerprintUsage = await conf.store.fingerprintUsageForType(workspaceId); 41 | 42 | const aspectsEligibleForDisplay = conf.aspectRegistry.aspects 43 | .filter(a => !!a.displayName) 44 | .filter(a => fingerprintUsage.some(fu => fu.type === a.name)); 45 | const foundAspects: AspectFingerprintsForDisplay[] = _.sortBy(aspectsEligibleForDisplay, a => a.displayName) 46 | .map(aspect => { 47 | const fingerprintsForThisAspect = fingerprintUsage.filter(fu => fu.type === aspect.name); 48 | return { 49 | aspect, 50 | fingerprints: fingerprintsForThisAspect 51 | .map(fp => formatFingerprintUsageForDisplay(aspect, fp)), 52 | }; 53 | }); 54 | 55 | const unfoundAspects: Aspect[] = conf.aspectRegistry.aspects 56 | .filter(f => !!f.displayName) 57 | .filter(f => !fingerprintUsage.some(fu => fu.type === f.name)); 58 | const virtualProjectCount = await conf.store.virtualProjectCount(workspaceId); 59 | 60 | res.send(renderStaticReactNode( 61 | Overview({ 62 | projectsAnalyzed: repos.length, 63 | foundAspects, 64 | unfoundAspects, 65 | repos: repos.map(r => ({ 66 | id: r.id, 67 | repo: r.repoRef.repo, 68 | owner: r.repoRef.owner, 69 | url: r.repoRef.url, 70 | })), 71 | virtualProjectCount, 72 | }), `Atomist Visualizer (${repos.length} repositories)`, 73 | conf.instanceMetadata)); 74 | } catch (e) { 75 | logger.error(e.stack); 76 | res.status(500).send(cleverlyExplainError(conf, e)); 77 | } 78 | }); 79 | } 80 | 81 | const ReadmeLink = "https://github.com/atomist/org-visualizer/#database-setup"; 82 | 83 | function cleverlyExplainError(conf: WebAppConfig, e: Error): string { 84 | if (e.message.includes(ConnectionErrorHeading) || e.message.includes("ENOCONNECT")) { 85 | return `This page cannot load without a database connection.
    86 | Please check the org-visualizer README for how to set up a database.

    87 | Error:
    ${e.message}`; 88 | } 89 | return `Failed to load page. Please check the log output of ${conf.instanceMetadata.name}`; 90 | } 91 | 92 | function formatFingerprintUsageForDisplay(aspect: Aspect, fp: FingerprintUsage): FingerprintForDisplay { 93 | return { 94 | ...fp, 95 | displayName: defaultedToDisplayableFingerprintName(aspect)(fp.name), 96 | entropy: supportsEntropy(aspect) ? fp.entropy : undefined, 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Atomist open source projects 2 | 3 | Have something you would like to contribute to this project? Awesome, 4 | and thanks for taking time to contribute! Here's what you need to 5 | know. 6 | 7 | ## Contributing code 8 | 9 | Is there an improvement to existing functionality or an entirely new 10 | feature you would like to see? Before creating enhancement 11 | suggestions, please check the issue list as you might find out that 12 | you don't need to create one. 13 | 14 | Did you know we have a [Slack community][slack]? This might be a 15 | great place to talk through your idea before starting. It allows you 16 | to see if anyone else is already working on something similar, having 17 | the same issue or to get feedback on your enhancement idea. 18 | Discussing things with the community first is likely to make the 19 | contribution process a better experience for yourself and those that 20 | are maintaining the projects. 21 | 22 | [slack]: https://join.atomist.com/ 23 | 24 | If you do not find an open issue related to your contribution and 25 | discussions in the Slack community are positive, the next thing to do 26 | is to create an issue in the appropriate GitHub repository. 27 | 28 | * Before we can accept any code changes into the Atomist codebase, 29 | we need to get some of the legal stuff covered. This is pretty 30 | standard for open-source projects. We are using 31 | [cla-assisant.io][cla-assistant] to track our Contributor License 32 | Agreement (CLA) signatures. If you have not signed a CLA for the 33 | repository to which you are contributing, you will be prompted to 34 | when you create a pull request (PR). 35 | * Be sure there is an open issue related to the contribution. 36 | * Code contributions should successfully build and pass tests. 37 | * Commit messages should follow the [standard format][commit] and 38 | should include a [reference][ref] to the open issue they are 39 | addressing. 40 | * All code contributions should be submitted via 41 | a [pull request (PR) from a forked GitHub repository][pr]. 42 | * Your PR will be reviewed by an Atomist developer. 43 | 44 | [cla-assistant]: https://cla-assistant.io/ 45 | [commit]: http://chris.beams.io/posts/git-commit/ 46 | [ref]: https://github.com/blog/957-introducing-issue-mentions 47 | [pr]: https://guides.github.com/activities/contributing-to-open-source/ 48 | 49 | ## Reporting problems 50 | 51 | Please go through the checklist below before reporting a 52 | problem. There's a chance it may have already been reported, or 53 | resolved. 54 | 55 | * Check if you can reproduce the problem in the latest version of 56 | the project. 57 | * Search the [atomist-community Slack][slack] community for common 58 | questions and problems. 59 | * Understand which repo the bug should be reported in. 60 | * Scan the list of issues to see if the problem has previously been 61 | reported. If so, you may add a comment to the existing issue 62 | rather than creating a new one. 63 | 64 | You went through the list above and it is still something you would 65 | like to report? Then, please provide us with as much of the context, 66 | by explaininig the problem and including any additional details that 67 | would help maintainers reproduce the problem. The more details you 68 | provide in the bug report, the better. 69 | 70 | Bugs are tracked as GitHub issues. After you've determined which 71 | repository your bug is related to, create an issue on that repository 72 | and provide as much information as possible. Feel free to use 73 | the bug report template below if you like. 74 | 75 | At a minimum include the following: 76 | 77 | * Where did you find the bug? For example, did you encounter the bug 78 | in chat, the CLI, somewhere else? 79 | * What version are you using? 80 | * What command were you using when it happened? (including 81 | parameters where applicable) 82 | 83 | ``` 84 | [Description of the problem] 85 | 86 | **How to Reproduce:** 87 | 88 | 1. [First Step] 89 | 2. [Second Step] 90 | 3. [n Step] 91 | 92 | **Expected behavior:** 93 | 94 | [Describe expected behavior here] 95 | 96 | **Observed behavior:** 97 | 98 | [Describe observed behavior here] 99 | 100 | **Screenshots and GIFs** 101 | 102 | ![Screenshots and GIFs which follow reproduction steps to demonstrate the problem](url) 103 | 104 | **Project version:** [Enter project version] 105 | **Atomist CLI version:** [Enter CLI version] 106 | ``` 107 | 108 | This project adheres to the Contributor Covenant [code of 109 | conduct][conduct]. By participating, you are expected to uphold this 110 | code. Please report unacceptable behavior to 111 | [code-of-conduct@atomist.com][email]. 112 | 113 | [conduct]: CODE_OF_CONDUCT.md 114 | [email]: mailto:code-of-conduct@atomist.com 115 | -------------------------------------------------------------------------------- /lib/analysis/offline/spider/local/LocalSpider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | GitCommandGitProject, 19 | GitProject, 20 | logger, 21 | RepoId, 22 | RepoRef, 23 | } from "@atomist/automation-client"; 24 | import { execPromise } from "@atomist/sdm"; 25 | import * as fs from "fs-extra"; 26 | import * as path from "path"; 27 | import { AnalysisTracker } from "../../../tracking/analysisTracker"; 28 | import { 29 | AnalysisRun, 30 | } from "../common"; 31 | import { ScmSearchCriteria } from "../ScmSearchCriteria"; 32 | import { 33 | Analyzer, 34 | Spider, 35 | SpiderOptions, 36 | SpiderResult, 37 | } from "../Spider"; 38 | 39 | export class LocalSpider implements Spider { 40 | 41 | public async spider(criteria: ScmSearchCriteria, 42 | analyzer: Analyzer, 43 | analysisTracking: AnalysisTracker, 44 | opts: SpiderOptions, 45 | ): Promise { 46 | 47 | const go = new AnalysisRun({ 48 | howToFindRepos: () => findRepositoriesUnder(this.localDirectory), 49 | determineRepoRef: repoRefFromLocalRepo, 50 | describeFoundRepo: f => ({ description: f.replace(this.localDirectory, "") }), 51 | howToClone: (rr, fr) => GitCommandGitProject.fromExistingDirectory(rr, fr) as Promise, 52 | analyzer, 53 | analysisTracking, 54 | persister: opts.persister, 55 | 56 | keepExistingPersisted: opts.keepExistingPersisted, 57 | projectFilter: criteria.projectTest, 58 | }, { 59 | workspaceId: opts.workspaceId, 60 | description: "local analysis under " + this.localDirectory, 61 | maxRepos: 1000, 62 | poolSize: opts.poolSize, 63 | }); 64 | 65 | return go.run(); 66 | } 67 | 68 | constructor(public readonly localDirectory: string) { 69 | } 70 | } 71 | 72 | async function* findRepositoriesUnder(dir: string): AsyncIterable { 73 | try { 74 | const stat = await fs.stat(await fs.realpath(dir)); 75 | if (!stat.isDirectory()) { 76 | // nothing interesting 77 | return; 78 | } 79 | } catch (err) { 80 | logger.error("Error opening " + dir + ": " + err.message); 81 | return; 82 | } 83 | 84 | const dirContents = await fs.readdir(dir); 85 | if (dirContents.includes(".git")) { 86 | // this is the repository you are looking for 87 | yield dir; 88 | return; 89 | } 90 | 91 | // recurse over everything inside 92 | for (const d of dirContents) { 93 | for await (const dd of findRepositoriesUnder(path.join(dir, d))) { 94 | yield dd; 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * @param repoDir full path to repository 101 | */ 102 | async function repoRefFromLocalRepo(repoDir: string): Promise { 103 | const repoId: RepoId = await execPromise("git", ["remote", "get-url", "origin"], { cwd: repoDir }) 104 | .then(execHappened => repoIdFromOriginUrl(execHappened.stdout)) 105 | .catch(() => inventRepoId(repoDir)); 106 | 107 | const sha = await execPromise("git", ["rev-parse", "HEAD"], { cwd: repoDir }) 108 | .then(execHappened => execHappened.stdout.trim()) 109 | .catch(() => "unknown"); 110 | 111 | return { 112 | ...repoId, 113 | sha, 114 | }; 115 | } 116 | 117 | function repoIdFromOriginUrl(originUrl: string): RepoId { 118 | const parse = /\/(?.+)\/(?.+)(.git)?$/.exec(originUrl); 119 | 120 | if (!parse) { 121 | throw new Error("Can't identify owner and repo in url: " + originUrl); 122 | } 123 | 124 | return { 125 | repo: parse.groups.repo, 126 | owner: parse.groups.owner, 127 | url: originUrl, 128 | }; 129 | } 130 | 131 | function inventRepoId(repoDir: string): RepoId { 132 | const { base, dir } = path.parse(repoDir); 133 | const repo = base; 134 | const owner = path.parse(dir).base || "pretendOwner"; 135 | 136 | return { 137 | repo, 138 | owner, 139 | url: "file://" + repoDir, 140 | }; 141 | } 142 | -------------------------------------------------------------------------------- /test/aspect/fileMatchAspect.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { InMemoryProject } from "@atomist/automation-client"; 18 | import { 19 | FileMatchData, 20 | } from "../../lib/aspect/compose/fileMatchAspect"; 21 | 22 | import * as assert from "assert"; 23 | 24 | import { microgrammar } from "@atomist/microgrammar"; 25 | import { FP } from "@atomist/sdm-pack-fingerprint"; 26 | import { microgrammarMatchAspect } from "../../lib/aspect/compose/microgrammarMatchAspect"; 27 | 28 | // tslint:disable 29 | 30 | describe("fileMatchAspect", () => { 31 | 32 | describe("microgrammarMatchAspect", () => { 33 | 34 | it("should not match in empty project", async () => { 35 | const p = InMemoryProject.of(); 36 | const aspect = microgrammarMatchAspect({ 37 | name: "foo", 38 | displayName: "foo", 39 | glob: "thing", 40 | grammar: microgrammar({ 41 | name: /.*/, 42 | }), 43 | path: "name", 44 | }); 45 | const r = await aspect.extract(p, undefined) as FP; 46 | assert(r !== undefined); 47 | assert.strictEqual(r.data.matches.length, 0, JSON.stringify(r)); 48 | }); 49 | 50 | it("should not match in file with no match", async () => { 51 | const p = InMemoryProject.of({ 52 | path: "thing", 53 | content: "whatever", 54 | }); 55 | const aspect = microgrammarMatchAspect({ 56 | name: "foo", 57 | displayName: "foo", 58 | glob: "thing", 59 | grammar: microgrammar({ 60 | name: "_", 61 | age: /[0-9]+/, 62 | end: "_", 63 | }), 64 | path: "age", 65 | }); 66 | const r = await aspect.extract(p, undefined) as FP; 67 | assert(r !== undefined); 68 | assert.strictEqual(r.data.matches.length, 0, JSON.stringify(r)); 69 | }); 70 | 71 | it("should match in file with match", async () => { 72 | const p = InMemoryProject.of({ 73 | path: "thing", 74 | content: "_25_", 75 | }); 76 | const aspect = microgrammarMatchAspect({ 77 | name: "foo", 78 | displayName: "foo", 79 | glob: "thing", 80 | grammar: microgrammar({ 81 | name: "_", 82 | age: /[0-9]+/, 83 | end: "_", 84 | }), 85 | path: "age", 86 | }); 87 | const r = await aspect.extract(p, undefined) as FP; 88 | assert.strictEqual(r.data.matches.length, 1); 89 | assert.strictEqual(r.data.matches[0].matchValue, "25"); 90 | }); 91 | 92 | it("should match real world example", async () => { 93 | const content = ` 94 | 95 | 96 | Exe 97 | netcoreapp2.2 98 | 99 | 100 | `; 101 | 102 | const grammar = microgrammar({ 103 | _open: //, 104 | targetFramework: /[a-zA-Z0-9_;/.]+/, 105 | _close: /<\/TargetFrameworks?>/, 106 | }); 107 | 108 | const parsed = grammar.findMatches(content); 109 | assert.strictEqual(parsed.length, 1); 110 | 111 | const p = InMemoryProject.of({ 112 | path: "thing.csproj", 113 | content, 114 | }); 115 | const aspect = microgrammarMatchAspect({ 116 | name: "foo", 117 | displayName: "foo", 118 | glob: "*.csproj", 119 | grammar, 120 | path: "targetFramework", 121 | }); 122 | const r = await aspect.extract(p, undefined) as FP; 123 | assert.strictEqual(r.data.matches.length, 1); 124 | assert.strictEqual(r.data.matches[0].matchValue, "netcoreapp2.2"); 125 | }); 126 | }); 127 | 128 | }); 129 | --------------------------------------------------------------------------------