<|start_filename|>app/lib/SecvisogramPage/View/FormEditorTab/ProductTree.js<|end_filename|>
import '@reach/combobox/styles.css'
import React from 'react'
import ArrayContainer from './shared/ArrayContainer'
import { Branches, FullProductName } from './shared/definitions'
import EnumAttribute from './shared/EnumAttribute'
import IdAttribute from './shared/IdAttribute'
import ObjectContainer from './shared/ObjectContainer'
import TextAttribute from './shared/TextAttribute'
import { uniqueGroupId, uniqueProductId } from './shared/unique-id'
import validationErrorShallowEqual from './shared/validationErrorShallowEqual'

export default React.memo(
  /**
   * @param {{
   *  value: unknown
   *  validationErrors: import('../../../shared/validationTypes').ValidationError[]
   *  dataPath: string
   *  onUpdate(dataPath: string, update: {}): void
   *  onCollectProductIds(): Promise<void | {id: string, name: string}[]>
   * }} props
   */
  function ProductTree(props) {
    const {
      value: productTree,
      validationErrors,
      dataPath,
      onUpdate,
      onCollectProductIds,
    } = props
    return (
      <ObjectContainer
        label="Product tree"
        description="Is a container for all fully qualified product names that can be referenced elsewhere in the document."
        validationErrors={validationErrors}
        dataPath={dataPath}
        value={productTree}
        defaultValue={() => ({})}
        onUpdate={onUpdate}
      >
        {(productTreeProps) => (
          <>
            <Branches {...productTreeProps('branches')} />
            <FullProductNames {...productTreeProps('full_product_names')} />
            <ProductGroups
              {...productTreeProps('product_groups')}
              onCollectProductIds={onCollectProductIds}
            />
            <Relationships
              {...productTreeProps('relationships')}
              onCollectProductIds={onCollectProductIds}
            />
          </>
        )}
      </ObjectContainer>
    )
  },
  validationErrorShallowEqual
)

const ProductGroups = React.memo(
  /**
   * @param {{
   *  value: unknown
   *  validationErrors: import('../../../shared/validationTypes').ValidationError[]
   *  dataPath: string
   *  onUpdate(dataPath: string, update: {}): void
   *  onCollectProductIds(): Promise<void | {id: string, name: string}[]>
   * }} props
   */
  function ProductGroups({ onCollectProductIds, ...props }) {
    return (
      <ArrayContainer
        {...props}
        label="List of product groups"
        description="Contains a list of product groups."
        defaultItemValue={() => ({
          group_id: uniqueGroupId(),
          product_ids: ['', ''],
        })}
      >
        {(itemProps) => (
          <ObjectContainer
            {...itemProps}
            label="Product group"
            description="Defines a new logical group of products that can then be referred to in other parts of the document to address a group of products with a single identifier."
          >
            {(productGroupProps) => (
              <>
                <TextAttribute
                  {...productGroupProps('summary')}
                  label="Summary of the product group"
                  description="Gives a short, optional description of the group."
                  deletable
                />
                <TextAttribute
                  {...productGroupProps('group_id')}
                  label="Reference token for product group instance"
                  description="Token required to identify a group of products so that it can be referred to from other parts in the document. There is no predefined or required format for the product_group_id as long as it uniquely identifies a group in the context of the current document."
                  placeholder="CSAFGID-0001"
                />
                <ArrayContainer
                  {...productGroupProps('product_ids')}
                  label="List of Product IDs"
                  description="Lists the product_ids of those products which known as one group in the document."
                  defaultItemValue={() => ''}
                >
                  {(productIdProps) => (
                    <IdAttribute
                      {...productIdProps}
                      label="Reference token for product instance"
                      description="Token required to identify a full_product_name so that it can be referred to from other parts in the document. There is no predefined or required format for the product_id as long as it uniquely identifies a product in the context of the current document."
                      placeholder="CSAFPID-0004 ..."
                      deletable
                      onCollectIds={onCollectProductIds}
                    />
                  )}
                </ArrayContainer>
              </>
            )}
          </ObjectContainer>
        )}
      </ArrayContainer>
    )
  },
  validationErrorShallowEqual
)

const FullProductNames = React.memo(
  /**
   * @param {{
   *  value: unknown
   *  validationErrors: import('../../../shared/validationTypes').ValidationError[]
   *  dataPath: string
   *  onUpdate(dataPath: string, update: {}): void
   * }} props
   */
  function FullProductNames(props) {
    return (
      <ArrayContainer
        {...props}
        label="List of full product names"
        description="Contains a list of full product names."
        defaultItemValue={() => ({
          product_id: uniqueProductId(),
          name: '',
        })}
      >
        {(fullProductNameItemProps) => (
          <FullProductName {...fullProductNameItemProps} />
        )}
      </ArrayContainer>
    )
  },
  validationErrorShallowEqual
)

const Relationships = React.memo(
  /**
   * @param {{
   *  value: unknown
   *  validationErrors: import('../../../shared/validationTypes').ValidationError[]
   *  dataPath: string
   *  onUpdate(dataPath: string, update: {}): void
   *  onCollectProductIds(): Promise<void | {id: string, name: string}[]>
   * }} props
   */
  function Relationships({ onCollectProductIds, ...props }) {
    return (
      <ArrayContainer
        {...props}
        label="List of relationships"
        description="Contains a list of relationships."
        defaultItemValue={() => ({
          full_product_name: { name: '', product_id: uniqueProductId() },
          product_reference: '',
          category: '',
          relates_to_product_reference: '',
        })}
      >
        {(relationshipItemProps) => (
          <ObjectContainer
            {...relationshipItemProps}
            label="Relationship"
            description="Establishes a link between two existing full_product_name_t elements, allowing the document producer to define a combination of two products that form a new full_product_name entry."
          >
            {(relationshipProps) => {
              const productReference = /** @type {string} */ (relationshipProps(
                'product_reference'
              ).value ?? '')
              const relatesToProductReference = /** @type {string} */ (relationshipProps(
                'relates_to_product_reference'
              ).value ?? '')
              const category = /** @type {string} */ (relationshipProps(
                'category'
              ).value ?? '')
              return (
                <>
                  <FullProductName
                    {...relationshipProps('full_product_name')}
                    onCollectProductIds={onCollectProductIds}
                    productReference={productReference}
                    category={category}
                    relatesToProductReference={relatesToProductReference}
                  />
                  <IdAttribute
                    {...relationshipProps('product_reference')}
                    label="Reference token for product instance"
                    description="Token required to identify a full_product_name so that it can be referred to from other parts in the document. There is no predefined or required format for the product_id as long as it uniquely identifies a product in the context of the current document."
                    placeholder="CSAFPID-0004 ..."
                    onCollectIds={onCollectProductIds}
                  />
                  <EnumAttribute
                    {...relationshipProps('category')}
                    label="Relationship category"
                    description="Defines the category of relationship for the referenced component."
                    options={[
                      'default_component_of',
                      'optional_component_of',
                      'external_component_of',
                      'installed_on',
                      'installed_with',
                    ]}
                  />
                  <IdAttribute
                    {...relationshipProps('relates_to_product_reference')}
                    label="Reference token for product instance"
                    description="Token required to identify a full_product_name so that it can be referred to from other parts in the document. There is no predefined or required format for the product_id as long as it uniquely identifies a product in the context of the current document."
                    placeholder="CSAFPID-0004 ..."
                    onCollectIds={onCollectProductIds}
                  />
                </>
              )
            }}
          </ObjectContainer>
        )}
      </ArrayContainer>
    )
  },
  validationErrorShallowEqual
)


<|start_filename|>app/lib/SecvisogramPage/View/FormEditorTab/shared/NumberAttribute.js<|end_filename|>
import React from 'react'
import Attribute from './shared/Attribute'
import Delete from './shared/Delete'

/**
 * @param {Pick<React.HTMLProps<HTMLInputElement>, 'min' | 'max' | 'step'> & {
 *  label: string
 *  description: string
 *  placeholder?: string
 *  pattern?: string
 *  defaultValue?(): string
 *  readOnly?: boolean
 *  deletable?: boolean
 *  required?: boolean
 *  validationErrors: import('../../../../shared/validationTypes').ValidationError[]
 *  dataPath: string
 *  value: unknown
 *  onUpdate(dataPath: string, update: {}): void
 * }} props
 */
export default function NumberAttribute({
  placeholder,
  pattern,
  required = true,
  min,
  max,
  readOnly = false,
  step,
  deletable,
  ...props
}) {
  return (
    <Attribute {...props}>
      {({ onChange, onDelete }) => (
        <div className="max-w-md flex items-baseline justify-center">
          <div className="w-full">
            <input
              value={String(props.value)}
              className="border border-gray-400 py-1 px-2 w-full shadow-inner rounded"
              type="number"
              placeholder={placeholder}
              pattern={pattern}
              min={min}
              max={max}
              required={required}
              step={step}
              readOnly={readOnly}
              onChange={(e) => {
                const float = parseFloat(e.target.value)
                onChange(Number.isNaN(float) ? '' : float, String(props.value))
              }}
            />
          </div>
          {deletable ? (
            <Delete
              doDelete={() => {
                onDelete(String(props.value))
              }}
            />
          ) : null}
        </div>
      )}
    </Attribute>
  )
}


<|start_filename|>app/lib/SecvisogramPage/View/JsonEditorTab.js<|end_filename|>
import {
  faCheckCircle,
  faCog,
  faExclamationTriangle,
  faFile,
  faFileAlt,
  faFolderOpen,
  faSave,
  faWindowClose
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React from 'react'
import { useAlert } from './shared/Alert'
import useDebounce from './shared/useDebounce'

/**
 * Embeds the ace editor.
 * @see https://ace.c9.io/
 *
 * @param {{
 *  formValues: import('../shared/FormValues').default
 *  validationErrors: import('../../shared/validationTypes').ValidationError[]
 *  strict: boolean
 *  onSetStrict(strict: boolean): void
 *  onChange(doc: {} | null): void
 *  onOpen(file: File): Promise<void | {}>
 *  onDownload(doc: {}): void
 *  onNewDocMin(): Promise<void | {}>
 *  onNewDocMax(): Promise<void | {}>
 *  onLockTab(): void
 *  onUnlockTab(): void
 * }} props
 */
export default function JsonEditorTab({
  formValues,
  validationErrors: errors,
  strict,
  onSetStrict,
  onChange,
  onOpen,
  onDownload,
  onNewDocMin,
  onNewDocMax,
  onLockTab,
  onUnlockTab,
}) {
  const { doc } = formValues

  /**
   * Holds the reference to the container element of the ace editor.
   */
  const ref = React.useRef(/** @type {HTMLDivElement | null} */ (null))

  /**
   * Holds the instance of the ace editor.
   */
  const editorRef = /** @type {React.MutableRefObject<import('../../../vendor/ace-builds/ace').Ace.Editor | undefined>} */ (React.useRef())

  const stringifiedDoc = React.useMemo(() => JSON.stringify(doc, null, 2), [
    doc,
  ])

  /**
   * The initial value of the state used to prevent a re-render of the ace editor
   * when the document changes from outside.
   */
  const [initialValue] = React.useState(stringifiedDoc)

  /**
   * Holds the value and potential parse errors of the ace editor input.
   */
  const [{ value, parseError }, setState] = React.useState({
    value: stringifiedDoc,
    parseError: null,
  })
  const [showExpertSettings, setShowExpertSettings] = React.useState(!strict)
  const [showErrors, setShowErrors] = React.useState(false)
  const debouncedValue = useDebounce(value)

  /**
   * Toggles between strict and lenient validation.
   */
  const toggleStrict = () => {
    onSetStrict(!strict)
  }

  const toggleExpertSettings = () => {
    setShowExpertSettings(!showExpertSettings)
  }

  const toggleShowErrors = () => {
    setShowErrors(!showErrors)
  }

  const confirmMin = () => {
    onNewDocMin().then((newDoc) => {
      editorRef.current?.setValue(JSON.stringify(newDoc, null, 2))
    })
    hideMin()
  }

  const confirmMax = () => {
    onNewDocMax().then((newDoc) => {
      editorRef.current?.setValue(JSON.stringify(newDoc, null, 2))
    })
    hideMax()
  }

  const handleOpen = (/** @type {File} */ file) => {
    onOpen(file).then((openedDoc) => {
      editorRef.current?.setValue(JSON.stringify(openedDoc, null, 2))
    })
  }

  /**
   * Locks the tab navigation if there are any parse errors.
   */
  React.useEffect(() => {
    if (parseError) onLockTab()
    else onUnlockTab()
  }, [parseError, onLockTab, onUnlockTab])

  /**
   * Initializes the ace editor.
   */
  React.useEffect(() => {
    const editorEl = /** @type {HTMLDivElement} */ (ref.current)
    if (!editorRef.current) {
      editorRef.current = ace.edit(editorEl)
    }
    const changeHandler = () => {
      setState((state) => ({
        ...state,
        value: editorRef.current?.getValue() ?? '',
      }))
    }
    editorEl.style.fontSize = '14px'

    editorRef.current.session.setMode('ace/mode/json')
    editorRef.current.setTheme('ace/theme/eclipse')
    editorRef.current.setOptions({
      foldStyle: 'markbeginend',
      fadeFoldWidgets: true,
      enableBasicAutocompletion: true,
      enableLiveAutocompletion: true,
      enableSnippets: true,
    })
    editorRef.current.on('change', changeHandler)
    editorRef.current.setValue(initialValue)

    return () => {
      editorRef.current?.off('change', changeHandler)
      editorRef.current?.destroy()
    }
  }, [initialValue])

  /**
   * Parses the ace editor input and replaces the document.
   */
  React.useEffect(() => {
    /** @type {{} | null} */
    let result = null
    try {
      result = JSON.parse(debouncedValue)
      setState((state) => ({ ...state, parseError: null }))
    } catch (e) {
      setState((state) => ({ ...state, parseError: e }))
      return
    }
    onChange(result)
  }, [debouncedValue, onChange])

  React.useEffect(() => {
    if (errors.length === 0) {
      setShowErrors(false)
    }
  }, [errors])

  const { show: showMin, hide: hideMin, Alert: MinAlert } = useAlert({
    description:
      'This will create a new CSAF document. All current content will be lost. Are you sure?',
    confirmLabel: 'Yes, create new document',
    cancelLabel: 'No, resume editing',
    confirm: confirmMin,
  })

  const { show: showMax, hide: hideMax, Alert: MaxAlert } = useAlert({
    description:
      'This will create a new CSAF document. All current content will be lost. Are you sure?',
    confirmLabel: 'Yes, create new document',
    cancelLabel: 'No, resume editing',
    confirm: confirmMax,
  })

  return (
    <>
      <MinAlert />
      <MaxAlert />
      <div className="json-editor flex h-full mr-3 bg-white">
        <div className="p-3 w-full">
          <div className={'relative ' + (showErrors ? 'h-4/5' : 'h-full')}>
            <div
              ref={ref}
              className="absolute top-0 right-0 bottom-0 left-0 h-full bg-white"
            />
          </div>
          <div
            className={
              'overflow-auto p-3 border border-red-600 bg-red-200 ' +
              (showErrors ? 'h-1/5' : 'hidden')
            }
          >
            <div className="flex justify-between items-start h-full">
              <div className="pr-4">
                <h2 className="text-xl font-bold">
                  Validation <br /> Errors:
                </h2>
              </div>
              <div className="mx-2 flex-grow overflow-auto h-full">
                {errors.map((error, i) => (
                  <div key={i}>
                    <b>{error.dataPath}</b>: {error.message}
                  </div>
                ))}
              </div>
              <button
                type="button"
                className="text-xl text-red-400"
                onClick={() => setShowErrors(false)}
              >
                <FontAwesomeIcon className="mr-1" icon={faWindowClose} />
              </button>
            </div>
          </div>
        </div>
        <div className="pl-3 pr-6 py-6 w-72 flex flex-col justify-between">
          <div className="flex flex-col">
            <button
              type="button"
              className="mb-2 py-1 px-3 rounded shadow border border-blue-400 bg-blue-400 text-white hover:text-blue-400 hover:bg-white"
              onClick={showMin}
            >
              <FontAwesomeIcon className="mr-1" icon={faFile} />
              New (minimal fields)
            </button>
            <button
              type="button"
              className="mb-2 py-1 px-3 rounded shadow border border-blue-400 bg-blue-400 text-white hover:text-blue-400 hover:bg-white"
              onClick={showMax}
            >
              <FontAwesomeIcon className="mr-1" icon={faFileAlt} />
              New (all fields)
            </button>
            <label
              htmlFor="openFile"
              className="mb-2 py-1 px-3 text-center rounded shadow border border-blue-400 bg-blue-400 text-white hover:text-blue-400 hover:bg-white"
            >
              <FontAwesomeIcon className="mr-1" icon={faFolderOpen} />
              Open
            </label>
            <input
              id="openFile"
              title="open file"
              type="file"
              className="hidden"
              accept="application/json"
              onChange={(e) => {
                if (!e.target.files || !e.target.files[0]) return
                if (e.target.files[0].size > 1 * 1024 * 1024) {
                  window.alert('File too large!')
                  return
                }
                handleOpen(e.target.files[0])
              }}
            />
            <button
              type="button"
              className="mb-2 py-1 px-3 rounded shadow border border-blue-400 bg-blue-400 text-white hover:text-blue-400 hover:bg-white"
              onClick={() => {
                onDownload(doc)
              }}
            >
              <FontAwesomeIcon className="mr-1" icon={faSave} />
              Save
            </button>
          </div>
          <div>
            {showExpertSettings ? (
              <div className="mb-6">
                <div className="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
                  <input
                    checked={strict}
                    type="checkbox"
                    name="toggle"
                    id="toggleExpertSettings"
                    onChange={toggleStrict}
                    className="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"
                  />
                  <label
                    htmlFor="toggleExpertSettings"
                    className="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"
                  />
                </div>
                <label
                  htmlFor="toggleExpertSettings"
                  className="text-xs text-gray-500"
                >
                  Disallow non-standard properties
                </label>
              </div>
            ) : (
              <button
                type="button"
                className="py-1 px-3 mb-6 h-9 underline text-gray-500"
                onClick={toggleExpertSettings}
              >
                <FontAwesomeIcon className="mr-1" icon={faCog} />
                Show expert settings
              </button>
            )}

            <h2 className="mb-4 text-xl font-bold">Validation Status</h2>
            {errors.length === 0 ? (
              <>
                <div className="mb-4 flex justify-end">
                  <FontAwesomeIcon
                    className="text-6xl text-green-500"
                    icon={faCheckCircle}
                  />
                </div>
                <div className="h-9" />
              </>
            ) : (
              <>
                <div className="mb-4 flex justify-between">
                  <span className="text-6xl text-red-500 font-bold">
                    {errors.length}
                  </span>
                  <FontAwesomeIcon
                    className="text-6xl text-red-500"
                    icon={faExclamationTriangle}
                  />
                </div>
                <button
                  type="button"
                  className="py-1 px-3 h-9 underline text-gray-500"
                  onClick={toggleShowErrors}
                >
                  {showErrors ? 'Hide errors' : 'Show errors'}
                </button>
              </>
            )}
          </div>
        </div>
      </div>
    </>
  )
}


<|start_filename|>app/tests/Core.js<|end_filename|>
import Ajv from 'ajv'
import { expect } from 'chai'
import { DocumentEntity } from '../lib/shared/Core/entities'
import createCoreFixture from './shared/CoreFixture'

suite('Core', () => {
  const fixture = createCoreFixture()

  setup(() => fixture.setup())
  teardown(() => fixture.teardown())

  suite('DocumentService', () => {
    test('The document can be validated against the JSON-schema', async () => {
      for (const document of fixture.documents) {
        const result = await fixture.core.document.validate({
          document: document.content,
          strict: false,
        })
        expect(result.isValid).to.equal(document.valid)
        if (document.valid) {
          expect(result.errors).to.have.lengthOf(0)
        } else {
          expect(result.errors).have.length.greaterThan(0)
        }
      }
    })

    test('The document can be minified using the CSAF-strip algorithm', async () => {
      for (const document of fixture.documents) {
        if (document.strippedVersion === undefined) continue
        const result = await fixture.core.document.strip({
          document: document.content,
        })

        expect(result.document).to.deep.equal(document.strippedVersion)
      }
    })
  })

  suite('DocumentEntity', () => {
    test('When stripping a json document properties with errors are removed', () => {
      const schemaValidator = new Ajv({ allErrors: true }).compile({
        type: 'object',
        properties: { title: { type: 'string' } },
        required: ['title'],
      })
      const documentEntity = new DocumentEntity({ schemaValidator })

      const result = documentEntity.strip({ document: { title: 4 } })

      expect(result.document).to.deep.equal({})
      expect(result.strippedPaths).to.deep.equal([
        { dataPath: '/title', error: true, message: 'should be string' },
      ])
    })

    test('When stripping a json document empty properties are removed', () => {
      const schemaValidator = new Ajv({ allErrors: true }).compile({
        type: 'object',
        properties: { title: { type: 'string' } },
      })
      const documentEntity = new DocumentEntity({ schemaValidator })

      const result = documentEntity.strip({ document: { title: '' } })

      expect(result.document).to.deep.equal({})
      expect(result.strippedPaths).to.deep.equal([
        { dataPath: '/title', error: false, message: 'value was empty' },
      ])
    })
  })
})


<|start_filename|>app/lib/SecvisogramPage/View/FormEditorTab/Document.js<|end_filename|>
import '@reach/combobox/styles.css'
import React from 'react'
import AggregateSeverity from './Document/AggregateSeverity'
import Distribution from './Document/Distribution'
import Publisher from './Document/Publisher'
import Tracking from './Document/Tracking'
import { Acknowledgments, Lang, Notes, References } from './shared/definitions'
import EnumAttribute from './shared/EnumAttribute'
import ObjectContainer from './shared/ObjectContainer'
import TextAttribute from './shared/TextAttribute'
import validationErrorShallowEqual from './shared/validationErrorShallowEqual'

export default React.memo(
  /**
   * @param {{
   *  value: unknown
   *  validationErrors: import('../../../shared/validationTypes').ValidationError[]
   *  dataPath: string
   *  onUpdate(dataPath: string, update: {}): void
   * }} props
   */
  function Document(props) {
    return (
      <ObjectContainer
        {...props}
        label="Document level meta-data"
        description="Captures the meta-data about this document describing a particular set of security advisories."
        defaultValue={() => ({
          category: '',
          csaf_version: '2.0',
          publisher: {
            category: '',
            name: '',
          },
          title: '',
          tracking: {
            current_release_date: '',
            id: '',
            initial_release_date: '',
            revision_history: [
              {
                date: '',
                number: '',
                summary: '',
              },
            ],
            status: '',
            version: '',
          },
        })}
      >
        {(documentLevelMetaDataProps) => (
          <>
            <Acknowledgments
              {...documentLevelMetaDataProps('acknowledgments')}
            />
            <AggregateSeverity
              {...documentLevelMetaDataProps('aggregate_severity')}
            />
            <TextAttribute
              {...documentLevelMetaDataProps('category')}
              label="Document category"
              description="Defines a short canonical name, chosen by the document producer, which will inform the end user as to the category of document."
              placeholder="Security Advisory"
            />
            <EnumAttribute
              {...documentLevelMetaDataProps('csaf_version')}
              label="CSAF version"
              description="Gives the version of the CSAF specification which the document was generated for."
              options={['2.0']}
            />
            <Distribution {...documentLevelMetaDataProps('distribution')} />
            <Lang
              label="Document language"
              description="Identifies the language used by this document, corresponding to IETF BCP 47 / RFC 5646."
              deletable
              {...documentLevelMetaDataProps('lang')}
            />
            <Notes
              {...documentLevelMetaDataProps('notes')}
              label="Notes associated with the whole document."
              description="Holds notes about this set of vulnerabilities."
            />
            <Publisher {...documentLevelMetaDataProps('publisher')} />
            <References {...documentLevelMetaDataProps('references')} />
            <Lang
              label="Source language"
              description="If this copy of the document is a translation then the value of this property describes from which language this document was translated."
              deletable
              {...documentLevelMetaDataProps('source_lang')}
            />
            <TextAttribute
              {...documentLevelMetaDataProps('title')}
              label="Title of this document"
              description="This SHOULD be a canonical name for the document, and sufficiently unique to distinguish it from similar documents."
              placeholder="Example Company Cross-Site-Scripting Vulnerability in Example Generator"
            />
            <Tracking {...documentLevelMetaDataProps('tracking')} />
          </>
        )}
      </ObjectContainer>
    )
  },
  validationErrorShallowEqual
)


<|start_filename|>app/lib/SecvisogramPage/View/FormEditorTab.js<|end_filename|>
import {
  faCheckCircle,
  faExclamationTriangle,
  faFile,
  faFileAlt,
  faFolderOpen,
  faMinusSquare,
  faPlusSquare,
  faSave,
  faWindowClose,
} from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import React from 'react'
import Document from './FormEditorTab/Document'
import ProductTree from './FormEditorTab/ProductTree'
import ObjectContainer from './FormEditorTab/shared/ObjectContainer'
import Vulnerabilities from './FormEditorTab/Vulnerabilities'
import { useAlert } from './shared/Alert'

/**
 * Defines the layout of the form editor.
 *
 * @param {{
 *  formValues: import('../shared/FormValues').default
 *  validationErrors: import('../../shared/validationTypes').ValidationError[]
 *  onUpdate: ((update: {}) => void) & ((dataPath: string, update: {}) => void)
 *  onOpen(file: File): void
 *  onDownload(doc: {}): void
 *  onNewDocMin(): void
 *  onNewDocMax(): void
 *  onCollectProductIds(): Promise<void | {id: string, name: string}[]>
 *  onCollectGroupIds(): Promise<void | {id: string, name: string}[]>
 * }} props
 */
export default function FormEditorTab({
  formValues,
  validationErrors: errors,
  onUpdate,
  onOpen,
  onDownload,
  onNewDocMin,
  onNewDocMax,
  onCollectProductIds,
  onCollectGroupIds,
}) {
  const ref = React.useRef(/** @type {HTMLDivElement | null} */ (null))
  const [showErrors, setShowErrors] = React.useState(false)

  const toggleErrors = () => {
    setShowErrors(!showErrors)
    if (!showErrors) setExpanded(true)
  }

  const confirmMin = () => {
    onNewDocMin()
    hideMin()
  }

  const confirmMax = () => {
    onNewDocMax()
    hideMax()
  }

  /**
   * Expands all collapsible form elements.
   *
   * @param {boolean} open
   */
  const setExpanded = (open) => {
    if (!ref.current) return
    /** @type {NodeListOf<HTMLDetailsElement>} */
    const elements = ref.current.querySelectorAll('.js-collapsible')
    for (const el of elements) {
      el.open = open
    }
  }

  React.useEffect(() => {
    if (errors.length === 0) {
      setShowErrors(false)
    }
  }, [errors])

  const { doc } = formValues

  const { show: showMin, hide: hideMin, Alert: MinAlert } = useAlert({
    description:
      'This will create a new CSAF document. All current content will be lost. Are you sure?',
    confirmLabel: 'Yes, create new document',
    cancelLabel: 'No, resume editing',
    confirm: confirmMin,
  })

  const { show: showMax, hide: hideMax, Alert: MaxAlert } = useAlert({
    description:
      'This will create a new CSAF document. All current content will be lost. Are you sure?',
    confirmLabel: 'Yes, create new document',
    cancelLabel: 'No, resume editing',
    confirm: confirmMax,
  })

  return (
    <>
      <MinAlert />
      <MaxAlert />
      <div ref={ref} className="form-editor flex h-full mr-3 bg-white">
        <div className="p-3 w-full">
          <div className={'overflow-auto ' + (showErrors ? 'h-4/5' : 'h-full')}>
            <Doc
              dataPath=""
              value={doc}
              validationErrors={errors}
              onUpdate={onUpdate}
              onCollectProductIds={onCollectProductIds}
              onCollectGroupIds={onCollectGroupIds}
            />
          </div>
          <div
            className={
              'overflow-auto p-3 border border-red-600 bg-red-200 ' +
              (showErrors ? 'h-1/5' : 'hidden')
            }
          >
            <div className="flex justify-between items-start h-full">
              <div className="pr-4">
                <h2 className="text-xl font-bold">
                  Validation <br /> Errors:
                </h2>
              </div>
              <div className="mx-2 flex-grow overflow-auto h-full">
                {errors.map((error, i) => (
                  <div key={i}>
                    <a href={'#' + error.dataPath} className="underline">
                      <b>{error.dataPath}</b>: {error.message}
                    </a>
                  </div>
                ))}
              </div>
              <button
                type="button"
                className="text-xl text-red-400"
                onClick={() => setShowErrors(false)}
              >
                <FontAwesomeIcon className="mr-1" icon={faWindowClose} />
              </button>
            </div>
          </div>
        </div>
        <div className="pl-3 pr-6 py-6 w-72 flex flex-col justify-between">
          <div className="flex flex-col">
            <button
              type="button"
              className="mb-2 py-1 px-3 rounded shadow border border-blue-400 bg-blue-400 text-white hover:text-blue-400 hover:bg-white"
              onClick={showMin}
            >
              <FontAwesomeIcon className="mr-1" icon={faFile} />
              New (minimal fields)
            </button>
            <button
              type="button"
              className="mb-2 py-1 px-3 rounded shadow border border-blue-400 bg-blue-400 text-white hover:text-blue-400 hover:bg-white"
              onClick={showMax}
            >
              <FontAwesomeIcon className="mr-1" icon={faFileAlt} />
              New (all fields)
            </button>
            <label
              htmlFor="openFile"
              className="mb-2 py-1 px-3 text-center rounded shadow border border-blue-400 bg-blue-400 text-white hover:text-blue-400 hover:bg-white"
            >
              <FontAwesomeIcon className="mr-1" icon={faFolderOpen} />
              Open
            </label>
            <input
              id="openFile"
              title="open file"
              type="file"
              className="hidden"
              accept="application/json"
              onChange={(e) => {
                if (!e.target.files || !e.target.files[0]) return
                if (e.target.files[0].size > 1 * 1024 * 1024) {
                  window.alert('File too large!')
                  return
                }
                onOpen(e.target.files[0])
              }}
            />
            <button
              type="button"
              className="mb-2 py-1 px-3 rounded shadow border border-blue-400 bg-blue-400 text-white hover:text-blue-400 hover:bg-white"
              onClick={() => {
                onDownload(doc)
              }}
            >
              <FontAwesomeIcon className="mr-1" icon={faSave} />
              Save
            </button>
            <button
              type="button"
              className="mb-2 py-1 px-3 rounded shadow border border-gray-400 bg-gray-400 text-white hover:text-gray-400 hover:bg-white"
              onClick={() => {
                setExpanded(true)
              }}
            >
              <FontAwesomeIcon className="mr-1" icon={faPlusSquare} />
              Expand all
            </button>
            <button
              type="button"
              className="mb-2 py-1 px-3 rounded shadow border border-gray-400 bg-gray-400 text-white hover:text-gray-400 hover:bg-white"
              onClick={() => {
                setExpanded(false)
              }}
            >
              <FontAwesomeIcon className="mr-1" icon={faMinusSquare} />
              Collapse all
            </button>
          </div>
          <div>
            <h2 className="mb-4 text-xl font-bold">Validation Status</h2>
            {errors.length === 0 ? (
              <>
                <div className="mb-4 flex justify-end">
                  <FontAwesomeIcon
                    className="text-6xl text-green-500"
                    icon={faCheckCircle}
                  />
                </div>
                <div className="h-9" />
              </>
            ) : (
              <div>
                <div className="mb-4 flex justify-between">
                  <span className="text-6xl text-red-500 font-bold">
                    {errors.length}
                  </span>
                  <FontAwesomeIcon
                    className="text-6xl text-red-500"
                    icon={faExclamationTriangle}
                  />
                </div>
                <button
                  type="button"
                  className="py-1 px-3 h-9 underline text-gray-500"
                  onClick={toggleErrors}
                >
                  {showErrors ? 'Hide errors' : 'Show errors'}
                </button>
              </div>
            )}
          </div>
        </div>
      </div>
    </>
  )
}

/**
 * Defines the layout of the document. The data path is passed throughout all
 * children to enable error filtering and nested updates. Containers and
 * attributes of the document are defined in respective components.
 *
 * @param {{
 *  value: unknown
 *  validationErrors: import('../../shared/validationTypes').ValidationError[]
 *  dataPath: string
 *  onUpdate(dataPath: string, update: {}): void
 *  onCollectProductIds(): Promise<void | {id: string, name: string}[]>
 *  onCollectGroupIds(): Promise<void | {id: string, name: string}[]>
 * }} props
 */
function Doc(props) {
  return (
    <ObjectContainer
      {...props}
      label="Common Security Advisory Framework"
      description="Representation of security advisory information as a JSON document."
      collapsible={false}
      deletable={false}
      defaultValue={() => ({
        document: {
          category: '',
          csaf_version: '2.0',
          publisher: {
            category: '',
            name: '',
          },
          title: '',
          tracking: {
            current_release_date: '',
            id: '',
            initial_release_date: '',
            revision_history: [
              {
                date: '',
                number: '',
                summary: '',
              },
            ],
            status: '',
            version: '',
          },
        },
      })}
    >
      {(csafProps) => (
        <>
          <Document {...csafProps('document')} />
          <ProductTree
            {...csafProps('product_tree')}
            onCollectProductIds={props.onCollectProductIds}
          />
          <Vulnerabilities
            {...csafProps('vulnerabilities')}
            onCollectGroupIds={props.onCollectGroupIds}
            onCollectProductIds={props.onCollectProductIds}
          />
        </>
      )}
    </ObjectContainer>
  )
}


<|start_filename|>app/lib/SecvisogramPage/View/FormEditorTab/Vulnerabilities/Scores.js<|end_filename|>
import React from 'react'
import ArrayContainer from '../shared/ArrayContainer'
import { Products } from '../shared/definitions'
import ObjectContainer from '../shared/ObjectContainer'
import TextAttribute from '../shared/TextAttribute'
import CVSSV3Editor from './Scores/CVSS3Editor'

/**
 * @param {{
 *  value: unknown
 *  validationErrors: import('../../../../shared/validationTypes').ValidationError[]
 *  dataPath: string
 *  onUpdate(dataPath: string, update: {}): void
 *  onCollectProductIds(): Promise<void | {id: string, name: string}[]>
 * }} props
 */
export default function Scores({ onCollectProductIds, ...props }) {
  return (
    <ArrayContainer
      {...props}
      label="List of scores"
      description="Contains score objects for the current vulnerability."
      defaultItemValue={() => ({
        products: [''],
      })}
    >
      {(scoreItemProps) => (
        <ObjectContainer
          {...scoreItemProps}
          label="Score"
          description="specifies information about (at least one) score of the vulnerability and for which products the given value applies."
        >
          {(scoreProps) => (
            <>
              <Products
                {...scoreProps('products')}
                onCollectProductIds={onCollectProductIds}
              />
              <ObjectContainer
                {...scoreProps('cvss_v2')}
                label="JSON Schema for Common Vulnerability Scoring System version 2.0"
                description=""
              >
                {(cvssV2Props) => (
                  <>
                    <TextAttribute
                      {...cvssV2Props('vectorString')}
                      label="VectorString"
                      description=""
                      canBeAdded={false}
                      readOnly
                    />
                  </>
                )}
              </ObjectContainer>
              <CVSSV3Editor {...scoreProps('cvss_v3')} />
            </>
          )}
        </ObjectContainer>
      )}
    </ArrayContainer>
  )
}


<|start_filename|>app/lib/SecvisogramPage/View/FormEditorTab/Vulnerabilities/Scores/CVSS3Editor.js<|end_filename|>
import React from 'react'
import DefaultButton from '../../../shared/DefaultButton'
import EnumAttribute from '../../shared/EnumAttribute'
import NumberAttribute from '../../shared/NumberAttribute'
import ObjectContainer from '../../shared/ObjectContainer'
import TextAttribute from '../../shared/TextAttribute'
import CVSSVector from './CVSS3Editor/CVSSVector'

/**
 * @param {{
 *  value: unknown
 *  validationErrors: import('../../../../../shared/validationTypes').ValidationError[]
 *  dataPath: string
 *  onUpdate: (dataPath: string, update: {}) => void
 * }} props
 */
export default function CVSSV3Editor(props) {
  const cvssVector = new CVSSVector(/** @type {{}} */ (props.value) || {})
  const canBeUpgraded = cvssVector.canBeUpgraded

  return (
    <ObjectContainer
      {...props}
      label="JSON Schema for Common Vulnerability Scoring System version 3.1"
      description=""
      defaultValue={() => ({
        version: '3.1',
        vectorString: '',
        baseScore: 0,
        baseSeverity: '',
        attackVector: '',
        attackComplexity: '',
        privilegesRequired: '',
        userInteraction: '',
        scope: '',
        confidentialityImpact: '',
        integrityImpact: '',
        availabilityImpact: '',
      })}
    >
      {(objectProps) => {
        const cvssV3Props = (/** @type {string} */ key) => {
          return {
            ...objectProps(key),
            onChange(/** @type {string} */ v) {
              if (key === 'vectorString') return
              const updatedCVSSMetrics = cvssVector.set(key, v)
              props.onUpdate(props.dataPath, {
                $merge: { ...updatedCVSSMetrics.data },
              })
            },
            onDelete() {
              if (key === 'vectorString') return
              const updatedCVSSMetrics = cvssVector.remove(key)
              props.onUpdate(props.dataPath, {
                $merge: { ...updatedCVSSMetrics.data },
              })
            },
          }
        }
        return (
          <>
            <EnumAttribute
              {...cvssV3Props('version')}
              label="CVSS Version"
              description="CVSS Version"
              options={['3.1', '3.0']}
              defaultValue={() => '3.1'}
            />
            <div>
              <TextAttribute
                {...cvssV3Props('vectorString')}
                label="VectorString"
                description=""
                pattern="^CVSS:3.[01]/((AV:[NALP]|AC:[LH]|PR:[UNLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XUNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/)*(AV:[NALP]|AC:[LH]|PR:[UNLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XUNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])$"
                onBlur={(e) => {
                  const updatedCVSSMetrics = cvssVector.updateFromVectorString(
                    e.target.value
                  )
                  const metrics = updatedCVSSMetrics
                  props.onUpdate(props.dataPath, {
                    $merge: { ...metrics.data },
                  })
                }}
              />
              <div className="mb-2">
                {canBeUpgraded ? (
                  <DefaultButton
                    onClick={() => {
                      props.onUpdate(props.dataPath, {
                        $merge: { ...cvssVector.updateVectorStringTo31().data },
                      })
                    }}
                  >
                    Upgrade to CVSS 3.1
                  </DefaultButton>
                ) : null}
              </div>
            </div>
            <EnumAttribute
              {...cvssV3Props('attackVector')}
              label="AttackVector"
              description=""
              options={['NETWORK', 'ADJACENT_NETWORK', 'LOCAL', 'PHYSICAL']}
            />
            <EnumAttribute
              {...cvssV3Props('attackComplexity')}
              label="AttackComplexity"
              description=""
              options={['HIGH', 'LOW']}
            />
            <EnumAttribute
              {...cvssV3Props('privilegesRequired')}
              label="PrivilegesRequired"
              description=""
              options={['HIGH', 'LOW', 'NONE']}
            />
            <EnumAttribute
              {...cvssV3Props('userInteraction')}
              label="UserInteraction"
              description=""
              options={['NONE', 'REQUIRED']}
            />
            <EnumAttribute
              {...cvssV3Props('scope')}
              label="Scope"
              description=""
              options={['UNCHANGED', 'CHANGED']}
            />
            <EnumAttribute
              {...cvssV3Props('confidentialityImpact')}
              label="ConfidentialityImpact"
              description=""
              options={['NONE', 'LOW', 'HIGH']}
            />
            <EnumAttribute
              {...cvssV3Props('integrityImpact')}
              label="IntegrityImpact"
              description=""
              options={['NONE', 'LOW', 'HIGH']}
            />
            <EnumAttribute
              {...cvssV3Props('availabilityImpact')}
              label="AvailabilityImpact"
              description=""
              options={['NONE', 'LOW', 'HIGH']}
            />
            <NumberAttribute
              {...cvssV3Props('baseScore')}
              label="BaseScore"
              description=""
              step="0.01"
              readOnly
            />
            <TextAttribute
              {...cvssV3Props('baseSeverity')}
              label="BaseSeverity"
              description=""
              readOnly
            />
            <EnumAttribute
              {...cvssV3Props('exploitCodeMaturity')}
              label="ExploitCodeMaturity"
              description=""
              options={[
                'UNPROVEN',
                'PROOF_OF_CONCEPT',
                'FUNCTIONAL',
                'HIGH',
                'NOT_DEFINED',
              ]}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('remediationLevel')}
              label="RemediationLevel"
              description=""
              options={[
                'OFFICIAL_FIX',
                'TEMPORARY_FIX',
                'WORKAROUND',
                'UNAVAILABLE',
                'NOT_DEFINED',
              ]}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('reportConfidence')}
              label="ReportConfidence"
              description=""
              options={['UNKNOWN', 'REASONABLE', 'CONFIRMED', 'NOT_DEFINED']}
              deletable
            />
            <NumberAttribute
              {...cvssV3Props('temporalScore')}
              label="TemporalScore"
              description=""
              step="0.01"
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('temporalSeverity')}
              label="TemporalSeverity"
              description=""
              options={['NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL']}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('confidentialityRequirement')}
              label="ConfidentialityRequirement"
              description=""
              options={['LOW', 'MEDIUM', 'HIGH', 'NOT_DEFINED']}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('integrityRequirement')}
              label="IntegrityRequirement"
              description=""
              options={['LOW', 'MEDIUM', 'HIGH', 'NOT_DEFINED']}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('availabilityRequirement')}
              label="AvailabilityRequirement"
              description=""
              options={['LOW', 'MEDIUM', 'HIGH', 'NOT_DEFINED']}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('modifiedAttackVector')}
              label="ModifiedAttackVector"
              description=""
              options={[
                'NETWORK',
                'ADJACENT_NETWORK',
                'LOCAL',
                'PHYSICAL',
                'NOT_DEFINED',
              ]}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('modifiedAttackComplexity')}
              label="ModifiedAttackComplexity"
              description=""
              options={['HIGH', 'LOW', 'NOT_DEFINED']}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('modifiedPrivilegesRequired')}
              label="ModifiedPrivilegesRequired"
              description=""
              options={['HIGH', 'LOW', 'NONE', 'NOT_DEFINED']}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('modifiedUserInteraction')}
              label="ModifiedUserInteraction"
              description=""
              options={['NONE', 'REQUIRED', 'NOT_DEFINED']}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('modifiedScope')}
              label="ModifiedScope"
              description=""
              options={['UNCHANGED', 'CHANGED', 'NOT_DEFINED']}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('modifiedConfidentialityImpact')}
              label="ModifiedConfidentialityImpact"
              description=""
              options={['NONE', 'LOW', 'HIGH', 'NOT_DEFINED']}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('modifiedIntegrityImpact')}
              label="ModifiedIntegrityImpact"
              description=""
              options={['NONE', 'LOW', 'HIGH', 'NOT_DEFINED']}
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('modifiedAvailabilityImpact')}
              label="ModifiedAvailabilityImpact"
              description=""
              options={['NONE', 'LOW', 'HIGH', 'NOT_DEFINED']}
              deletable
            />
            <NumberAttribute
              {...cvssV3Props('environmentalScore')}
              label="EnvironmentalScore"
              description=""
              step="0.01"
              deletable
            />
            <EnumAttribute
              {...cvssV3Props('environmentalSeverity')}
              label="EnvironmentalSeverity"
              description=""
              options={['NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL']}
              deletable
            />
          </>
        )
      }}
    </ObjectContainer>
  )
}


<|start_filename|>app/lib/SecvisogramPage/View/FormEditorTab/shared/DateAttribute/DatePicker.js<|end_filename|>
import React from 'react'

const DatePicker = (
  /**
   * @type {Omit<React.HTMLProps<HTMLInputElement>, 'value' | 'onChange'> & {
   *   value: string | null
   *   onChange(str: string | null): void
   * }}
   */
  { onChange, value, ...props }
) => {
  const [dateString, timeString] = React.useMemo(() => {
    if (!value) return ['', '']
    const date = new Date(value)
    return [
      formatDate(date),
      date.getHours().toString().padStart(2, '0') +
        ':' +
        date.getMinutes().toString().padStart(2, '0'),
    ]
  }, [value])

  return (
    <div className="flex items-center">
      <input
        {...props}
        className="border border-gray-400 py-1 px-2 w-full shadow-inner rounded-l"
        type="date"
        value={dateString}
        onChange={(e) => {
          onChange(
            e.target.value
              ? new Date(
                  `${e.target.value} ${timeString || '12:00'}`
                ).toISOString()
              : ''
          )
        }}
      />
      <input
        {...props}
        className="border border-gray-400 py-1 px-2 w-full shadow-inner rounded-r"
        type="time"
        value={timeString}
        onChange={(e) => {
          onChange(
            new Date(
              `${dateString || formatDate(new Date())} ${
                e.target.value || '00:00'
              }`
            ).toISOString()
          )
        }}
      />
    </div>
  )
}

export default DatePicker

/**
 * @param {Date} date
 */
function formatDate(date) {
  return (
    date.getFullYear().toString().padStart(4, '0') +
    '-' +
    (date.getMonth() + 1).toString().padStart(2, '0') +
    '-' +
    date.getDate().toString().padStart(2, '0')
  )
}


<|start_filename|>app/lib/SecvisogramPage/View/FormEditorTab/shared/ObjectContainer.js<|end_filename|>
import { parse } from 'json-pointer'
import React from 'react'
import Container from './shared/Container'

/** @type {import('../../../../SecvisogramPage').ValidationError[]} */
const EMPTY_ARRAY = []

/**
 * @typedef {Object} ChildProps
 * @property {unknown} value
 * @property {string} dataPath
 * @property {import('../../../../shared/validationTypes').ValidationError[]} validationErrors
 * @property {(dataPath: string, update: {}) => void} onUpdate
 */

/**
 * Calculates the child props.
 *
 * @param {{
 *  value: unknown
 *  label: string
 *  description: string
 *  deletable?: boolean
 *  collapsible?: boolean
 *  validationErrors: import('../../../../shared/validationTypes').ValidationError[]
 *  dataPath: string
 *  defaultValue?(): {}
 *  onDelete?(): void
 *  onUpdate: (dataPath: string, update: {}) => void
 *  children(props: (attributeName: string) => ChildProps): React.ReactNode;
 * }} props
 */
export default function ObjectContainer(props) {
  /**
   * @param {unknown} v
   * @returns {v is { [key: string]: unknown }}
   */
  const isValid = (v) =>
    Boolean(v && typeof v === 'object' && !Array.isArray(v))

  /**
   * Is a memoized map of all validation errors for its children.
   */
  const validationErrorCache = React.useMemo(() => {
    /** @type {Map<string, import('../../../../SecvisogramPage').ValidationError[]>} */
    const c = new Map()
    for (const e of props.validationErrors) {
      if (e.dataPath.startsWith(props.dataPath + '/')) {
        const attributeName = parse(e.dataPath)[parse(props.dataPath).length]
        c.set(attributeName, (c.get(attributeName) ?? []).concat([e]))
      }
    }
    return c
  }, [props.dataPath, props.validationErrors])

  return (
    <Container {...props} isValid={isValid}>
      {(childProps) =>
        props.children((attributeName) => ({
          ...childProps,
          dataPath: `${props.dataPath}/${attributeName}`,
          value: childProps.value[attributeName],
          onUpdate: props.onUpdate,
          validationErrors:
            validationErrorCache.get(attributeName) ?? EMPTY_ARRAY,
        }))
      }
    </Container>
  )
}


<|start_filename|>app/lib/SecvisogramPage/View/FormEditorTab/Document/AggregateSeverity.js<|end_filename|>
import '@reach/combobox/styles.css'
import React from 'react'
import ObjectContainer from '../shared/ObjectContainer'
import TextAttribute from '../shared/TextAttribute'
import validationErrorShallowEqual from '../shared/validationErrorShallowEqual'

export default React.memo(
  /**
   * @param {{
   *  validationErrors: import('../../../../shared/validationTypes').ValidationError[]
   *  dataPath: string
   *  value: unknown
   *  onUpdate(dataPath: string, update: {}): void
   * }} props
   */
  function AggregateSeverity({
    value: aggregateSeverity,
    validationErrors,
    dataPath,
    onUpdate,
  }) {
    return (
      <ObjectContainer
        label="Aggregate severity"
        description="Is a vehicle that is provided by the document producer to convey the urgency and criticality with which the one or more vulnerabilities reported should be addressed. It is a document-level metric and applied to the document as a whole — not any specific vulnerability. The range of values in this field is defined according to the document producer's policies and procedures."
        validationErrors={validationErrors}
        dataPath={dataPath}
        value={aggregateSeverity}
        defaultValue={() => ({
          text: '',
        })}
        onUpdate={onUpdate}
      >
        {(aggregateSeverityProps) => (
          <>
            <TextAttribute
              {...aggregateSeverityProps('namespace')}
              label="Namespace of aggregate severity"
              description="Points to the namespace so referenced."
              type="url"
              deletable
            />
            <TextAttribute
              {...aggregateSeverityProps('text')}
              label="Text of aggregate severity"
              description="Provides a severity which is independent of - and in addition to - any other standard metric for determining the impact or severity of a given vulnerability (such as CVSS)."
              placeholder="Moderate"
            />
          </>
        )}
      </ObjectContainer>
    )
  },
  validationErrorShallowEqual
)


<|start_filename|>bin/cli/generatePreviewTemplatingTable.js<|end_filename|>
const jsonPtr = require('json-pointer')
const { resolve } = require('path')

/**
 * @param {{
 *    csaf20Schema: string
 *    cvss31Schema: string
 *    cvss20Schema: string
 *  }} args
 */
module.exports = function generatePreviewTemplatingTable(args) {
  const rootSchema = require(resolve(args.csaf20Schema))
  const cvss3Schema = require(resolve(args.cvss31Schema))
  const cvss2Schema = require(resolve(args.cvss20Schema))
  Object.assign(
    rootSchema.definitions,
    cvss3Schema.definitions,
    cvss2Schema.definitions
  )

  /** @typedef {{ path: string; schema: any; items?: Array<Entry>; depth: number }} Entry */

  /**
   * @param {any} schema
   * @param {string[]} dataPath
   * @param {number} depth
   * @returns {Array<Entry>}
   */
  function generateSchemaPaths(schema, dataPath = [], depth = 1) {
    const path = dataPath.length ? dataPath.join('.') : '.'
    if (depth > 10) return [{ path, schema, depth }]
    switch (schema.type) {
      case 'object':
        return [
          { path, schema, depth },
          ...Object.entries(schema.properties || {}).flatMap(([key, value]) =>
            generateSchemaPaths(value, dataPath.concat([key]), depth + 1)
          ),
        ]
      case 'array':
        return [
          {
            path,
            schema,
            items: generateSchemaPaths(schema.items, [], depth + 1),
            depth,
          },
        ]
      default:
        if (schema.$ref && schema.$ref.startsWith('#')) {
          return generateSchemaPaths(
            jsonPtr.get(rootSchema, schema.$ref.slice(1)),
            dataPath,
            depth
          )
        }
        if (
          schema.oneOf?.find(
            (/** @type {any} */ s) =>
              s.$ref === 'https://www.first.org/cvss/cvss-v3.1.json'
          )
        ) {
          return generateSchemaPaths(cvss3Schema, dataPath, depth)
        }
        if (schema.$ref === 'https://www.first.org/cvss/cvss-v2.0.json') {
          return generateSchemaPaths(cvss2Schema, dataPath, depth)
        }
        return [{ schema, path, depth }]
    }
  }

  /**
   * @param {Array<Entry>} entries
   * @param {string} parentPath
   * @returns {string}
   */
  function generateTable(entries, parentPath = '') {
    return entries.reduce((markdown, entry) => {
      const depth = entry.depth
      const key =
        parentPath.length > 0
          ? parentPath + (entry.path != '.' ? '.' + entry.path : '')
          : entry.path

      switch (entry.schema.type) {
        case 'string':
          return (
            markdown +
            `| \`${key}\` | ` +
            (entry.schema.description ? `${entry.schema.description} ` : '') +
            `| ` +
            (entry.schema.examples
              ? `${entry.schema.examples.join(', ')} `
              : '') +
            `|\n`
          )

        case 'object':
          return (
            markdown +
            `| \`${key}\` | ` +
            (entry.schema.description ? `${entry.schema.description} ` : '') +
            `| |\n`
          )

        case 'array':
          return (
            markdown +
            `| \`${key}\` | ${entry.schema.description}| |\n` +
            (entry.items ? `${generateTable(entry.items, key + '[]')}` : '')
          )

        default:
          return markdown
      }
    }, '')
  }

  console.log(
    `| Attribute                                                          | Description                                                                                                                                                                                                                                                                                                                                                                             | Example value                                                                                                                                                                                           |\n` +
      `| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n` +
      `${generateTable(generateSchemaPaths(rootSchema, []))}`
  )
}


<|start_filename|>app/lib/SecvisogramPage/View/FormEditorTab/shared/CweAttribute.js<|end_filename|>
import {
  Combobox,
  ComboboxInput,
  ComboboxList,
  ComboboxOption,
  ComboboxPopover,
} from '@reach/combobox'
import { matchSorter } from 'match-sorter'
import React from 'react'
import cwec_4_3 from '../../../../shared/Core/cwec_4.3.json'
import useDebounce from '../../shared/useDebounce'
import ObjectContainer from './ObjectContainer'
import Attribute from './shared/Attribute'

/**
 * Custom attribute.
 *
 * @param {{
 *  validationErrors: import('../../../../shared/validationTypes').ValidationError[]
 *  dataPath: string
 *  value: unknown
 *  onUpdate: (dataPath: string, update: {}) => void
 * }} props
 */
export default function CweAttribute(props) {
  return (
    <>
      <ObjectContainer
        {...props}
        label="CWE"
        description="Holds the MITRE standard Common Weakness Enumeration (CWE) for the weakness associated."
        defaultValue={() => ({
          id: '',
          name: '',
        })}
      >
        {(cweProps) => {
          const onChange = (
            /** @type {{id: string, name: string}} */ { id, name }
          ) => {
            props.onUpdate(props.dataPath, {
              $set: {
                id: id,
                name: name,
              },
            })
          }
          return (
            <>
              <CwecId {...cweProps('id')} onChange={onChange} />
              <CwecName {...cweProps('name')} onChange={onChange} />
            </>
          )
        }}
      </ObjectContainer>
    </>
  )
}

/**
 * @param {string} term
 * @returns {any[] | null}
 */
function useCwecMatch(term) {
  const throttledTerm = useDebounce(term, 100)
  return React.useMemo(
    () =>
      throttledTerm.trim() === ''
        ? null
        : matchSorter(cwec_4_3.weaknesses, throttledTerm, {
            keys: [(item) => `${item.id}, ${item.name}`],
          }),
    [throttledTerm]
  )
}

/**
 * @param {{
 *  validationErrors: import('../../../../shared/validationTypes').ValidationError[]
 *  dataPath: string
 *  value: unknown
 *  onUpdate: (dataPath: string, update: {}) => void
 *  onChange({}): void
 * }} props
 */
function CwecId(props) {
  const [value, setValue] = React.useState(/** @type string */ (props.value))
  const [term, setTerm] = React.useState(/** @type string */ (props.value))
  const results = useCwecMatch(term)
  /** @param {React.ChangeEvent<HTMLInputElement>} event  */
  const handleChange = (event) => {
    setValue(event.target.value)
    setTerm(event.target.value)
  }

  /** @param {string} id  */
  const handleSelect = (id) => {
    setTerm('')
    const name = cwec_4_3.weaknesses.find((w) => w.id === id)?.name
    props.onChange({ id: id, name: name })
  }

  React.useEffect(() => {
    setValue(/** @type string */ (props.value))
    setTerm('')
  }, [props.value])

  return (
    <Attribute
      label="Weakness ID"
      description="Holds the ID for the weakness associated."
      {...props}
    >
      {() => (
        <div className="max-w-md flex">
          <div className="w-full">
            <Combobox className="w-full" onSelect={handleSelect}>
              <ComboboxInput
                value={value}
                className="border border-gray-400 py-1 px-2 w-full shadow-inner rounded"
                pattern="^CWE-[1-9]\d{0,5}$"
                placeholder="^CWE-[1-9]\d{0,5}$"
                required={true}
                onChange={handleChange}
              />
              {results && (
                <ComboboxPopover className="shadow-popup">
                  {results.length > 0 ? (
                    <ComboboxList>
                      {results.slice(0, 10).map((result, index) => (
                        <ComboboxOption key={index} value={result.id}>
                          {`${result.id}, ${result.name}`}
                        </ComboboxOption>
                      ))}
                    </ComboboxList>
                  ) : (
                    <span style={{ display: 'block', margin: 8 }}>
                      No results found
                    </span>
                  )}
                </ComboboxPopover>
              )}
            </Combobox>
          </div>
        </div>
      )}
    </Attribute>
  )
}

/**
 * @param {{
 *  validationErrors: import('../../../../shared/validationTypes').ValidationError[]
 *  dataPath: string
 *  value: unknown
 *  onUpdate: (dataPath: string, update: {}) => void
 *  onChange({}): void
 * }} props
 */
function CwecName(props) {
  const [value, setValue] = React.useState(/** @type string */ (props.value))
  const [term, setTerm] = React.useState(/** @type string */ (props.value))
  const results = useCwecMatch(term)

  /** @param {React.ChangeEvent<HTMLInputElement>} event  */
  const handleChange = (event) => {
    setValue(event.target.value)
    setTerm(event.target.value)
  }

  /** @param {string} name  */
  const handleSelect = (name) => {
    setTerm('')
    const id = cwec_4_3.weaknesses.find((w) => w.name === name)?.id
    props.onChange({ id: id, name: name })
  }

  React.useEffect(() => {
    setValue(/** @type string */ (props.value))
    setTerm('')
  }, [props.value])

  return (
    <Attribute
      label="Weakness name"
      description="Holds the full name of the weakness as given in the CWE specification."
      {...props}
    >
      {() => (
        <div className="max-w-md flex">
          <div className="w-full">
            <Combobox className="w-full" onSelect={handleSelect}>
              <ComboboxInput
                value={value}
                className="border border-gray-400 py-1 px-2 w-full shadow-inner rounded"
                placeholder="Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') ..."
                required={true}
                onChange={handleChange}
              />
              {results && (
                <ComboboxPopover className="shadow-popup">
                  {results.length > 0 ? (
                    <ComboboxList>
                      {results.slice(0, 10).map((result, index) => (
                        <ComboboxOption key={index} value={result.name}>
                          {`${result.id}, ${result.name}`}
                        </ComboboxOption>
                      ))}
                    </ComboboxList>
                  ) : (
                    <span style={{ display: 'block', margin: 8 }}>
                      No results found
                    </span>
                  )}
                </ComboboxPopover>
              )}
            </Combobox>
          </div>
        </div>
      )}
    </Attribute>
  )
}


<|start_filename|>app/lib/SecvisogramPage/View/FormEditorTab/Document/Distribution.js<|end_filename|>
import '@reach/combobox/styles.css'
import React from 'react'
import ObjectContainer from '../shared/ObjectContainer'
import TextAreaAttribute from '../shared/TextAreaAttribute'
import Tlp from './Distribution/Tlp'

/**
 * @param {{
 *  validationErrors: import('../../../../shared/validationTypes').ValidationError[]
 *  dataPath: string
 *  value: unknown
 *  onUpdate(dataPath: string, update: {}): void
 * }} props
 */
export default function Distribution({
  value: distribution,
  validationErrors,
  dataPath,
  onUpdate,
}) {
  return (
    <ObjectContainer
      label="Rules for sharing document"
      description="Describe any constraints on how this document might be shared."
      validationErrors={validationErrors}
      dataPath={dataPath}
      value={distribution}
      defaultValue={() => ({})}
      onUpdate={onUpdate}
    >
      {(distributionProps) => (
        <>
          <TextAreaAttribute
            {...distributionProps('text')}
            label="Textual description"
            description="Provides a textual description of additional constraints."
            placeholder="Share only on a need-to-know-basis only."
            deletable
          />
          <Tlp {...distributionProps('tlp')} />
        </>
      )}
    </ObjectContainer>
  )
}
