Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Github Analyses support for Pull Requests #470

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ module.exports = {
"no-var": ERROR,
"prefer-const": ERROR,

"comma-dangle": [ERROR, {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "always-multiline", // Not includeded in the single-line config option.
}],
"eqeqeq": ERROR,
"filenames/match-regex": [ERROR, "^([a-z0-9]+)([A-Z][a-z0-9]+)*(\.(config|d|layouts|spec))?$"],
"header/header": [ERROR, "line", [
Expand Down
109 changes: 109 additions & 0 deletions src/extension/analysisProviderGithub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import fetch, { FetchError } from 'node-fetch';
import { authentication } from 'vscode';
import { outputChannel } from './outputChannel';

type LogInfo = { analysisId: number, uri: string, text: string };

// Subset of the GitHub API.
export interface AnalysisInfo {
id: number;
commit_sha: string;
created_at: string;
tool: { name: string };
results_count: number;
}

export class AnalysisProviderGithub {
constructor(readonly user: string, readonly repoName: string) {}

async fetchAnalysisInfos(
branch: string,
updateMessage: (message: string) => void)
: Promise<AnalysisInfo[] | undefined> {

try {
updateMessage('Checking GitHub Advanced Security...');

// STEP 1: Auth
const session = await authentication.getSession('github', ['security_events'], { createIfNone: true });
const { accessToken } = session;
if (!accessToken) {
updateMessage('Unable to authenticate.');
return undefined;
}

// STEP 2: Fetch
// Useful for debugging the progress indicator: await new Promise(resolve => setTimeout(resolve, 2000));
const analysesResponse = await fetch(`https://api.github.com/repos/${this.user}/${this.repoName}/code-scanning/analyses?ref=refs/heads/${branch}`, {
headers: {
authorization: `Bearer ${accessToken}`,
},
});

if (analysesResponse.status === 403) {
updateMessage('GitHub Advanced Security is not enabled for this repository.');
return undefined;
}

// STEP 3: Parse
const anyResponse = await analysesResponse.json();
if (anyResponse.message) {
// Sample message response:
// {
// "message": "You are not authorized to read code scanning alerts.",
// "documentation_url": "https://docs.github.com/rest/reference/code-scanning#list-code-scanning-analyses-for-a-repository"
// }
const messageResponse = anyResponse as { message: string, documentation_url: string };
updateMessage(messageResponse.message);
return undefined;
}

const analyses = anyResponse as AnalysisInfo[];

// Possibilities:
// a) analysis is not enabled for repo or branch.
// b) analysis is enabled, but pending first-ever run.
if (!analyses.length) {
updateMessage('Refresh to check for more current results.');
return undefined;
}
const analysesString = analyses.map(({ created_at, commit_sha, id, tool, results_count }) => `${created_at} ${commit_sha} ${id} ${tool.name} ${results_count}`).join('\n');
outputChannel.appendLine(`Analyses:\n${analysesString}\n`);

return analyses;
} catch (error) {
if (error instanceof FetchError) {
// Expected if the network is disabled.
// error.name: FetchError
// error.message: request to https://api.github.com/repos/microsoft/sarif-vscode-extension/code-scanning/analyses?ref=refs/heads/main failed, reason: getaddrinfo ENOTFOUND api.github.com
updateMessage('Network error. Refresh to try again.');
}
return undefined;
}
}

async fetchAnalysis(analysisInfoIds: number[]): Promise<LogInfo[]> {
const session = await authentication.getSession('github', ['security_events'], { createIfNone: true });
const { accessToken } = session; // Assume non-null as we already called it recently.

const logTexts = [] as LogInfo[];
for (const analysisId of analysisInfoIds) {
const uri = `https://api.github.com/repos/${this.user}/${this.repoName}/code-scanning/analyses/${analysisId}`;
const analysisResponse = await fetch(uri, {
headers: {
accept: 'application/sarif+json',
authorization: `Bearer ${accessToken}`,
},
});
logTexts.push({
analysisId,
uri,
text: await analysisResponse.text(),
});
}
return logTexts;
}
}
8 changes: 4 additions & 4 deletions src/extension/index.activateDecorations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function activateDecorations(disposables: Disposable[], store: Store) {
const activeResultId = observable.box<string | undefined>();

const decorationTypeCallout = window.createTextEditorDecorationType({
after: { color: new ThemeColor('problemsWarningIcon.foreground') }
after: { color: new ThemeColor('problemsWarningIcon.foreground') },
});
const decorationTypeHighlight = window.createTextEditorDecorationType({
border: '1px',
Expand All @@ -45,7 +45,7 @@ export function activateDecorations(disposables: Disposable[], store: Store) {
// then manually figuring with diagnostics are at the caret. However `languages.registerCodeActionsProvider`
// provides the diagnostics for free. The only odd part is that we always return [] when `provideCodeActions` is called.
return [];
}
},
}));

// Update decorations on:
Expand Down Expand Up @@ -78,7 +78,7 @@ export function activateDecorations(disposables: Disposable[], store: Store) {
return docUriString === artifactUriString;
});

const originalDoc = await getOriginalDoc(store.analysisInfo?.commit_sha, currentDoc);
const originalDoc = await getOriginalDoc(store.analysisInfos?.commit_sha, currentDoc);
const diffBlocks = originalDoc ? diffChars(originalDoc.getText(), currentDoc.getText()) : [];
const ranges = locationsInDoc.map(tfl => driftedRegionToSelection(diffBlocks, currentDoc, tfl.location?.physicalLocation?.region, originalDoc));
editor.setDecorations(decorationTypeHighlight, ranges);
Expand All @@ -101,7 +101,7 @@ export function activateDecorations(disposables: Disposable[], store: Store) {
const decorCallouts = rangesEnd.map((range, i) => ({
range,
hoverMessage: messages[i],
renderOptions: { after: { contentText: ` ${'┄'.repeat(maxRangeEnd - rangesEndAdj[i])} ${messages[i]}`, } }, // ←
renderOptions: { after: { contentText: ` ${'┄'.repeat(maxRangeEnd - rangesEndAdj[i])} ${messages[i]}` } }, // ←
}));
editor.setDecorations(decorationTypeCallout, decorCallouts);
}
Expand Down
72 changes: 72 additions & 0 deletions src/extension/index.activateDiagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
/* eslint-disable filenames/match-regex */

import { diffChars } from 'diff';
import { observe } from 'mobx';
import { DiagnosticSeverity, Disposable, languages, TextDocument, workspace } from 'vscode';
import '../shared/extension';
import { getOriginalDoc } from './getOriginalDoc';
import { outputChannel } from './outputChannel';
import { driftedRegionToSelection } from './regionToSelection';
import { ResultDiagnostic } from './resultDiagnostic';
import { Store } from './store';
import { UriRebaser } from './uriRebaser';

export function activateDiagnostics(disposables: Disposable[], store: Store, baser: UriRebaser) {
const diagsAll = languages.createDiagnosticCollection('SARIF');
disposables.push(diagsAll);
const setDiags = async (doc: TextDocument) => {
// When the user opens a doc, VS Code commonly silently opens the associate `*.git`. We are not interested in these events.
if (doc.fileName.endsWith('.git')) return;
if (doc.uri.scheme === 'output') return; // Example "output:extension-output-MS-SarifVSCode.sarif-viewer-%231-Sarif%20Viewer"
if (doc.uri.scheme === 'vscode') return; // Example "vscode:scm/git/scm0/input?rootUri..."

const localUri = await (async () => {
if (doc.uri.scheme === 'sarif') {
return doc.uri.toString();
}
return await baser.translateLocalToArtifact(doc.uri.toString());
})();
const severities = {
error: DiagnosticSeverity.Error,
warning: DiagnosticSeverity.Warning,
} as Record<string, DiagnosticSeverity>;
const matchingResults = store.results
.filter(result => {
const artifactUri = result._uriContents ?? result._uri;
return artifactUri === localUri;
});

const workspaceUri = workspace.workspaceFolders?.[0]?.uri.toString() ?? 'file://';
outputChannel.appendLine(`updateDiags ${doc.uri.toString().replace(workspaceUri, '')}. ${matchingResults.length} Results.\n`);

if (!matchingResults.length) {
diagsAll.set(doc.uri, []);
return;
}

const currentDoc = doc; // Alias for juxtaposition.
const originalDoc = await getOriginalDoc(store.analysisInfos?.commit_sha, currentDoc);
const diffBlocks = originalDoc ? diffChars(originalDoc.getText(), currentDoc.getText()) : [];

const diags = matchingResults
.map(result => {
return new ResultDiagnostic(
driftedRegionToSelection(diffBlocks, currentDoc, result._region, originalDoc),
result._message ?? '—',
severities[result.level ?? ''] ?? DiagnosticSeverity.Information, // note, none, undefined.
result,
);
});

diagsAll.set(doc.uri, diags);
};
workspace.textDocuments.forEach(setDiags);
disposables.push(workspace.onDidOpenTextDocument(setDiags));
disposables.push(workspace.onDidCloseTextDocument(doc => diagsAll.delete(doc.uri))); // Spurious *.git deletes don't hurt.
disposables.push(workspace.onDidChangeTextDocument(({ document }) => setDiags(document))); // TODO: Consider updating the regions independently of the list of diagnostics.

const disposerStore = observe(store, 'results', () => workspace.textDocuments.forEach(setDiags));
disposables.push({ dispose: disposerStore });
}
8 changes: 4 additions & 4 deletions src/extension/index.activateFixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ResultDiagnostic } from './resultDiagnostic';
import { Store } from './store';
import { UriRebaser } from './uriRebaser';

export function activateFixes(disposables: Disposable[], store: Pick<Store, 'analysisInfo' | 'resultsFixed'>, baser: UriRebaser) {
export function activateFixes(disposables: Disposable[], store: Pick<Store, 'analysisInfos' | 'resultsFixed'>, baser: UriRebaser) {
disposables.push(languages.registerCodeActionsProvider('*',
{
provideCodeActions(_doc, _range, context) {
Expand Down Expand Up @@ -46,13 +46,13 @@ export function activateFixes(disposables: Disposable[], store: Pick<Store, 'ana
if (fix) {
const edit = new WorkspaceEdit();
for (const artifactChange of fix.artifactChanges) {
const [uri, _uriContents] = parseArtifactLocation(result, artifactChange.artifactLocation);
const [uri] = parseArtifactLocation(result, artifactChange.artifactLocation);
const artifactUri = uri;
if (!artifactUri) continue;

const localUri = await baser.translateArtifactToLocal(artifactUri);
const currentDoc = await workspace.openTextDocument(Uri.parse(localUri, true /* Why true? */));
const originalDoc = await getOriginalDoc(store.analysisInfo?.commit_sha, currentDoc);
const originalDoc = await getOriginalDoc(store.analysisInfos?.commit_sha, currentDoc);
const diffBlocks = originalDoc ? diffChars(originalDoc.getText(), currentDoc.getText()) : [];

for (const replacement of artifactChange.replacements) {
Expand All @@ -71,7 +71,7 @@ export function activateFixes(disposables: Disposable[], store: Pick<Store, 'ana
},
},
{
providedCodeActionKinds: [CodeActionKind.QuickFix]
providedCodeActionKinds: [CodeActionKind.QuickFix],
},
));
}
Expand Down
Loading