├── .vscode └── settings.json ├── components ├── Viewer.Success.png ├── assets │ ├── empty.png │ ├── vs-icon.png │ └── vscode-icon.png ├── Viewer.ZeroData.png ├── RunCard.scss ├── extension.ts ├── TooltipSpan.tsx ├── RunCard.renderPathCell.test.ts ├── RunStore.test.ts ├── Hi.tsx ├── try.tsx ├── SimpleList.tsx ├── RunCard.renderActionsCell.tsx ├── Snippet.scss ├── RunCard.renderCell.scss ├── Viewer.Types.ts ├── RunCard.TreeColumnSorting.ts ├── Viewer.scss ├── getRepoUri.ts ├── FilterBar.tsx ├── getRepositoryDetailsFromRemoteUrl.ts ├── PathCellDemo.tsx ├── RunCard.renderPathCell.tsx ├── RunCard.renderCell.tsx ├── RunCard.tsx ├── Viewer.tsx ├── Snippet.tsx └── RunStore.ts ├── babel.config.js ├── .gitignore ├── webpack.config.docs.js ├── webpack.config.js ├── docs-components ├── Shield.scss ├── Index.scss ├── Shield.tsx └── Index.tsx ├── jest.config.js ├── .github └── workflows │ └── publish.yaml ├── tsconfig.json ├── webpack.config.npm.js ├── webpack.config.common.js ├── LICENSE ├── package.json ├── index.tsx ├── SECURITY.md ├── README.md ├── resources └── sample.ts ├── index.html └── docs └── index.html /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "dist*": true, 4 | "packages": true 5 | } 6 | } -------------------------------------------------------------------------------- /components/Viewer.Success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/sarif-web-component/HEAD/components/Viewer.Success.png -------------------------------------------------------------------------------- /components/assets/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/sarif-web-component/HEAD/components/assets/empty.png -------------------------------------------------------------------------------- /components/assets/vs-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/sarif-web-component/HEAD/components/assets/vs-icon.png -------------------------------------------------------------------------------- /components/Viewer.ZeroData.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/sarif-web-component/HEAD/components/Viewer.ZeroData.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ '@babel/preset-env', { targets: { node: 'current' } } ], 4 | ], 5 | } 6 | -------------------------------------------------------------------------------- /components/assets/vscode-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/sarif-web-component/HEAD/components/assets/vscode-icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ~* 3 | 4 | node_modules 5 | package-lock.json 6 | resources 7 | !resources/sample.ts 8 | *.txt 9 | 10 | # Star to allow names like "dist-2106" 11 | dist* 12 | 13 | # A place to store local/private packages 14 | packages 15 | 16 | # A place to keep personal notes, etc 17 | ignore 18 | -------------------------------------------------------------------------------- /webpack.config.docs.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.config.common') 2 | 3 | module.exports = { 4 | ...common, 5 | mode: 'production', 6 | entry: { 7 | 'docs': './docs-components/Index.tsx', 8 | }, 9 | output: { 10 | path: __dirname, 11 | filename: '[name]/index.js', 12 | libraryTarget: 'umd', 13 | globalObject: 'this', 14 | }, 15 | externals: { 16 | 'react': 'React', 17 | 'react-dom': 'ReactDOM', 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const common = require('./webpack.config.common') 3 | 4 | module.exports = { 5 | ...common, 6 | mode: 'development', 7 | entry: './index.tsx', 8 | output: { 9 | path: path.join(__dirname, 'dist'), 10 | filename: 'index.js', 11 | }, 12 | devServer : { 13 | publicPath: '/dist', 14 | //https: true, 15 | //host: '0.0.0.0', // Necessary to server outside localhost 16 | stats: 'none', 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /docs-components/Shield.scss: -------------------------------------------------------------------------------- 1 | .shield { 2 | pointer-events: none; 3 | opacity: 0; 4 | transition: opacity 0.5s; 5 | &.shieldEnabled { opacity: 1; } 6 | position: fixed; 7 | top: 0; bottom: 0; 8 | left: 0; right: 0; 9 | background-color: hsla(0, 0%, 100%, 0.9); 10 | padding: 10%; 11 | 12 | .shieldInner { 13 | height: 100%; 14 | border: 2px dashed hsl(0, 0%, 80%); 15 | display: flex; 16 | justify-content: center; // Left/Right 17 | align-items: center; // Top/Bottom 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 10x perf improvement, see: 3 | // https://github.com/kulshekhar/ts-jest/issues/1044 4 | // Consider ts-jest isolatedModules for more perf. 5 | maxWorkers: 1, 6 | 7 | // testEnvironment: 'node', 8 | moduleNameMapper: { 9 | '\\.(png|s?css)$': 'identity-obj-proxy' 10 | }, 11 | transform: { 12 | '^.+\\.tsx?$': 'ts-jest', 13 | '^.+\\.js?$': 'babel-jest', 14 | }, 15 | transformIgnorePatterns: [ 16 | 'node_modules/(?!azure-devops-ui)/' 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | publish: 7 | runs-on: macos-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: '16' 13 | registry-url: 'https://registry.npmjs.org' 14 | - run: npm install 15 | - run: npx webpack --config ./webpack.config.npm.js 16 | - run: npm publish 17 | env: 18 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} 19 | -------------------------------------------------------------------------------- /components/RunCard.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | @import "azure-devops-ui/Core/_platformCommon.scss"; 5 | 6 | .swcRunTitle { 7 | display: flex; 8 | align-items: center; 9 | 10 | .bolt-pill { margin-left: 8px; } 11 | } 12 | 13 | .bolt-table-cell .bolt-dropdown-expandable { 14 | display: block; 15 | .bolt-button { 16 | width: 100%; 17 | } 18 | } 19 | 20 | .swcRunEmpty { 21 | color: $disabled-text; 22 | text-align: center; 23 | margin-bottom: 32px; 24 | } -------------------------------------------------------------------------------- /components/extension.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // At least one export needed otherwise: 5 | // Augmentations for the global scope can only be directly nested in external modules or ambient module declarations. 6 | export default true 7 | 8 | declare global { 9 | interface Array { 10 | sorted(sortf): Array 11 | } 12 | } 13 | 14 | Array.prototype.sorted = function(sortf) { 15 | const copy = this.slice() 16 | copy.sort(sortf) 17 | return copy 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", // './' prefix not necessary 4 | "declaration": true, 5 | "sourceMap": true, 6 | "noImplicitAny": false, 7 | "target": "ES6", 8 | "module" : "CommonJS", 9 | "moduleResolution": "Node", 10 | "jsx": "react", 11 | "downlevelIteration": true, 12 | "experimentalDecorators": true 13 | }, 14 | "exclude": [ 15 | "dist", // Avoids error: Cannot write file '/dist/components/FilterBar.d.ts' because it would overwrite input file. 16 | "index.tsx", // For webpack-dev-server only. Prevent index.d.ts being generated to /dist. 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /components/TooltipSpan.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as React from 'react' 5 | import {Tooltip, ITooltipProps} from 'azure-devops-ui/TooltipEx' 6 | 7 | // Bridges two common gaps in azure-devops-ui/TooltipEx/Tooltip: 8 | // - Tooltips with plain text. React.Children.only() rejects string. 9 | // - Tooltips with a child that does not support onMouseEnter/Leave. 10 | export class TooltipSpan extends React.Component { 11 | render() { 12 | return 13 | {this.props.children} 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /components/RunCard.renderPathCell.test.ts: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme' 2 | import * as Enzyme from 'enzyme' 3 | import * as Adapter from 'enzyme-adapter-react-16' 4 | import { renderPathCell } from './RunCard.renderPathCell' 5 | import { demoResults } from './PathCellDemo' 6 | 7 | Enzyme.configure({ adapter: new Adapter() }) 8 | 9 | function log(element: JSX.Element) { 10 | console.log( 11 | shallow(element) 12 | .debug({ ignoreProps: false, verbose: false }) 13 | .replace('[undefined]', '') 14 | ) 15 | } 16 | 17 | // WIP: Testing various inputs. 18 | test('renderPathCell', () => { 19 | for (const result of demoResults()) { 20 | log(renderPathCell(result)) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /components/RunStore.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import 'react-dom' 5 | jest.mock('react-dom') 6 | 7 | import { Run } from 'sarif' 8 | import { RunStore } from './RunStore' 9 | import { Viewer } from './Viewer' 10 | 11 | import { MobxFilter } from './FilterBar' 12 | jest.mock('./FilterBar') 13 | 14 | it('does not explode', () => { // Bare bones perf is 0.2s 15 | const run = { 16 | tool: { driver: { name: "Sample Tool" } }, 17 | results: [{ 18 | message: { text: 'Message 1' }, 19 | }], 20 | } as Run 21 | 22 | const runStore = new RunStore(run, 0, new MobxFilter()) 23 | }) 24 | 25 | it('handles multiple logs', () => { 26 | const viewer = new Viewer({}) 27 | 28 | }) -------------------------------------------------------------------------------- /webpack.config.npm.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.config.common') 2 | 3 | module.exports = { 4 | ...common, 5 | mode: 'production', 6 | entry: { 7 | 'dist': './components/Viewer.tsx', 8 | }, 9 | optimization: { 10 | // Blocking Snowpack import thus disabling `minimize`. 11 | minimize: false, 12 | }, 13 | output: { 14 | path: __dirname, 15 | filename: '[name]/index.js', 16 | libraryTarget: 'umd', 17 | globalObject: 'this', 18 | }, 19 | externals: { 20 | 'react': { 21 | commonjs: 'react', 22 | commonjs2: 'react', 23 | amd: 'React', 24 | root: 'React', 25 | }, 26 | 'react-dom': { 27 | commonjs: 'react-dom', 28 | commonjs2: 'react-dom', 29 | amd: 'ReactDOM', 30 | root: 'ReactDOM', 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /webpack.config.common.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | resolve: { 3 | extensions: ['.js', '.ts', '.tsx'] // .js is necessary for transitive imports 4 | }, 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.tsx?$/, 9 | use: 'ts-loader', 10 | exclude: /node_modules/ 11 | }, 12 | { 13 | test: /\.s?css$/, 14 | use: ['style-loader', 'css-loader', 'sass-loader'] 15 | }, 16 | { test: /\.png$/, use: 'url-loader' }, 17 | { test: /\.woff$/, use: 'url-loader' }, 18 | ] 19 | }, 20 | performance: { 21 | // azure-devops-ui is the majority of the payload 22 | // and is needed on boot (thus cannot be lazy loaded). 23 | maxAssetSize: 820 * 1024, 24 | maxEntrypointSize: 820 * 1024, 25 | }, 26 | stats: 'minimal', // If left on will disrupt `webpack --profile`. 27 | } 28 | -------------------------------------------------------------------------------- /components/Hi.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as React from 'react' 5 | import {FilterKeywordContext} from './Viewer' 6 | 7 | export class Hi extends React.Component { 8 | static contextType = FilterKeywordContext 9 | render() { 10 | let children = this.props.children 11 | if (!children) return null 12 | if (typeof children !== 'string') return children // Gracefully (and silently) fail if not a string. 13 | 14 | let term = this.context 15 | if (!term || term.length <= 1) return children 16 | 17 | term = term && term.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, "\\$&").replace(/\*/g, ".*") 18 | return children 19 | .split(new RegExp(`(${term.split(/\s+/).filter(part => part).join('|')})`, 'i')) 20 | .map((word, i) => i % 2 === 1 ? {word} : word) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs-components/Index.scss: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | height: 100%; 7 | margin: 0; 8 | font: 14px 'Segoe UI', sans-serif; 9 | 10 | main { 11 | width: 100%; 12 | } 13 | 14 | #app { 15 | height: 100%; 16 | display: flex; flex-direction: column; 17 | } 18 | } 19 | 20 | ::-webkit-scrollbar { 21 | height: 8px; 22 | width: 8px; 23 | } 24 | ::-webkit-scrollbar-thumb { 25 | background: hsla(0, 0, 0, 0.1); 26 | &:hover { background: hsla(0, 0, 0, 0.2); } 27 | } 28 | 29 | pre { margin: 0 } 30 | 31 | // Matching GitHub style. 32 | code { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; } 33 | 34 | button, input { font: inherit; } // Override user agent. 35 | 36 | .demoHeader { 37 | flex: 0 0 auto; 38 | padding: 15px; padding-bottom: 10px; 39 | display: flex; 40 | border-bottom: 1px solid #EFEFEF; // Match policy headers. 41 | } 42 | -------------------------------------------------------------------------------- /components/try.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as React from 'react' 5 | import {Link} from 'azure-devops-ui/Link' 6 | 7 | export const tryLink = (fHref: () => string, inner: string | JSX.Element, className?: string, onClick?: (event: React.MouseEvent) => void) => { 8 | try { 9 | const href = fHref() 10 | if (!href) throw null 11 | return 17 | {inner} 18 | 19 | } 20 | catch (e) { return inner } 21 | } 22 | 23 | export function tryOr(...functions) { 24 | for (const func of functions) { 25 | if (typeof func !== 'function') return func as T // A non-function constant value. 26 | try { 27 | const value = func() 28 | if (!value) continue 29 | return value as T 30 | } 31 | catch (e) {} 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs-components/Shield.tsx: -------------------------------------------------------------------------------- 1 | import './Shield.scss' 2 | import {observable} from 'mobx' 3 | import {observer} from 'mobx-react' 4 | import * as React from 'react' 5 | 6 | @observer export default class Shield extends React.Component { 7 | @observable shielding = false 8 | componentDidMount() { 9 | addEventListener('dragover', e => { 10 | e.preventDefault() 11 | this.shielding = true 12 | }) 13 | addEventListener('dragleave', e => { 14 | this.shielding = false 15 | }) 16 | addEventListener('drop', async e => { 17 | e.preventDefault() 18 | this.shielding = false 19 | this.props.onDrop(e.dataTransfer.files[0]) 20 | }) 21 | } 22 | render() { 23 | return
24 |
Drop files here
25 |
26 | } 27 | } 28 | 29 | function o2c(o) { 30 | // Object --> css class names (string) 31 | // { a: true, b: false, c: 1 } --> 'a c' 32 | return Object.keys(o).filter(k => o[k]).join(' ') 33 | } 34 | -------------------------------------------------------------------------------- /components/SimpleList.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { autorun } from 'mobx' 5 | import { observer } from 'mobx-react' 6 | import * as React from 'react' 7 | import { Component } from 'react' 8 | 9 | import { ObservableArray } from 'azure-devops-ui/Core/Observable' 10 | import { List, ListItem, ListSelection } from 'azure-devops-ui/List' 11 | 12 | @observer export class SimpleList extends Component<{ items: T[] }> { 13 | private items = new ObservableArray([]) 14 | private selection = new ListSelection(true) 15 | 16 | private itemsDisposer = autorun(() => { 17 | this.items.value = this.props.items 18 | }) 19 | 20 | public render() { 21 | return 22 | itemProvider={this.items} 23 | selection={this.selection} 24 | renderRow={(index, item, details) => { 25 | return 26 |
{item}
27 |
28 | }} 29 | width="100%" 30 | /> 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/RunCard.renderActionsCell.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import './RunCard.renderCell.scss' 5 | 6 | import * as React from 'react' 7 | 8 | import { ActionProps } from './Viewer.Types'; 9 | import { Link } from 'azure-devops-ui/Link'; 10 | import { Result } from 'sarif' 11 | 12 | const emptyPng = require('./assets/empty.png') 13 | const vsCodePng = require('./assets/vscode-icon.png') 14 | const vsPng = require('./assets/vs-icon.png') 15 | 16 | const images = { 17 | empty: emptyPng, 18 | vscode: vsCodePng, 19 | vs: vsPng 20 | } 21 | 22 | function renderAction(props: ActionProps) { 23 | const { text, linkUrl, imageName, className } = props 24 | return 25 | {text} 26 | {text} 27 | 28 | } 29 | 30 | export function renderActionsCell(result: Result) { 31 | return result.actions?.map(actionProps =>
{renderAction(actionProps)}
); 32 | } 33 | -------------------------------------------------------------------------------- /components/Snippet.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | .swcSnippet { 5 | margin: 4px 0; // override user agent 6 | padding-right: 4px; // Override the 2px from _platformCommon.scss so the ClipboardButton looks right. 7 | font-size: 12px; 8 | overflow: hidden; 9 | position: relative; // For gradient 10 | display: flex; // For line number column. 11 | 12 | code { 13 | font-family: SFMono-Regular, monospace; 14 | } // Override UA default 'monospace'. 15 | 16 | &.clipped { 17 | &::before { 18 | content: ""; 19 | width: 100%; 20 | height: 108px; // 108px is a 6-line snippet which is very common. 21 | position: absolute; 22 | left: 0; 23 | top: 0; 24 | background: linear-gradient(transparent 75px, var(--palette-black-alpha-6)); 25 | } 26 | } 27 | 28 | .swcRegion { 29 | background-color: rgba(255, 230, 0, 0.5); 30 | } 31 | } 32 | 33 | code.lineNumber { 34 | width: 36px; 35 | color: hsl(0, 0%, 60%); 36 | flex: 0 0 auto; 37 | } 38 | 39 | .swcSnippet .hljs { 40 | color: inherit; 41 | display: initial; 42 | overflow-x: initial; 43 | padding: initial; 44 | background: initial; 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /components/RunCard.renderCell.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | .swcTree .bolt-list-cell { 5 | white-space: normal; // Override .bolt-list-cell { white-space: nowrap } 6 | } 7 | 8 | .swcMarkDown { 9 | li { 10 | list-style: unset; 11 | list-style-position: inside; 12 | } 13 | 14 | & > :first-child { 15 | margin-top: 0; 16 | } 17 | 18 | & > :last-child { 19 | margin-bottom: 0; 20 | } 21 | } 22 | 23 | .swcMarkDown + .swcSnippet { 24 | margin-top: 12px; // Normally 8. 25 | } 26 | 27 | .swcColorUnset { 28 | color: unset; 29 | } 30 | 31 | .swcWidth100 { 32 | width: 100%; 33 | } 34 | 35 | .swcRowRule { 36 | display: flex; 37 | flex-direction: row; 38 | align-items: center; 39 | .bolt-pill { margin-left: 8px; } 40 | } 41 | 42 | .midEllipsis { 43 | display: flex; 44 | white-space: nowrap; 45 | :first-child { 46 | overflow: hidden; 47 | text-overflow: ellipsis; 48 | } 49 | } 50 | 51 | .action { 52 | display: flex; 53 | } 54 | 55 | .action a { 56 | display: flex; 57 | flex-direction: row; 58 | justify-content: center; 59 | margin: 4px 0px; 60 | } 61 | 62 | .action img { 63 | height: 16px; 64 | width: 16px; 65 | margin: 0; 66 | margin-right: 10px; 67 | } 68 | -------------------------------------------------------------------------------- /components/Viewer.Types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import {Run, ReportingDescriptor, Result} from 'sarif' 5 | import {ITreeItem} from 'azure-devops-ui/Utilities/TreeItemProvider' 6 | 7 | declare module 'sarif' { 8 | interface Result { 9 | run: Run 10 | _rule: Rule // rule already used for ReportingDescriptorReference. 11 | firstDetection?: Date 12 | sla?: string, 13 | actions: ActionProps[], 14 | } 15 | } 16 | 17 | export interface Rule extends ReportingDescriptor { 18 | isRule: boolean // Artificial marker to determine type. 19 | results: Result[] 20 | treeItem: ITreeItem 21 | run: Run // For taxa. 22 | } 23 | 24 | export interface More { 25 | onClick: any 26 | } 27 | 28 | export interface ActionProps { 29 | text: string 30 | linkUrl: string 31 | imageName?: string 32 | className?: string 33 | } 34 | 35 | export interface RepositoryDetails { 36 | organizationName?: string 37 | projectName?: string 38 | repositoryName?: string 39 | errorMessage?: string 40 | } 41 | 42 | export type ResultOrRuleOrMore = Result | Rule | More 43 | 44 | declare module 'azure-devops-ui/Utilities/TreeItemProvider' { 45 | interface ITreeItem { 46 | childItemsAll?: ITreeItem[] 47 | isShowAll?: boolean 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@microsoft/sarif-web-component", 3 | "version": "0.6.0-29", 4 | "author": "Microsoft", 5 | "description": "SARIF Viewer", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Microsoft/sarif-web-component" 10 | }, 11 | "scripts": { 12 | "start": "webpack-dev-server --mode development", 13 | "test": "jest", 14 | "test:watch": "jest --watchAll", 15 | "npm": "webpack --config ./webpack.config.npm.js", 16 | "docs": "webpack --config ./webpack.config.docs.js" 17 | }, 18 | "devDependencies": { 19 | "@babel/preset-env": "^7.4.5", 20 | "@types/enzyme": "3.10.8", 21 | "@types/jest": "^27.0.4", 22 | "@types/react": "^16.8.18", 23 | "@types/react-dom": "^16.8.4", 24 | "@types/sarif": "^2.1.3", 25 | "autobind-decorator": "^2.4.0", 26 | "babel-jest": "^27.5.1", 27 | "css-loader": "^2.1.1", 28 | "enzyme": "^3.11.0", 29 | "enzyme-adapter-react-16": "^1.15.6", 30 | "identity-obj-proxy": "^3.0.0", 31 | "jest": "^27.0.4", 32 | "react": "^16.8.6", 33 | "react-dom": "^16.8.6", 34 | "sass-loader": "^10.1.1", 35 | "style-loader": "^0.23.1", 36 | "ts-jest": "^27.0.4", 37 | "ts-loader": "^8.1.0", 38 | "typescript": "^4.9.5", 39 | "url-loader": "^2.0.0", 40 | "webpack": "^4.46.0", 41 | "webpack-bundle-analyzer": "^3.3.2", 42 | "webpack-cli": "^3.3.12", 43 | "webpack-dev-server": "^3.4.1" 44 | }, 45 | "dependencies": { 46 | "azure-devops-ui": "2.167.22", 47 | "highlight.js": "^10.5.0", 48 | "mobx": "^5.9.4", 49 | "mobx-react": "^5.4.4", 50 | "mobx-utils": "^5.5.5", 51 | "react-linkify": "^1.0.0-alpha", 52 | "react-markdown": "^4.0.8", 53 | "sass": "^1.80.6", 54 | "uri-join": "^1.0.1", 55 | "vss-web-extension-sdk": "^5.141.0" 56 | }, 57 | "peerDependencies": { 58 | "react": "^16.8.6", 59 | "react-dom": "^16.8.6" 60 | }, 61 | "files": [ 62 | "dist/components", 63 | "dist/index.js" 64 | ], 65 | "main": "dist/index.js", 66 | "types": "dist/components/Viewer.d.ts" 67 | } 68 | -------------------------------------------------------------------------------- /components/RunCard.TreeColumnSorting.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import {cellFromEvent} from 'azure-devops-ui/List' 5 | import {IBehavior} from 'azure-devops-ui/Utilities/Behavior' 6 | import {IEventDispatch} from 'azure-devops-ui/Utilities/Dispatch' 7 | import {ITreeProps, ITree, Tree} from 'azure-devops-ui/TreeEx' 8 | import {KeyCode} from 'azure-devops-ui/Util' 9 | import {sortDelegate, SortOrder} from 'azure-devops-ui/Table' 10 | 11 | // Derived from azure-devops-ui ColumnSorting.ts 12 | export class TreeColumnSorting implements IBehavior, ITree> { 13 | private onSort: sortDelegate 14 | private props: Readonly> 15 | 16 | constructor(onSort: sortDelegate) { 17 | this.onSort = onSort 18 | } 19 | 20 | public initialize = (props: Readonly>, table: Tree, eventDispatch: IEventDispatch): void => { 21 | this.props = props 22 | 23 | eventDispatch.addEventListener("click", this.onClick) 24 | eventDispatch.addEventListener("keydown", this.onKeyDown) 25 | } 26 | 27 | private onClick = (event: React.MouseEvent) => { 28 | if (!event.defaultPrevented) { 29 | this.processSortEvent(event) 30 | } 31 | } 32 | 33 | private onKeyDown = (event: React.KeyboardEvent) => { 34 | if (!event.defaultPrevented) { 35 | if (event.which === KeyCode.enter || event.which === KeyCode.space) { 36 | this.processSortEvent(event) 37 | } 38 | } 39 | } 40 | 41 | private processSortEvent(event: React.KeyboardEvent | React.MouseEvent) { 42 | const clickedCell = cellFromEvent(event) 43 | 44 | if (clickedCell.rowIndex === -1) { 45 | const column = this.props.columns[clickedCell.cellIndex] 46 | 47 | // If the column is currently sorted ascending then we need to invert the sort. 48 | if (column && column.sortProps) { 49 | this.onSort( 50 | clickedCell.cellIndex, 51 | column.sortProps.sortOrder === SortOrder.ascending ? SortOrder.descending : SortOrder.ascending, 52 | event 53 | ) 54 | event.preventDefault() 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /components/Viewer.scss: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | @import "azure-devops-ui/Core/_platformCommon.scss"; 5 | 6 | .bolt-page { // Parent is #app which is display block. 7 | height: 100%; 8 | 9 | & > * { flex: 0 0 auto; } 10 | 11 | .bolt-messagecard { 12 | margin: 16px 32px 0; 13 | } 14 | 15 | .swcShim { 16 | height: 32px; 17 | position: sticky; 18 | z-index: 2; 19 | top: 0; 20 | background-color: $neutral-2; 21 | } 22 | 23 | .vss-FilterBar { 24 | position: sticky; 25 | z-index: 2; // Focused inputs/buttons are z=1. 26 | top: 29px; // 3px less than .svShim height so the shadow overlaps neatly. 27 | 28 | // `azure-devops-ui@1` defaulted to 0, now `azure-devops-ui@2` defaults this to 1, thus overriding. 29 | // This prevents the filter bar from squishing when there is (or is more than) a full page of results. 30 | flex-shrink: 0; 31 | } 32 | 33 | // Nested under .bolt-page for artificial precedence. 34 | .bolt-card-content { // bolt-card-content forced by bolt-card. 35 | flex-direction: column; // Override DevOps row. To allow ZeroData to center. 36 | } 37 | } 38 | 39 | .vss-ZeroData { 40 | min-height: 400px; // Mimicking pipelines page. 41 | margin-top: 0; // Override DevOps 35px, since we have min-height. 42 | .vss-ZeroDataItem { justify-content: center; } 43 | } 44 | 45 | // Used by both renderUniversalTreeCell and Snippet 46 | pre { 47 | background-color: var(--palette-black-alpha-6); 48 | border-radius: 2px; 49 | padding: 4px 8px 6px 8px; 50 | } 51 | 52 | .bolt-card { // Technically belongs in RunCard.tsx. 53 | // List-cells are also used in DropDowns, this only applies to cards. 54 | .bolt-list-cell { 55 | padding: 6px 8px; // Have all cells behave like .bolt-table-cell-content-with-inline-link 56 | } 57 | } 58 | 59 | .swcSplitter { 60 | width: 100%; 61 | &.vss-Splitter--container .vss-Splitter--divider:after { 62 | background-color: transparent; 63 | } 64 | 65 | .swcNearElement { 66 | margin-right: -24px; 67 | padding-bottom: 16px; 68 | } 69 | 70 | .swcFarElement { 71 | margin-left: -24px; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /docs-components/Index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.scss" 2 | import autobind from 'autobind-decorator' 3 | import {observable} from "mobx" 4 | import {observer} from "mobx-react" 5 | import * as React from 'react' 6 | import { Log } from 'sarif' 7 | 8 | import { Viewer } from '../components/Viewer' 9 | import Shield from './Shield' 10 | 11 | const demoLog = { 12 | version: "2.1.0", 13 | runs: [{ 14 | tool: { driver: { 15 | name: "Example Tool" }, 16 | }, 17 | results: [ 18 | { 19 | ruleId: 'Example Rule', 20 | level: 'error', 21 | locations: [{ 22 | physicalLocation: { artifactLocation: { uri: 'example.txt' } }, 23 | }], 24 | message: { text: 'Welcome to the online SARIF Viewer demo. Drag and drop a SARIF file here to view.' }, 25 | baselineState: 'new', 26 | }, 27 | ], 28 | }] 29 | } as Log 30 | 31 | // file is File/Blob 32 | const readAsText = file => new Promise((resolve, reject) => { 33 | let reader = new FileReader() 34 | reader.onload = () => resolve(reader.result as any) 35 | reader.onerror = reject 36 | reader.readAsText(file) 37 | }) 38 | 39 | @observer export class Index extends React.Component { 40 | @observable.ref sample = demoLog 41 | @autobind async loadFile(file) { 42 | if (!file) return 43 | if (!file.name.match(/.(json|sarif)$/i)) { 44 | alert('File name must end with ".json" or ".sarif"') 45 | return 46 | } 47 | this.sample = JSON.parse(await readAsText(file)) 48 | } 49 | render() { 50 | return <> 51 |
52 | SARIF Viewer 53 | 54 | { 56 | e.persist() 57 | this.loadFile(Array.from(e.target.files)[0]) 58 | }} /> 59 | (this.refs.inputFile as any).click() } />  60 |
61 | 66 | 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // Used solely for live development. 5 | import autobind from 'autobind-decorator' 6 | import { observable, runInAction } from 'mobx' 7 | import { observer } from 'mobx-react' 8 | import * as React from 'react' 9 | import * as ReactDOM from 'react-dom' 10 | import { Log } from 'sarif' 11 | import { Viewer } from './components/Viewer' 12 | import Shield from './docs-components/Shield' 13 | import sample from './resources/sample' 14 | 15 | // file is File/Blob 16 | const readAsText = file => new Promise((resolve, reject) => { 17 | let reader = new FileReader() 18 | reader.onload = () => resolve(reader.result as any) 19 | reader.onerror = reject 20 | reader.readAsText(file) 21 | }) 22 | 23 | @observer export class Index extends React.Component { 24 | @observable discussionId = undefined 25 | @observable.ref logs = undefined as Log[] 26 | 27 | @autobind async loadFile(file) { 28 | if (!file) return 29 | if (!file.name.match(/.(json|sarif)$/i)) { 30 | alert('File name must end with ".json" or ".sarif"') 31 | return 32 | } 33 | const text = await readAsText(file) 34 | 35 | // Use the following lines to cache the dropped log (in localstorage) between refreshes. 36 | localStorage.setItem('logName', file.name) 37 | localStorage.setItem('log', text) 38 | 39 | runInAction(() => { 40 | this.discussionId = file.name 41 | this.logs = [JSON.parse(text)] 42 | }) 43 | 44 | } 45 | 46 | render() { 47 | return <> 48 | 57 | 58 | 59 | } 60 | 61 | componentDidMount() { 62 | const logName = localStorage.getItem('logName') 63 | const log = localStorage.getItem('log') 64 | runInAction(() => { 65 | this.discussionId = logName ?? 'localhost.1' 66 | this.logs = log && [JSON.parse(log)] || sample 67 | }) 68 | } 69 | } 70 | 71 | ReactDOM.render( 72 | , 73 | document.getElementById("app") 74 | ) 75 | -------------------------------------------------------------------------------- /components/getRepoUri.ts: -------------------------------------------------------------------------------- 1 | import { Region, Run } from 'sarif' 2 | import * as urlJoin from 'uri-join' 3 | 4 | function getHostname(url: string | undefined): string | undefined { 5 | if (!url) return undefined 6 | try { 7 | return new URL(url).hostname 8 | } catch (_) { 9 | return undefined 10 | } 11 | } 12 | 13 | // TODO: Account for URI joins (normalizing slashes). 14 | export function getRepoUri(uri: string | undefined, run: Run, region?: Region | undefined): string | undefined { 15 | if (!uri) return undefined 16 | 17 | const versionControlDetails = run.versionControlProvenance?.[0] 18 | if (!versionControlDetails) return undefined // Required. 19 | 20 | const { repositoryUri, revisionId } = versionControlDetails 21 | const hostname = getHostname(repositoryUri) 22 | if (!hostname) return undefined // Required. 23 | 24 | if (hostname.endsWith('azure.com') || hostname?.endsWith('visualstudio.com')) { 25 | // Examples: 26 | // https://dev.azure.com/microsoft/sarif-web-component/_git/sarif-web-component?path=%2F.gitignore 27 | // https://dev.azure.com/microsoft/sarif-web-component/_git/sarif-web-component?path=%2F.gitignore&version=GCd14c42f18766159a7ef6fbb8858ab5ad4f0b532a 28 | let repoUri = revisionId 29 | ? `${repositoryUri}?path=${encodeURIComponent(uri)}&version=GC${revisionId}` 30 | : `${repositoryUri}?path=${encodeURIComponent(uri)}` 31 | if (region?.startLine) { // lines and columns are 1-based, so it is safe to to use simple truthy checks. 32 | // First three params required even in the most basic case (highlight a single line). 33 | // If there is no endColumn, we +1 the lineEnd to select the entire line. 34 | repoUri += `&line=${region!.startLine}` 35 | repoUri += `&lineEnd=${region!.endLine ?? (region!.startLine + (region!.endColumn ? 0 : 1))}` 36 | repoUri += `&lineStartColumn=${region!.startColumn ?? 1}` 37 | if (region?.endColumn) { 38 | repoUri += `&lineEndColumn=${region!.endColumn}` 39 | } 40 | } 41 | return repoUri 42 | } 43 | 44 | if (hostname.endsWith('github.com')) { 45 | // Examples: 46 | // https://github.com/microsoft/sarif-web-component/blob/main/.gitignore 47 | // https://github.com/microsoft/sarif-web-component/blob/d14c42f18766159a7ef6fbb8858ab5ad4f0b532a/.gitignore 48 | // https://github.com/microsoft/sarif-web-component/blob/d14c42f18766159a7ef6fbb8858ab5ad4f0b532a/.gitignore#L1 49 | // Note: path-browserify's path.join does does not preserve authority slashes 50 | // (ex: https://github.com becomes https:/github.com). Thus using url-join. 51 | let repoUri = urlJoin(`${repositoryUri}/blob/${revisionId ?? 'main'}`, uri) 52 | if (region?.startLine) { // `startLine` is 1-based. 53 | repoUri += `#L${region!.startLine}` 54 | } 55 | return repoUri 56 | } 57 | 58 | return undefined // Unsupported host. 59 | } 60 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![npm version](https://img.shields.io/npm/v/@microsoft/sarif-web-component.svg?style=flat)](https://www.npmjs.com/package/@microsoft/sarif-web-component) 3 | 4 | # SARIF Web Component 5 | 6 | A React-based component for viewing [SARIF](https://www.sarif.info) files. [Try it out](https://microsoft.github.io/sarif-web-component/). 7 | 8 | ## Usage 9 | 10 | ``` 11 | npm install @microsoft/sarif-web-component 12 | ``` 13 | 14 | ```js 15 | import * as React from 'react' 16 | import * as ReactDOM from 'react-dom' 17 | import {Viewer} from '@microsoft/sarif-web-component' 18 | 19 | ReactDOM.render(, document.body.firstChild) 20 | ``` 21 | In the HTML page hosting this component, `` is required to avoid text rendering issues. 22 | 23 | ## Publishing 24 | Update the package version. Run workflow `Publish`. Make sure Repository secret `NODE_AUTH_TOKEN` exists. 25 | 26 | ## Publishing (Manual) 27 | In your local clone of this repo, do the following. Double-check `package.json` `name` in case it was modified for development purposes. 28 | ``` 29 | git pull 30 | npm install 31 | npx webpack --config ./webpack.config.npm.js 32 | npm login 33 | npm publish 34 | ``` 35 | 36 | For a scoped non-paid accounts (such as for personal testing), publish would require: `npm publish --access public`. 37 | For a dry-run publish: `npm publish --dry-run`. Careful: the typo `--dryrun` results in a real publish. 38 | 39 | ## Publishing (Local/Private) 40 | As needed, run `git pull` and `npm install`. Then... 41 | ``` 42 | npx webpack --config ./webpack.config.npm.js 43 | npm pack 44 | ``` 45 | Our convention is to move/keep the tarballs in the `packages` directory. 46 | 47 | ## Bundle Size Analysis 48 | In `webpack.config.common.js` temporarily disable `stats: 'minimal'`. 49 | 50 | ``` 51 | npx webpack --profile --json > stats.json 52 | npx webpack-bundle-analyzer stats.json 53 | rm stats.json 54 | ``` 55 | 56 | ## Contributing 57 | 58 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 59 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 60 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 61 | 62 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 63 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 64 | provided by the bot. You will only need to do this once across all repos using our CLA. 65 | 66 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 67 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 68 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 69 | -------------------------------------------------------------------------------- /components/FilterBar.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { createAtom } from 'mobx' 5 | import { observer } from 'mobx-react' 6 | import * as React from 'react' 7 | 8 | import { DropdownFilterBarItem as AzDropdownFilterBarItem } from 'azure-devops-ui/Dropdown' 9 | import { FilterBar as AzFilterBar } from 'azure-devops-ui/FilterBar' 10 | import { KeywordFilterBarItem } from 'azure-devops-ui/TextFilterBarItem' 11 | import { DropdownMultiSelection } from 'azure-devops-ui/Utilities/DropdownSelection' 12 | import { Filter, FILTER_CHANGE_EVENT, IFilterState } from 'azure-devops-ui/Utilities/Filter' 13 | 14 | export const recommendedDefaultState = { 15 | Baseline: { value: ['new', 'unchanged', 'updated'] }, 16 | Suppression: { value: ['unsuppressed'] }, 17 | } 18 | 19 | export class MobxFilter extends Filter { 20 | private atom = createAtom('MobxFilter') 21 | constructor(defaultState?: IFilterState, startingState?: IFilterState) { 22 | super() 23 | this.setDefaultState(defaultState || recommendedDefaultState) 24 | this.setState(startingState || defaultState || recommendedDefaultState, true) 25 | this.subscribe(() => { 26 | this.atom.reportChanged() 27 | }, FILTER_CHANGE_EVENT) 28 | } 29 | getState() { 30 | this.atom.reportObserved() 31 | return super.getState() 32 | } 33 | } 34 | 35 | @observer export class FilterBar extends React.Component<{ filter: MobxFilter, readonly groupByAge: boolean, hideBaseline?: boolean, hideLevel?: boolean, showSuppression?: boolean, showAge?: boolean }> { 36 | private ms1 = new DropdownMultiSelection() 37 | private ms2 = new DropdownMultiSelection() 38 | private msSuppression = new DropdownMultiSelection() 39 | private msAge = new DropdownMultiSelection() 40 | 41 | render() { 42 | const {filter, groupByAge, hideBaseline, hideLevel, showSuppression, showAge} = this.props 43 | return 44 | 45 | {!hideBaseline && ({ id: text.toLowerCase(), text }))} 50 | selection={this.ms1} 51 | />} 52 | {!hideLevel && ({ id: text.toLowerCase(), text }))} 57 | selection={this.ms2} 58 | />} 59 | {showSuppression && ({ id: text.toLowerCase(), text }))} 64 | selection={this.msSuppression} 65 | />} 66 | {showAge && !groupByAge && ({ id: text.toLowerCase(), text }))} 71 | selection={this.msAge} 72 | />} 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /components/getRepositoryDetailsFromRemoteUrl.ts: -------------------------------------------------------------------------------- 1 | import { RepositoryDetails } from "./Viewer.Types"; 2 | 3 | // https://dev.azure.com/ OR https://org@dev.azure.com/ 4 | const AzureReposUrl = 'dev.azure.com/'; 5 | 6 | // git@ssh.dev.azure.com:v3/ 7 | const SSHAzureReposUrl = 'ssh.dev.azure.com:v3/'; 8 | 9 | // https://org.visualstudio.com/ 10 | const VSOUrl = '.visualstudio.com/'; 11 | 12 | // org@vs-ssh.visualstudio.com:v3/ 13 | const SSHVsoReposUrl = 'vs-ssh.visualstudio.com:v3/'; 14 | 15 | const failedToDetermineAzureRepoDetails: string = 'Failed to determine Azure Repo details from remote url. Please ensure that the remote points to a valid Azure Repos url.'; 16 | const notAzureRepoUrl: string = 'The repo isn\'t hosted with Azure Repos.'; 17 | 18 | export function isRepositoryDetailsComplete(repositoryDetails?: RepositoryDetails): boolean { 19 | return repositoryDetails 20 | && repositoryDetails.organizationName 21 | && repositoryDetails.projectName 22 | && repositoryDetails.repositoryName 23 | && !repositoryDetails.errorMessage; 24 | } 25 | 26 | export function getRepositoryDetailsFromRemoteUrl(remoteUrl: string): RepositoryDetails { 27 | if (remoteUrl.indexOf(AzureReposUrl) >= 0) { 28 | const part = remoteUrl.substring(remoteUrl.indexOf(AzureReposUrl) + AzureReposUrl.length); 29 | const parts = part.split('/'); 30 | if (parts.length !== 4) { 31 | return { errorMessage: failedToDetermineAzureRepoDetails }; 32 | } 33 | 34 | return { 35 | organizationName: parts[0].trim(), 36 | projectName: parts[1].trim(), 37 | repositoryName: parts[3].split('?')[0].trim() 38 | }; 39 | } else if (remoteUrl.indexOf(VSOUrl) >= 0) { 40 | const part = remoteUrl.substring(remoteUrl.indexOf(VSOUrl) + VSOUrl.length); 41 | const organizationName = remoteUrl.substring(remoteUrl.indexOf('https://') + 'https://'.length, remoteUrl.indexOf('.visualstudio.com')); 42 | const parts = part.split('/'); 43 | 44 | if (parts.length === 4 && parts[0].toLowerCase() === 'defaultcollection') { 45 | // Handle scenario where part is 'DefaultCollection//_git/' 46 | parts.shift(); 47 | } 48 | 49 | if (parts.length !== 3) { 50 | return { errorMessage: failedToDetermineAzureRepoDetails }; 51 | } 52 | 53 | return { 54 | organizationName: organizationName, 55 | projectName: parts[0].trim(), 56 | repositoryName: parts[2].split('?')[0].trim() 57 | }; 58 | } else if (remoteUrl.indexOf(SSHAzureReposUrl) >= 0 || remoteUrl.indexOf(SSHVsoReposUrl) >= 0) { 59 | const urlFormat = remoteUrl.indexOf(SSHAzureReposUrl) >= 0 ? SSHAzureReposUrl : SSHVsoReposUrl; 60 | const part = remoteUrl.substring(remoteUrl.indexOf(urlFormat) + urlFormat.length); 61 | const parts = part.split('/'); 62 | if (parts.length !== 3) { 63 | return { errorMessage: failedToDetermineAzureRepoDetails }; 64 | } 65 | 66 | return { 67 | organizationName: parts[0].trim(), 68 | projectName: parts[1].trim(), 69 | repositoryName: parts[2].split('?')[0].trim() 70 | }; 71 | } else { 72 | return { errorMessage: notAzureRepoUrl }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /components/PathCellDemo.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as React from 'react' 5 | import { Result, Run } from 'sarif' 6 | import './RunCard.renderCell.scss' 7 | import { renderPathCell } from './RunCard.renderPathCell' 8 | 9 | export function* demoResults() { 10 | const clone = (o: T) => JSON.parse(JSON.stringify(o)) as T 11 | 12 | const emptyResult: Result = { 13 | message: {}, 14 | run: {} as any, 15 | _rule: {} as any, 16 | actions: {} as any 17 | } 18 | 19 | { // fullyQualifiedName 20 | const result = clone(emptyResult) 21 | 22 | // no fullyQualifiedName 23 | yield clone(result) 24 | 25 | // fullyQualifiedName 26 | result.locations = [{ 27 | logicalLocations: [{ 28 | fullyQualifiedName: 'fullyQualifiedName' 29 | }] 30 | }] 31 | yield clone(result) 32 | 33 | // fullyQualifiedName + uri 34 | result.locations[0].physicalLocation = { 35 | artifactLocation: { 36 | uri: 'https://example.com/folder/file1.txt' 37 | } 38 | } 39 | yield clone(result) 40 | 41 | // fullyQualifiedName + uri overshadowed by artifactDescription 42 | result.run = { 43 | artifacts: [{ 44 | description: { 45 | text: 'Artifact Description' 46 | } 47 | }] 48 | } as Run 49 | result.locations[0].physicalLocation.artifactLocation.index = 0 50 | yield clone(result) 51 | } 52 | 53 | 54 | { // long uri 55 | // = ellipsis and link 56 | const result: Result = clone(emptyResult) 57 | result.locations = [{ 58 | physicalLocation: { 59 | artifactLocation: { 60 | uri: 'https://example.com/folder1/folder2/folder3/file1.txt' 61 | } 62 | } 63 | }] 64 | yield clone(result) 65 | 66 | // long uri, but not url 67 | // = ellipsis and no link 68 | result.locations = [{ 69 | physicalLocation: { 70 | artifactLocation: { 71 | uri: '/folder1/folder2/folder3/folder4/folder5/file1.txt' 72 | } 73 | } 74 | }] 75 | yield clone(result) 76 | } 77 | 78 | // Artifact contents 79 | const resultWithContents: Result = clone(emptyResult) 80 | resultWithContents.run = { 81 | artifacts: [{ 82 | location: { 83 | uri: 'contents.txt' 84 | }, 85 | contents: { 86 | text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' 87 | } 88 | }] 89 | } as Run 90 | resultWithContents.locations = [{ 91 | physicalLocation: { 92 | artifactLocation: { 93 | index: 0 94 | } 95 | } 96 | }] 97 | yield clone(resultWithContents) 98 | 99 | // repositoryUri only 100 | // = no href 101 | const resultWithRepo: Result = clone(emptyResult) 102 | resultWithRepo.run = { 103 | versionControlProvenance: [{ 104 | repositoryUri: 'https://dev.azure.com/Office/Office/_git/Office', 105 | }] 106 | } as Run 107 | resultWithRepo.locations = [{ 108 | physicalLocation: { 109 | artifactLocation: { 110 | uri: '/folder/file1.txt', 111 | } 112 | } 113 | }] 114 | yield clone(resultWithRepo) 115 | 116 | // repositoryUri + uriBaseId 117 | // = href 118 | resultWithRepo.locations[0].physicalLocation.artifactLocation.uriBaseId = 'SCAN_ROOT' 119 | yield clone(resultWithRepo) 120 | 121 | // repositoryUri not Azure DevOps 122 | // = no href 123 | resultWithRepo.run = { 124 | versionControlProvenance: [{ 125 | repositoryUri: 'https://github.com', 126 | }] 127 | } as Run 128 | yield clone(resultWithRepo) 129 | } 130 | 131 | // TODO: Test highlighting. 132 | export function PathCellDemo() { 133 | const style = { 134 | border: '1px dotted black', 135 | margin: 8, 136 | padding: 8, 137 | width: 300, 138 | } 139 | return <> 140 | {[...demoResults()].map((result, i) =>
141 | {renderPathCell(result)} 142 |
)} 143 | 144 | } 145 | -------------------------------------------------------------------------------- /components/RunCard.renderPathCell.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import { Tooltip } from 'azure-devops-ui/TooltipEx' 5 | import * as React from 'react' 6 | import { Result } from 'sarif' 7 | import { getRepoUri } from './getRepoUri' 8 | import { Hi } from './Hi' 9 | import './RunCard.renderCell.scss' 10 | import { TooltipSpan } from './TooltipSpan' 11 | import { tryLink, tryOr } from './try' 12 | 13 | function isValidURL(url: string) { 14 | try { 15 | return !!new URL(url) 16 | } catch (error) { 17 | return false 18 | } 19 | } 20 | 21 | interface SimpleRegion { 22 | startLine?: number 23 | startColumn?: number 24 | endColumn?: number 25 | } 26 | 27 | function openInNewTab(fileName: string, text: string, region: SimpleRegion | undefined): void { 28 | const line = region?.startLine ?? 1 29 | const col = region?.startColumn ?? 1 30 | const length = (region?.endColumn ?? 1) - col 31 | const [_, pre, hi, post] = new RegExp(`((?:.*?\\n){${line - 1}}.{${col - 1}})(.{${length}})((?:.|\\n)*)`, 's').exec(text) 32 | 33 | const escape = unsafe => unsafe 34 | .replace(/&/g, "&") 35 | .replace(//g, ">") 37 | .replace(/"/g, """) 38 | .replace(/'/g, "'"); 39 | 40 | const {document} = window.open() 41 | document.title = fileName 42 | document.body.innerHTML = `
${escape(pre)}${escape(hi)}${escape(post)}
` 43 | setTimeout(() => document.body.querySelector('mark').scrollIntoView({ block: 'center' })) 44 | } 45 | 46 | // TODO: 47 | // Unify runArt vs resultArt. 48 | // Distinguish uri and text. 49 | export function renderPathCell(result: Result) { 50 | const ploc = result.locations?.[0]?.physicalLocation 51 | const resArtLoc 52 | = ploc?.artifactLocation 53 | ?? result.analysisTarget 54 | const runArt = result.run.artifacts?.[resArtLoc?.index ?? -1] 55 | const runArtLoc = runArt?.location 56 | const uri 57 | = resArtLoc?.description?.text 58 | ?? runArtLoc?.description?.text // vs runArt?.description?.text? 59 | ?? resArtLoc?.uri 60 | ?? runArtLoc?.uri // Commonly a relative URI. 61 | 62 | const [path, fileName] = (() => { 63 | if (!uri) return ['—'] 64 | const index = uri.lastIndexOf('/') 65 | return index >= 0 66 | ? [uri.slice(0, index), uri.slice(index + 1)] 67 | : [uri] 68 | })() 69 | const uriWithEllipsis = fileName // This is what ultimately gets displayed 70 | ? 71 | {path} 72 | /{fileName} 73 | 74 | : {uri ?? '—'} 75 | 76 | // Example of href scenario: 77 | // uri = src\Prototypes\README.md 78 | // href = https://org.visualstudio.com/project/_git/repo?path=%2Fsrc%2FPrototypes%2FREADME.md&_a=preview 79 | const href = resArtLoc?.properties?.['href'] 80 | 81 | const runArtContentsText = runArt?.contents?.text 82 | const repoUri = getRepoUri(uri, result.run, ploc?.region) ?? uri 83 | 84 | const getHref = () => { 85 | if (uri?.endsWith('.dll')) return undefined 86 | if (href) return href 87 | if (runArtContentsText) return '#' 88 | if (!isValidURL(repoUri)) return undefined // uri as artDesc case takes this code path. 89 | return repoUri 90 | } 91 | 92 | const region = ploc?.region 93 | const onClick = event => { 94 | if (href) return // TODO: Test precedence. 95 | if (!runArtContentsText) return 96 | event.preventDefault() 97 | event.stopPropagation() 98 | openInNewTab(fileName, runArtContentsText, region) // BUG: if uri is "aaa", then fileName will be empty 99 | } 100 | 101 | const rowClasses = 'bolt-table-two-line-cell-item flex-row scroll-hidden' 102 | 103 | return tryOr( 104 | () =>
105 |
106 |
{/* Constraining width to 100% to play well with the Tooltip. */} 107 | 108 |
{result.locations[0].logicalLocations[0].fullyQualifiedName}
109 |
110 |
111 |
112 | {tryOr(() => { 113 | if (!uri) throw undefined 114 | return
115 | {/* TODO: Enable tooltip if a) inner !== href, or b) inner === href and inner is clipped (aka overflowing) */} 116 | 117 | {tryLink( 118 | getHref, 119 | uriWithEllipsis, 120 | 'fontSize font-size secondary-text swcColorUnset swcWidth100' /* Override .bolt-list-cell */, 121 | onClick)} 122 | 123 |
124 | })} 125 |
, 126 | () =>
{/* From Advanced table demo. */} 127 | {/* Since we don't know when the ellipsis text is in effect, thus TooltipSpan.overflowOnly=false/default */} 128 | {/* Consider overflowOnly=false for the other branch above. */} 129 | 130 | {tryLink( 131 | getHref, 132 | uriWithEllipsis, 133 | 'swcColorUnset', 134 | onClick)} 135 | 136 |
137 | ) 138 | } 139 | -------------------------------------------------------------------------------- /resources/sample.ts: -------------------------------------------------------------------------------- 1 | import {Log, Run, ReportingDescriptor, Result} from 'sarif' 2 | 3 | export default [{ 4 | version: "2.1.0", 5 | runs: [{ 6 | tool: { 7 | driver: { 8 | name: "Sample Tool", 9 | rules: [ 10 | { 11 | id: 'RULE01', 12 | name: 'Rule 1 Name', 13 | helpUri: 'https://github.com/Microsoft/sarif-sdk', 14 | fullDescription: { text: 'Full description for RuleId 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' }, 15 | relationships: [ 16 | { 17 | target: { 18 | id: '??', 19 | index: 0, 20 | toolComponent: { index: 0 } 21 | } 22 | }, 23 | ], 24 | }, 25 | ] 26 | }, 27 | }, 28 | taxonomies: [ 29 | { 30 | taxa: [ 31 | { 32 | name: 'Taxon00', 33 | helpUri: 'Taxon00/help/uri', 34 | }, 35 | ] 36 | } 37 | ], 38 | versionControlProvenance: [ 39 | { 40 | repositoryUri: "https://dev.azure.com/Office/Office/_git/Office", 41 | revisionId: "da79c32c5a0dd2d08b1f411a6a2a1f752e215e73", 42 | branch: "master" 43 | } 44 | ], 45 | results: [ 46 | // versionControlProvenance 47 | { 48 | ruleId: 'RULE01', 49 | message: { text: 'a) Version Control Provenance.' }, 50 | locations: [{ 51 | physicalLocation: { artifactLocation: { uri: '/folder/file1.txt', uriBaseId: "SCAN_ROOT", } }, 52 | }], 53 | }, 54 | // { // Bare minimum 55 | // message: {}, 56 | // }, 57 | { 58 | ruleId: 'RULE01', 59 | message: { text: 'Message only. Keyword "location" for testing.' }, 60 | locations: [{ 61 | physicalLocation: { artifactLocation: { uri: 'folder/file3.txt' } }, 62 | }], 63 | baselineState: 'absent', 64 | workItemUris: ['http://sarifviewer.azurewebsites.net/'], 65 | }, 66 | { 67 | ruleId: 'RULE01/1', 68 | message: { text: 'Message with basic snippet.' }, 69 | locations: [{ 70 | physicalLocation: { 71 | artifactLocation: { uri: 'folder/file1.txt', properties: { href: 'http://example.com' } }, 72 | region: { 73 | snippet: { text: 'Region snippet text only abc\n'.repeat(10) }, 74 | }, 75 | } 76 | }], 77 | baselineState: 'new', 78 | level: 'error', 79 | }, 80 | { 81 | ruleId: 'RULE01', 82 | message: {}, 83 | locations: [{ 84 | physicalLocation: { 85 | artifactLocation: { uri: 'folder/file2.txt' }, 86 | region: { 87 | snippet: { text: 'Region snippet text only. No message. def' }, 88 | }, 89 | } 90 | }], 91 | baselineState: 'unchanged', 92 | level: 'note', 93 | }, 94 | { 95 | ruleId: 'RULE01', 96 | message: { text: 'Testing show all.' }, 97 | locations: [{ 98 | physicalLocation: { artifactLocation: { uri: 'folder/file4.txt' } }, 99 | }], 100 | baselineState: 'new', 101 | }, 102 | { 103 | ruleId: 'RULE01', 104 | message: { text: 'Empty circle for level: none, kind: (undefined).' }, 105 | locations: [{ 106 | physicalLocation: { artifactLocation: { uri: 'folder/file5.txt' } }, 107 | }], 108 | baselineState: 'new', 109 | level: 'none', 110 | }, 111 | { 112 | ruleId: 'RULE01', 113 | message: { text: 'Green check for level: none, kind: pass.' }, 114 | locations: [{ 115 | physicalLocation: { artifactLocation: { uri: 'folder/file5.txt' } }, 116 | }], 117 | baselineState: 'new', 118 | level: 'none', 119 | kind: 'pass', 120 | }, 121 | 122 | // Variation in Path. 123 | { 124 | ruleId: 'RULE02', 125 | message: { text: 'No path.' }, 126 | baselineState: 'updated', 127 | level: 'none', 128 | }, 129 | { 130 | ruleId: 'RULE02', 131 | message: { text: 'Only analysisTarget.' }, 132 | analysisTarget: { uri: 'analysisTarget' } 133 | }, 134 | { 135 | ruleId: 'RULE02', 136 | message: { 137 | markdown: "Sample [link](https://example.com). Fix any of the following:\n- Element does not have an alt attribute\n- aria-label attribute does not exist or is empty\n- aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty\n- Element has no title attribute or the title attribute is empty\n- Element's default semantics were not overridden with role=\"presentation\"\n- Element's default semantics were not overridden with role=\"none\".", 138 | text: 'Only analysisTarget.', 139 | }, 140 | analysisTarget: { uri: 'analysisTarget' } 141 | }, 142 | 143 | // Variations in Snippets. 144 | { 145 | ruleId: 'RULE03', 146 | message: { text: '1. Message with basic snippet and startLine' }, 147 | locations: [{ 148 | physicalLocation: { 149 | artifactLocation: { uri: 'folder/file.txt' }, 150 | region: { 151 | snippet: { text: 'Region snippet text only.' }, 152 | startLine: 100, 153 | }, 154 | } 155 | }] 156 | }, 157 | { 158 | ruleId: 'RULE03', 159 | message: { text: '2. Message with basic snippet and contextRegion' }, 160 | locations: [{ 161 | physicalLocation: { 162 | artifactLocation: { uri: 'folder/file.txt' }, 163 | region: { 164 | snippet: { text: 'Region snippet text only.' }, 165 | startLine: 100, 166 | }, 167 | contextRegion: { 168 | snippet: { text: 'aaa\nRegion snippet text only.\nbbb' }, 169 | startLine: 99, // Required. 170 | }, 171 | } 172 | }] 173 | }, 174 | 175 | // Variations in AXE. 176 | { 177 | ruleId: 'RULE04', 178 | message: { text: '1. AXE-ish location. Typical.' }, 179 | locations: [{ 180 | logicalLocation: { fullyQualifiedName: 'fullyQualifiedName' }, 181 | physicalLocation: { 182 | artifactLocation: { index: 0 }, // Link to... 183 | region: { 184 | snippet: { text: 'Region snippet text only' }, 185 | }, 186 | } 187 | }] 188 | }, 189 | { 190 | ruleId: 'RULE04', 191 | message: { text: '2. AXE-ish location. No artifact location.' }, 192 | locations: [{ 193 | logicalLocation: { fullyQualifiedName: 'fullyQualifiedName' }, 194 | physicalLocation: { 195 | region: { 196 | snippet: { text: 'Region snippet text only' }, 197 | }, 198 | } 199 | }] 200 | }, 201 | { 202 | ruleId: 'RULE04', 203 | message: { text: '1. AXE-ish location. Typical.' }, 204 | locations: [{ 205 | physicalLocation: { 206 | artifactLocation: { uri: 'fallback-missing-fullyQualifiedName' }, 207 | region: { 208 | snippet: { text: 'Region snippet text only' }, 209 | }, 210 | } 211 | }] 212 | }, 213 | ], 214 | artifacts: [ 215 | { 216 | location: { uri: 'indexed/artifact/uri' }, 217 | description: { text: 'Some really long text for indexed/artifact/uri' }, 218 | }, 219 | ], 220 | properties: { 221 | buildId: 123, 222 | artifactName: 'drop_build_build_sdl_analysis', 223 | filePath: '/build/antimalware/001/antimalware.sarif', 224 | } 225 | }], 226 | }, 227 | { 228 | version: "2.1.0", 229 | runs: [{ 230 | tool: { 231 | driver: { 232 | name: "Sample Tool 2" 233 | } 234 | }, 235 | results: [ 236 | { // Bare minimum 237 | message: {}, 238 | }, 239 | ], 240 | }, 241 | { 242 | tool: { 243 | driver: { 244 | name: "Sample Tool 3" 245 | } 246 | }, 247 | results: [ 248 | { // Bare minimum 249 | message: {}, 250 | }, 251 | ], 252 | }] 253 | }] as Log[] -------------------------------------------------------------------------------- /components/RunCard.renderCell.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import './RunCard.renderCell.scss' 5 | import * as React from 'react' 6 | import {Fragment} from 'react' 7 | import * as ReactMarkdown from 'react-markdown' 8 | import {Result} from 'sarif' 9 | 10 | import {Hi} from './Hi' 11 | import {tryOr, tryLink} from './try' 12 | import {Rule, More, ResultOrRuleOrMore} from './Viewer.Types' 13 | import {Snippet} from './Snippet' 14 | 15 | import {css} from 'azure-devops-ui/Util' 16 | import {Link} from 'azure-devops-ui/Link' 17 | import {ObservableLike} from 'azure-devops-ui/Core/Observable' 18 | import {Status, Statuses, StatusSize} from "azure-devops-ui/Status" 19 | import {PillSize, Pill} from 'azure-devops-ui/Pill' 20 | import {ISimpleTableCell, TableCell} from 'azure-devops-ui/Table' 21 | import {ExpandableTreeCell, ITreeColumn} from 'azure-devops-ui/TreeEx' 22 | import {ITreeItemEx, ITreeItem} from 'azure-devops-ui/Utilities/TreeItemProvider' 23 | import {Icon, IconSize} from 'azure-devops-ui/Icon' 24 | import { renderPathCell } from './RunCard.renderPathCell' 25 | import { renderActionsCell } from './RunCard.renderActionsCell' 26 | import { getRepoUri } from './getRepoUri' 27 | 28 | const colspan = 99 // No easy way to parameterize this, however extra does not hurt, so using an arbitrarily large value. 29 | 30 | export function renderCell( 31 | rowIndex: number, 32 | columnIndex: number, 33 | treeColumn: ITreeColumn, 34 | treeItem: ITreeItemEx): JSX.Element { 35 | 36 | const data = ObservableLike.getValue(treeItem.underlyingItem.data) 37 | const commonProps = { 38 | className: treeColumn.className, 39 | columnIndex, 40 | treeItem, 41 | treeColumn, 42 | } 43 | 44 | // ROW AGE 45 | const isAge = (item => item.isAge) as (item: any) => item is { name: string, treeItem: ITreeItem } 46 | if (isAge(data)) { 47 | const age = data 48 | return columnIndex === 0 49 | ? ExpandableTreeCell({ 50 | children:
{/* Div for flow layout. */} 51 | {age.name} 52 | {age.treeItem.childItemsAll.length} 53 |
, 54 | colspan, 55 | ...commonProps, 56 | }) 57 | : null 58 | } 59 | 60 | // ROW RULE 61 | const isRule = (item => item.isRule) as (item: any) => item is Rule 62 | if (isRule(data)) { 63 | const rule = data 64 | return columnIndex === 0 65 | ? ExpandableTreeCell({ 66 | children:
{/* Div for flow layout. */} 67 | {tryLink(() => rule.helpUri, {rule.id || rule.guid})} 68 | {tryOr(() => rule.name && <>: {rule.name})} 69 | {tryOr(() => rule.relationships.map((rel, i) => { 70 | const taxon = rule.run.taxonomies[rel.target.toolComponent.index].taxa[rel.target.index] 71 | return {i > 0 ? ',' : ''} {tryLink(() => taxon.helpUri, taxon.name)} 72 | }))} 73 | {rule.treeItem.childItemsAll.length} 74 |
, 75 | colspan, 76 | ...commonProps, 77 | }) 78 | : null 79 | } 80 | 81 | // ROW RESULT 82 | const capitalize = str => `${str[0].toUpperCase()}${str.slice(1)}` 83 | const isResult = (item => item.message !== undefined) as (item: any) => item is Result 84 | if (isResult(data)) { 85 | const result = data 86 | const status = { 87 | none: result.kind === 'pass' ? Statuses.Success : Statuses.Queued, 88 | note: Statuses.Information, 89 | error: Statuses.Failed, 90 | }[result.level] || Statuses.Warning 91 | return columnIndex === 0 92 | // ExpandableTreeCell (td div.bolt-table-cell-content.flex-row.flex-center TreeExpand children) 93 | // calls SimpleTableCell - adds an extra div 94 | // calls TableCell 95 | ? ExpandableTreeCell({ // As close to Table#TwoLineTableCell (which calls TableCell) as possible. 96 | children: <> 97 | 98 | {renderPathCell(result)} 99 | , 100 | ...commonProps, 101 | }) 102 | : TableCell({ // Don't want SimpleTableCell as it has flex row. 103 | children: (() => { 104 | const rule = result._rule 105 | switch (treeColumn.id) { 106 | case 'Actions': 107 | return <> {renderActionsCell(result)} 108 | case 'Details': 109 | const messageFromRule = result._rule?.messageStrings?.[result.message.id ?? -1] ?? result.message; 110 | const formattedMessage = format(messageFromRule.text || result.message?.text, result.message.arguments) ?? ''; 111 | const formattedMarkdown = format(messageFromRule.markdown || result.message?.markdown, result.message.arguments); // No '—', leave undefined if empty. 112 | return <> 113 | {formattedMarkdown 114 | ?
115 | {children} }} /> 117 |
// Div to cancel out containers display flex row. 118 | : {renderMessageWithEmbeddedLinks(result, formattedMessage)} || ''} 119 | 120 | 121 | case 'Rule': 122 | return <> 123 | {tryLink(() => rule.helpUri, {rule.id || rule.guid})} 124 | {tryOr(() => rule.name && <>: {rule.name})} 125 | 126 | case 'Baseline': 127 | return {result.baselineState && capitalize(result.baselineState) || 'New'} 128 | case 'Bug': 129 | return tryOr(() => 130 | 131 | ) 132 | case 'Age': 133 | return {result.sla} 134 | case 'FirstObserved': 135 | return {result.firstDetection.toLocaleDateString()} 136 | } 137 | })(), 138 | className: css(treeColumn.className, 'font-size'), 139 | columnIndex, 140 | }) 141 | } 142 | 143 | // ROW MORE 144 | const isMore = (item => item.onClick !== undefined) as (item: any) => item is More 145 | if (isMore(data)) { 146 | return columnIndex === 0 147 | ? ExpandableTreeCell({ 148 | children: Show All, 149 | colspan, 150 | ...commonProps 151 | }) 152 | : null 153 | } 154 | 155 | return null 156 | } 157 | 158 | // Replace [text](relatedIndex) with 159 | function renderMessageWithEmbeddedLinks(result: Result, message: string) { 160 | const rxLink = /\[([^\]]*)\]\(([^\)]+)\)/ // Matches [text](id). Similar to below, but with an extra grouping around the id part. 161 | return message.match(rxLink) 162 | ? message 163 | .split(/(\[[^\]]*\]\([^\)]+\))/g) 164 | .map((item, i) => { 165 | if (i % 2 === 0) return item 166 | const [_, text, id] = item.match(rxLink) 167 | 168 | const href = (() => { 169 | if (isNaN(id as any)) return id // `id` is a URI string 170 | 171 | // Else `id` is a number 172 | // TODO: search other location collections 173 | // RelatedLocations is typically [{ id: 1, ...}, { id: 2, ...}] 174 | const physicalLocation = result.relatedLocations?.find(location => location.id === +id)?.physicalLocation 175 | return getRepoUri(physicalLocation?.artifactLocation?.uri, result.run, physicalLocation?.region) 176 | })() 177 | 178 | return href 179 | ? {text} 180 | : text 181 | }) 182 | : message 183 | } 184 | 185 | // Borrowed from sarif-vscode-extension. 186 | function format(template: string | undefined, args?: string[]) { 187 | if (!template) return undefined; 188 | if (!args) return template; 189 | return template.replace(/{(\d+)}/g, (_, group) => args[group]); 190 | } 191 | -------------------------------------------------------------------------------- /components/RunCard.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import './RunCard.scss' 5 | import * as React from 'react' 6 | import {Component} from 'react' 7 | import {autorun, runInAction, observable, computed, untracked} from 'mobx' 8 | import {observer} from 'mobx-react' 9 | 10 | import {Hi} from './Hi' 11 | import {renderCell} from './RunCard.renderCell' 12 | import {More, ResultOrRuleOrMore} from './Viewer.Types' 13 | import {RunStore, SortRuleBy} from './RunStore' 14 | import {TreeColumnSorting} from './RunCard.TreeColumnSorting' 15 | import {tryOr} from './try' 16 | 17 | import {Card} from 'azure-devops-ui/Card' 18 | import {Observer} from 'azure-devops-ui/Observer' 19 | import {ObservableValue, IObservableValue} from 'azure-devops-ui/Core/Observable' 20 | import {IHeaderCommandBarItem} from 'azure-devops-ui/HeaderCommandBar' 21 | import {MenuItemType} from 'azure-devops-ui/Menu' 22 | import {Pill, PillSize} from "azure-devops-ui/Pill" 23 | import {Tree, ITreeColumn} from 'azure-devops-ui/TreeEx' 24 | import {TreeItemProvider, ITreeItemEx} from 'azure-devops-ui/Utilities/TreeItemProvider' 25 | import {Tooltip} from 'azure-devops-ui/TooltipEx' 26 | 27 | @observer export class RunCard extends Component<{ runStore: RunStore, index: number, runCount: number }> { 28 | @observable private show = true 29 | private groupByMenuItems = [] as IHeaderCommandBarItem[] 30 | private itemProvider = new TreeItemProvider([]) 31 | private columnCache = new Map>() 32 | 33 | @computed({ keepAlive: true }) private get sortRuleByMenuItems(): IHeaderCommandBarItem[] { 34 | const {runStore} = this.props 35 | const sortRuleBy = untracked(() => runStore.sortRuleBy) 36 | const onActivate = menuItem => { 37 | runStore.sortRuleBy = menuItem.data 38 | this.sortRuleByMenuItems.forEach(item => (item.checked as IObservableValue).value = item.id === menuItem.id) 39 | } 40 | return [ 41 | { 42 | data: SortRuleBy.Count, 43 | id: 'sortByRuleCount', 44 | text: 'Sort by rule count', 45 | ariaLabel: 'Sort by rule count', 46 | onActivate, 47 | important: false, 48 | checked: new ObservableValue(sortRuleBy === SortRuleBy.Count), 49 | }, 50 | { 51 | data: SortRuleBy.Name, 52 | id: 'sortByRuleName', 53 | text: 'Sort by rule name', 54 | ariaLabel: 'Sort by rule name', 55 | onActivate, 56 | important: false, 57 | checked: new ObservableValue(sortRuleBy === SortRuleBy.Name), 58 | }, 59 | ] 60 | } 61 | 62 | @computed private get columns() { 63 | const {runStore} = this.props 64 | return runStore.columns.map((col, i) => { 65 | const {id, width} = col 66 | if (!this.columnCache.has(id)) { 67 | const observableWidth = new ObservableValue(width) 68 | this.columnCache.set(id, { 69 | id: id.replace(/ /g, ''), 70 | name: id, 71 | width: observableWidth, 72 | onSize: (e, i, newWidth) => observableWidth.value = newWidth, 73 | renderCell: renderCell, // Normally renderTreeCell 74 | sortProps: { 75 | ariaLabelAscending: "Sorted A to Z", // Need to change for date values. 76 | ariaLabelDescending: "Sorted Z to A", 77 | sortOrder: i === runStore.sortColumnIndex ? runStore.sortOrder : undefined 78 | }, 79 | } as ITreeColumn) 80 | } 81 | return this.columnCache.get(id) 82 | }) 83 | } 84 | 85 | constructor(props) { 86 | super(props) 87 | const {runStore} = this.props 88 | 89 | if (runStore.showAge) { 90 | const onActivateGroupBy = menuItem => { 91 | runStore.groupByAge.set(menuItem.data) 92 | this.groupByMenuItems 93 | .filter(item => item.itemType !== MenuItemType.Divider) 94 | .forEach(item => (item.checked as IObservableValue).value = item.id === menuItem.id) 95 | } 96 | 97 | this.groupByMenuItems = [ 98 | { 99 | data: true, 100 | id: 'groupByAge', 101 | text: 'Group by age', 102 | ariaLabel: 'Group by age', 103 | onActivate: onActivateGroupBy, 104 | important: false, 105 | checked: new ObservableValue(runStore.groupByAge.get()), 106 | }, 107 | { 108 | data: false, 109 | id: 'groupByRule', 110 | text: 'Group by rule', 111 | ariaLabel: 'Group by rule', 112 | onActivate: onActivateGroupBy, 113 | important: false, 114 | checked: new ObservableValue(!runStore.groupByAge.get()), 115 | }, 116 | { id: "separator", important: false, itemType: MenuItemType.Divider }, 117 | ] 118 | } 119 | 120 | autorun(() => { 121 | this.itemProvider.clear() 122 | this.itemProvider.splice(undefined, undefined, [{ items: this.props.runStore.rulesTruncated }]) 123 | }) 124 | 125 | autorun(() => this.show = this.props.index === 0) 126 | } 127 | 128 | private sortingBehavior = new TreeColumnSorting>( 129 | (columnIndex, proposedSortOrder, event) => { 130 | for (let index = 0; index < this.columns.length; index++) { 131 | const column = this.columns[index] 132 | if (column.sortProps) { 133 | column.sortProps.sortOrder = index === columnIndex ? proposedSortOrder : undefined 134 | } 135 | } 136 | runInAction(() => { 137 | this.props.runStore.sortColumnIndex = columnIndex 138 | this.props.runStore.sortOrder = proposedSortOrder 139 | }) 140 | } 141 | ) 142 | 143 | render() { 144 | const {show, itemProvider} = this 145 | const {runStore, runCount} = this.props 146 | 147 | return 148 | {(observedProps: { itemProvider }) => { 149 | const qualityDomain = tryOr(() => runStore.run.tool.driver.properties['microsoft/qualityDomain']) 150 | return 155 |
{tryOr( 156 | () => runStore.run.tool.driver.fullName, 157 | () => `${runStore.run.tool.driver.name} ${runStore.run.tool.driver.semanticVersion || ''}`, 158 | )}
159 | {tryOr( 160 | () =>
{runStore.run.tool.driver.fullDescription.text}
, 161 | () =>
{runStore.run.tool.driver.shortDescription.text}
, 162 | )} 163 | as any}> 164 | 165 | {runStore.driverName}{qualityDomain && ` (${qualityDomain})`} 166 | {runStore.filteredCount} 167 | {/* Tooltip marked as React.Children.only thus extra span. */} 168 | as any 169 | }} 170 | contentProps={{ contentPadding: false }} 171 | headerCommandBarItems={[ 172 | runCount > 1 173 | ? { 174 | id: 'hide', 175 | text: '', // Remove? 176 | ariaLabel: 'Show/Hide', 177 | onActivate: () => this.show = !this.show, 178 | iconProps: { iconName: this.show ? 'ChevronDown' : 'ChevronUp' }, // Naturally updates as this entire object is re-created each render. 179 | important: runCount > 1 180 | } 181 | : undefined, 182 | ...this.groupByMenuItems, 183 | ...this.sortRuleByMenuItems, 184 | ].filter(item => item)} 185 | className="flex-grow bolt-card-no-vertical-padding"> 186 | {show && (itemProvider.length 187 | ? 188 | className="swcTree" 189 | columns={this.columns} 190 | itemProvider={itemProvider} 191 | onToggle={(event, treeItem: ITreeItemEx) => { 192 | itemProvider.toggle(treeItem.underlyingItem) 193 | }} 194 | onActivate={(event, treeRow) => { 195 | const treeItem = treeRow.data.underlyingItem 196 | const more = treeItem.data as More 197 | if (more.onClick) { 198 | more.onClick() // Handle "Show All" 199 | } else { 200 | itemProvider.toggle(treeItem) 201 | } 202 | }} 203 | behaviors={[this.sortingBehavior]} 204 | selectableText={true} 205 | /> 206 | :
No Results
207 | )} 208 |
209 | }} 210 |
211 | } 212 | } 213 | -------------------------------------------------------------------------------- /components/Viewer.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import './Viewer.scss' 5 | import * as React from 'react' 6 | import { Component } from 'react' 7 | import { computed, observable, autorun, IObservableValue } from 'mobx' 8 | import { observer } from 'mobx-react' 9 | import { computedFn } from 'mobx-utils' 10 | import { Log, Run } from 'sarif' 11 | 12 | import './extension' 13 | 14 | // Contexts must come before renderCell or anything the uses this. 15 | export const FilterKeywordContext = React.createContext('') 16 | 17 | import { FilterBar, MobxFilter, recommendedDefaultState } from './FilterBar' 18 | import { RunCard } from './RunCard' 19 | import { RunStore } from './RunStore' 20 | const successPng = require('./Viewer.Success.png') 21 | const noResultsPng = require('./Viewer.ZeroData.png') 22 | 23 | import { Card } from 'azure-devops-ui/Card' 24 | import { MessageCard, MessageCardSeverity } from "azure-devops-ui/MessageCard" 25 | import { Page } from 'azure-devops-ui/Page' 26 | import { SurfaceBackground, SurfaceContext } from 'azure-devops-ui/Surface' 27 | import { IFilterState } from 'azure-devops-ui/Utilities/Filter' 28 | import { ZeroData } from 'azure-devops-ui/ZeroData' 29 | import { ObservableValue } from 'azure-devops-ui/Core/Observable' 30 | 31 | interface ViewerProps { 32 | logs?: Log[] 33 | 34 | /** 35 | * Consider this the "initial" or "starting" state. This value is only applied once (during load). 36 | */ 37 | filterState?: IFilterState 38 | 39 | /** 40 | * The state applied when the user resets. If omitted, the default is: 41 | * ```javascript 42 | * { 43 | * Baseline: { value: ['new', 'unchanged', 'updated'] }, 44 | * Suppression: { value: ['unsuppressed'] }, 45 | * } 46 | * ``` 47 | */ 48 | defaultFilterState?: IFilterState 49 | 50 | user?: string 51 | hideBaseline?: boolean 52 | hideLevel?: boolean 53 | showSuppression?: boolean // If true, also defaults to Unsuppressed. 54 | showAge?: boolean // Enables age-related columns, group by age, and an age dropdown filter. 55 | showActions?: boolean 56 | 57 | /** 58 | * When there are zero errors¹, show this message instead of just "No Results". 59 | * Intended to communicate definitive positive confidence since "No Results" may be interpreted as inconclusive. 60 | * 61 | * Note¹: If the (starting) `filterState` shows... 62 | * * Only errors (and hides warnings), 63 | * then success is only communicated when there are zero errors (even if there exists warnings). 64 | * * Both errors and warnings, 65 | * then success is communicated only when there are zero errors *and* zero warnings. 66 | * * Neither errors nor warnings, 67 | * then the behavior is undefined. The current implementation will never communicate success. 68 | */ 69 | successMessage?: string 70 | } 71 | 72 | @observer export class Viewer extends Component { 73 | private collapseComments = new ObservableValue(false) 74 | private filter: MobxFilter 75 | private groupByAge: IObservableValue 76 | 77 | constructor(props) { 78 | super(props) 79 | const {defaultFilterState, filterState, showAge} = this.props 80 | this.filter = new MobxFilter(defaultFilterState, filterState) 81 | this.groupByAge = observable.box(showAge) 82 | } 83 | 84 | @observable warnOldVersion = false 85 | _warnOldVersion = autorun(() => { 86 | const {logs} = this.props 87 | this.warnOldVersion = logs?.some(log => log.version !== '2.1.0') 88 | }) 89 | 90 | private runStores = computedFn(logs => { 91 | const {hideBaseline, showAge, showActions} = this.props 92 | if (!logs) return [] // Undef interpreted as loading. 93 | const runs = [].concat(...logs.filter(log => log.version === '2.1.0').map(log => { log.runs.forEach((run, index) => { run._index = index }); return log.runs })) as Run[] 94 | const {filter, groupByAge} = this 95 | const runStores = runs.map((run, i) => new RunStore(run, i, filter, groupByAge, hideBaseline, showAge, showActions)) 96 | runStores.sort((a, b) => a.driverName.localeCompare(b.driverName)) // May not be required after introduction of runStoresSorted. 97 | return runStores 98 | }, { keepAlive: true }) 99 | 100 | @computed get runStoresSorted() { 101 | const {logs} = this.props 102 | return this.runStores(logs).slice().sorted((a, b) => b.filteredCount - a.filteredCount) // Highest count first. 103 | } 104 | 105 | render() { 106 | const {hideBaseline, hideLevel, showSuppression, showAge, successMessage} = this.props 107 | 108 | // Computed values fail to cache if called from onRenderNearElement() for unknown reasons. Thus call them in advance. 109 | const currentfilterState = this.filter.getState() 110 | const filterKeywords = currentfilterState.Keywords?.value 111 | const nearElement = (() => { 112 | const {runStoresSorted} = this 113 | if (!runStoresSorted.length) return null // Interpreted as loading. 114 | const filteredResultsCount = runStoresSorted.reduce((total, run) => total + run.filteredCount, 0) 115 | if (filteredResultsCount === 0) { 116 | 117 | const startingFilterState = this.props.filterState || recommendedDefaultState 118 | const startingFilterStateLevel: string[] = startingFilterState['Level']?.value ?? [] 119 | if (!startingFilterStateLevel.length) { 120 | startingFilterStateLevel.push('error', 'warning', 'note', 'none') // Normalize. 121 | } 122 | 123 | const currentfilterStateLevel: string[] = currentfilterState['Level']?.value ?? [] 124 | if (!currentfilterStateLevel.length) { 125 | currentfilterStateLevel.push('error', 'warning', 'note', 'none') // Normalize. 126 | } 127 | 128 | // Desired Behavior Matrix: 129 | // start curr 130 | // ew ew success (common) 131 | // ew e- noResult (there could still be warnings) 132 | // ew -w noResult (there could still be errors) 133 | // ew -- noResult (there could still be either) 134 | // e- ew success 135 | // e- e- success (common) 136 | // e- -w noResult (there could still be errors) 137 | // e- -- noResult (there could still be either) 138 | // -w ew success (uncommon) 139 | // -w e- noResult (there could still be warnings, uncommon) 140 | // -w -w success (uncommon) 141 | // -w -- noResult (there could still be either) 142 | // -- ** no scenario 143 | const showSuccess = successMessage 144 | && (!startingFilterStateLevel.includes('error') || currentfilterStateLevel.includes('error')) 145 | && (!startingFilterStateLevel.includes('warning') || currentfilterStateLevel.includes('warning')) 146 | 147 | if (showSuccess && !filterKeywords) { 148 | return
149 | 150 | 154 | 155 |
156 | } 157 | 158 | return
159 | 160 | 164 | 165 |
166 | } 167 | return runStoresSorted 168 | .filter(run => !filterKeywords || run.filteredCount) 169 | .map((run, index) =>
170 | 171 |
) 172 | })() as JSX.Element 173 | 174 | return 175 | 176 | 177 |
178 | 179 | {this.warnOldVersion && this.warnOldVersion = false}> 182 | Pre-SARIF-2.1 logs have been omitted. Use the Artifacts explorer to access all files. 183 | } 184 | {nearElement} 185 |
186 |
187 |
188 | } 189 | } 190 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | SARIF Viewer 9 | 10 | 11 | 208 | 209 | 210 |
211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | Sarif Viewer 8 | 9 | 206 | 207 | 208 |
209 | 210 | 211 | 212 | 216 | 217 | 218 | -------------------------------------------------------------------------------- /components/Snippet.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import './Snippet.scss' 5 | import * as React from 'react' 6 | import {observable} from 'mobx' 7 | import {observer} from 'mobx-react' 8 | import * as hljs from 'highlight.js/lib/core' 9 | require('!style-loader!css-loader!highlight.js/styles/vs.css') 10 | hljs.registerLanguage('csharp', require('highlight.js/lib/languages/csharp')) 11 | hljs.registerLanguage('java', require('highlight.js/lib/languages/java')) 12 | hljs.registerLanguage('typescript', require('highlight.js/lib/languages/typescript')) 13 | hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml')) 14 | 15 | import {FilterKeywordContext} from './Viewer' 16 | import {Hi} from './Hi' 17 | import {PhysicalLocation} from 'sarif' 18 | import {tryOr} from './try' 19 | 20 | @observer export class Snippet extends React.Component<{ ploc?: PhysicalLocation, style?: React.CSSProperties }> { 21 | static contextType = FilterKeywordContext 22 | @observable showAll = false 23 | 24 | render () { 25 | const {ploc} = this.props 26 | if (!ploc) return null 27 | if (!ploc.region) return null 28 | 29 | let term = this.context 30 | 31 | let body = tryOr( 32 | () => { 33 | const {region, contextRegion} = ploc 34 | if (!contextRegion) return undefined // tryOr fallthrough. 35 | 36 | const crst = contextRegion.snippet.text 37 | 38 | // Search/Filter highlighting is active so bypass snippet highlighting and return plain text. 39 | if (term) return crst 40 | 41 | // Carriage returns (\n) causing hljs colorization off-by-one errors, thus stripping them here. 42 | let lines = crst.replace(/\r/g, '').split('\n') 43 | const minLeadingWhitespace = Math.min( 44 | ...lines 45 | .filter(line => line.trimLeft().length) // Blank lines often have indent 0, so throwing these out. 46 | .map(line => line.match(/^ */)[0].length) 47 | ) 48 | lines = lines.map(line => line.slice(minLeadingWhitespace)) 49 | 50 | // Per 3.30.2 SARIF line and columns are 1-based. 51 | let {startLine, startColumn = 1, endLine = startLine, endColumn = Number.MAX_SAFE_INTEGER} = region 52 | 53 | // "3.30.5 startLine property - When a region object represents a text region specified by line/column properties, it SHALL contain .. startLine..." 54 | // If startLine is undefined, then it not line/column-specified and likely offset/length-specified. The later is not currently supported. 55 | if (startLine === undefined) return undefined // tryOr fallthrough. 56 | 57 | // Convert region to 0-based for ease of calculations. 58 | startLine -= 1 59 | startColumn -= 1 60 | endLine -= 1 61 | endColumn -= 1 62 | 63 | // Same comments from above apply to the contextRegion. 64 | let {startLine: crStartLine = 1, startColumn: crStartColumn = 1 } = contextRegion 65 | crStartLine -= 1 66 | crStartColumn -= 1 67 | 68 | // Map region from document coordinates to contextRegion coordinates. 69 | startLine -= crStartLine 70 | startColumn = Math.max(0, startColumn - crStartColumn - minLeadingWhitespace) 71 | endLine -= crStartLine 72 | endColumn = Math.max(0, endColumn - crStartColumn - minLeadingWhitespace) 73 | 74 | // Insert start stop markers. 75 | const marker = '\u200B' 76 | // Endline marker must be inserted first. Otherwise if startLine=endLine, the marker char will make the slice off by one. 77 | lines[endLine] 78 | = lines[endLine].slice(0, endColumn) + marker + lines[endLine].slice(endColumn) 79 | lines[startLine] 80 | = lines[startLine].slice(0, startColumn) + marker + lines[startLine].slice(startColumn) 81 | 82 | const [pre, hi, post] = lines.join('\n').split(marker) 83 | return <>{pre}{hi}{post} 84 | }, 85 | () => ploc.region.snippet.text, // No need to un-indent as these infrequently include leading whitespace. 86 | ) 87 | if (!body) return null // May no longer be needed. 88 | 89 | if (term) body = {body} 90 | 91 | const lineNumbersAndCode = <> 92 | {tryOr(() => { 93 | const region = ploc.contextRegion || ploc.region 94 | if (!region.startLine) return undefined // Don't take up left margin space if there's nothing to show. 95 | const endLine = region.endLine ?? region.startLine 96 | let lineNos = '' 97 | for (let i = region.startLine; i <= endLine; i++) { 98 | lineNos += `${i}\n` 99 | } 100 | return {lineNos} 101 | })} 102 | { 106 | if (!code) return 107 | try { 108 | hljs.highlightBlock(code) 109 | } catch(e) { 110 | // Commonly throws if the language is not loaded. Will add telemetry here to track. 111 | console.log(code, e) 112 | } 113 | }}> 114 | {body} 115 | 116 | 117 | 118 | // title={JSON.stringify(ploc, null, ' ')} 119 | return <> 120 |
 this.showAll = !this.showAll}
123 | 				ref={pre => {
124 | 					if (!pre) return
125 | 					const isClipped = pre.scrollHeight > pre.clientHeight
126 | 					if (isClipped) pre.classList.add('clipped')
127 | 					else pre.classList.remove('clipped')
128 | 				}}>
129 | 				{lineNumbersAndCode}
130 | 			
131 | 132 | } 133 | } 134 | 135 | export class SnippetTest extends React.Component { 136 | render() { 137 | return
138 | 139 | 140 | 141 | rules)\r\n {\r\n if (!string.IsNullOrEmpty(result.RuleId))\r\n {\r\n if (rules.ContainsKey(result.RuleId))\r\n {\r\n return rules[result.RuleId];\r\n }\r\n }\r\n return null;\r\n }" 151 | } 152 | }, 153 | contextRegion: { 154 | startLine: 183, 155 | endLine: 199, 156 | snippet: { 157 | text: " return results;\n }\n \n private ReportingDescriptor GetRuleFromResources(Result result, IDictionary rules)\n {\n if (!string.IsNullOrEmpty(result.RuleId))\n {\n if (rules.ContainsKey(result.RuleId))\n {\n return rules[result.RuleId];\n }\n }\n return null;\n }\n\n private SarifLog ConstructSarifLogFromMatchedResults(\n IEnumerable results, \n" 158 | } 159 | } 160 | }} /> 161 | 162 | 168 | 169 | 179 | 180 | 201 | 202 | ).RemoveAll(f => f == null);" 212 | }, 213 | }, 214 | contextRegion: { 215 | startLine: 777, 216 | endLine: 783, 217 | snippet: { 218 | text: " if (result.Fixes != null)\n {\n // Null Fixes will be present in the case of unsupported encoding\n (result.Fixes as List).RemoveAll(f => f == null);\n\n if (result.Fixes.Count == 0)\n {\n" 219 | }, 220 | } 221 | }} /> 222 | 223 | 229 | 230 | 243 | 244 | 250 | 251 | 272 | 273 |
1. Sign into Houston POS with username 123456 and password 789
2. Do an exchange for item 0001