@snyk/cocoapods-lockfile-parser
Version: 
Generate a Snyk dependency graph from a Podfile.lock file
197 lines • 8.67 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
const dep_graph_1 = require("@snyk/dep-graph");
const utils_1 = require("./utils");
class LockfileParser {
    static async readFile(lockfilePath) {
        const rootName = path.basename(path.dirname(path.resolve(lockfilePath)));
        return new Promise((resolve, reject) => {
            fs.readFile(lockfilePath, { encoding: 'utf8' }, (err, fileContents) => {
                if (err) {
                    reject(err);
                }
                try {
                    const parser = this.readContents(fileContents, {
                        name: rootName,
                        version: '0.0.0',
                    });
                    resolve(parser);
                }
                catch (err) {
                    reject(err);
                }
            });
        });
    }
    static readFileSync(lockfilePath) {
        const fileContents = fs.readFileSync(lockfilePath, 'utf8');
        const rootName = path.basename(path.dirname(path.resolve(lockfilePath)));
        return this.readContents(fileContents, {
            name: rootName,
            version: '0.0.0',
        });
    }
    static readContents(contents, rootPkgInfo) {
        return new LockfileParser(yaml.safeLoad(contents), rootPkgInfo);
    }
    constructor(hash, rootPkgInfo) {
        this.rootPkgInfo = undefined;
        this.rootPkgInfo = rootPkgInfo;
        this.internalData = hash;
    }
    toDepGraph() {
        const builder = new dep_graph_1.DepGraphBuilder(this.pkgManager, this.rootPkgInfo);
        const allDeps = {};
        // Add all package nodes first, but collect dependencies
        this.internalData.PODS.forEach((elem) => {
            let pkgInfo;
            let pkgDeps;
            if (typeof elem === 'string') {
                // When there are NO dependencies. This equals in yaml e.g.
                //    - Expecta (1.0.5)
                pkgInfo = (0, utils_1.pkgInfoFromSpecificationString)(elem);
                pkgDeps = [];
            }
            else {
                // When there are dependencies. This equals in yaml e.g.
                //    - React/Core (0.59.2):
                //      - yoga (= 0.59.2.React)
                const objKey = Object.keys(elem)[0];
                pkgInfo = (0, utils_1.pkgInfoFromSpecificationString)(objKey);
                pkgDeps = elem[objKey].map(utils_1.pkgInfoFromDependencyString);
            }
            const nodeId = this.nodeIdForPkgInfo(pkgInfo);
            builder.addPkgNode(pkgInfo, nodeId, {
                labels: this.nodeInfoLabelsForPod(pkgInfo.name),
            });
            allDeps[nodeId] = pkgDeps;
        });
        // Connect explicitly in the manifest (`Podfile`)
        // declared dependencies to the root node.
        this.internalData.DEPENDENCIES.map(utils_1.pkgInfoFromDependencyString).forEach((pkgInfo) => {
            builder.connectDep(builder.rootNodeId, this.nodeIdForPkgInfo(pkgInfo));
        });
        // Now we can start to connect dependencies
        Object.entries(allDeps).forEach(([nodeId, pkgDeps]) => pkgDeps.forEach((pkgInfo) => {
            const depNodeId = this.nodeIdForPkgInfo(pkgInfo);
            if (!allDeps[depNodeId]) {
                // The pod is not a direct dependency of any targets of the integration,
                // which can happen for platform-specific transitives, when their platform
                // is not used in any target. (e.g. PromiseKit/UIKit is iOS-specific and is
                // a transitive of PromiseKit, but won't be included for a macOS project.)
                return;
            }
            builder.connectDep(nodeId, depNodeId);
        }));
        return builder.build();
    }
    /// CocoaPods guarantees that every pod is only present in one version,
    /// so we can use just the pod name as node ID.
    nodeIdForPkgInfo(pkgInfo) {
        return pkgInfo.name;
    }
    /// Gathers relevant info from the lockfile and transform
    /// them into the expected labels data structure.
    nodeInfoLabelsForPod(podName) {
        let nodeInfoLabels = {
            checksum: this.checksumForPod(podName),
        };
        const repository = this.repositoryForPod(podName);
        if (repository) {
            nodeInfoLabels = Object.assign(Object.assign({}, nodeInfoLabels), { repository });
        }
        const externalSourceInfo = this.externalSourceInfoForPod(podName);
        if (externalSourceInfo) {
            nodeInfoLabels = Object.assign(Object.assign({}, nodeInfoLabels), { externalSourcePodspec: externalSourceInfo[':podspec'], externalSourcePath: externalSourceInfo[':path'], externalSourceGit: externalSourceInfo[':git'], externalSourceTag: externalSourceInfo[':tag'], externalSourceCommit: externalSourceInfo[':commit'], externalSourceBranch: externalSourceInfo[':branch'] });
        }
        const checkoutOptions = this.checkoutOptionsForPod(podName);
        if (checkoutOptions) {
            nodeInfoLabels = Object.assign(Object.assign({}, nodeInfoLabels), { checkoutOptionsPodspec: checkoutOptions[':podspec'], checkoutOptionsPath: checkoutOptions[':path'], checkoutOptionsGit: checkoutOptions[':git'], checkoutOptionsTag: checkoutOptions[':tag'], checkoutOptionsCommit: checkoutOptions[':commit'], checkoutOptionsBranch: checkoutOptions[':branch'] });
        }
        // Sanitize labels by removing null fields
        // (as they don't survive a serialization/parse cycle and break tests)
        Object.entries(nodeInfoLabels).forEach(([key, value]) => {
            if (value === null || value === undefined) {
                delete nodeInfoLabels[key];
            }
        });
        return nodeInfoLabels;
    }
    /// The checksum of the pod.
    checksumForPod(podName) {
        const rootName = (0, utils_1.rootSpecName)(podName);
        return this.internalData['SPEC CHECKSUMS'][rootName];
    }
    /// This can be either an URL or the local repository name.
    repositoryForPod(podName) {
        // Older Podfile.lock might not have this section yet.
        const specRepos = this.internalData['SPEC REPOS'];
        if (!specRepos) {
            return undefined;
        }
        const rootName = (0, utils_1.rootSpecName)(podName);
        const specRepoEntry = Object.entries(specRepos).find(([, deps]) => deps.includes(rootName));
        if (specRepoEntry) {
            return specRepoEntry[0];
        }
        return undefined;
    }
    /// Extracts the external source info for a given pod, if there is any.
    externalSourceInfoForPod(podName) {
        // Older Podfile.lock might not have this section yet.
        const externalSources = this.internalData['EXTERNAL SOURCES'];
        if (!externalSources) {
            return undefined;
        }
        const externalSourceEntry = externalSources[(0, utils_1.rootSpecName)(podName)];
        if (externalSourceEntry) {
            return externalSourceEntry;
        }
        return undefined;
    }
    /// Extracts the checkout options for a given pod, if there is any.
    checkoutOptionsForPod(podName) {
        // Older Podfile.lock might not have this section yet.
        const checkoutOptions = this.internalData['CHECKOUT OPTIONS'];
        if (!checkoutOptions) {
            return undefined;
        }
        const checkoutOptionsEntry = checkoutOptions[(0, utils_1.rootSpecName)(podName)];
        if (checkoutOptionsEntry) {
            return checkoutOptionsEntry;
        }
        return undefined;
    }
    get repositories() {
        // Older Podfile.lock might not have this section yet.
        const specRepos = this.internalData['SPEC REPOS'];
        if (!specRepos) {
            return [];
        }
        return Object.keys(specRepos).map((nameOrUrl) => {
            return { alias: nameOrUrl };
        });
    }
    get pkgManager() {
        return {
            name: 'cocoapods',
            version: this.cocoapodsVersion,
            repositories: this.repositories,
        };
    }
    /// The CocoaPods version encoded in the lockfile which was used to
    /// create this resolution.
    get cocoapodsVersion() {
        return this.internalData.COCOAPODS || 'unknown';
    }
    /// The checksum of the Podfile, which was used when resolving this integration.
    /// - Note: this was not tracked by earlier versions of CocoaPods.
    get podfileChecksum() {
        return this.internalData['PODFILE CHECKSUM'];
    }
}
exports.default = LockfileParser;
//# sourceMappingURL=lockfile-parser.js.map